-- Leo's gemini proxy

-- Connecting to gemi.dev:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini

Where in the World? A new geography game in Gemini space

2022-08-04 | #game | @Acidus


Earlier this week I created Where in the World?, a daily geography puzzle game where you have to guess the country from a picture.


🗺 Where In The World?


Every day, a new country or territory is selected. You have 6 guesses to name it. The game resets every day. Everyone gets the same puzzle, so you can play with or compete with people, and can share a spolier-free summary of your game via Station. Here is my summary for today's puzzle, where I guessed the country in 3 turns:


Where In The World? • Puzzle #4 • 2022-08-04

✅✅✅✅❌⬅️

✅✅✅✅❌↖️

✅✅✅✅✅🎉

gemini://gemi.dev/cgi-bin/witw.cgi/play


How to play Where In The World


Where In The World? (WITW) shows you an ASCII art image of a country and you try to guess it. As you select different countries or territories, WITW gives you feedback saying how close your guess was, and the direction you need to go from the country you guessed to the actual country. It looks like this:


Guess 1. Slovakia | 1114 km | ⬅️ | 94 %


I guessed "Slovakia" as my first guess. I was wrong, but I was close. The actual country is ~1100 km to the west of Slovakia. I was 94% of the way there. You use this feedback to get closer and closer, until you get the guess the right country. You can read the full instructions below:

How to play Where In The World?


Why I built it


I suck at word games like scrabble, crosswords, and the like. So Gemini native games like Spellbinding, while cool, are not appealing to me. I am, however, good at geography, and have been playing Worldle with friends for a few months:


Worldle


The mechanics of Worldle were really simple, and I wondered if I could port it to Gemini. This became WITW.


How I built it


There were 3 challenges I had to overcome when creating Where In The World?

How to show a detailed enough map of the country or territory.

How to get input from the user about what country they are guessing.

How to maintain game state, since I planned to build it as a CGI program that starts fresh with each request.


Drawing countries in Gemini clients


The first challenge is especially important. The entire game is built around showing a picture of a country. If the country cannot be displayed easily, or in enough detail, the game will be frustrating to play. Gemini clients vary a lot in capabilities. A few will show an image inline in the content (Elaho, Lagrange), others show an image as a pop-over window (Buran), and others don't show images inside the client at all, forcing you to download and view the image in another program.


I know relying on just an image of the country would be a bad experience, so I started looking different ASCII art converts. Luckily, the Worldle git repo had all the country and territory images as SVGs.


SVG maps of countries in Worldle via GitHub.


I downloaded these, converted them to PNG, and started testing different ASCII art converters. Initial results were not good. Here are a few different converters showing Germany:

MMMMMMMMMMMMMN0Oo:clldKMMMMMMMMMMMMMMMWNWMMMMMMMMM
MMMMMMMMMMMMMWN0c.    :k0KKKOXMMMMXkxxl;oXMMMMMMMM
MMMMMMMMMMMMMMMXo.       ..;dXKkoc'    .:dKWMMMMMM
MMMMMMMMWNNXNWNkc'         .',.          .;OWMMMMM
MMMMMMMXo,..,ol.                           ,OMMMMM
MMMMMMMNl                                   oWMMMM
MMMMMMMNo                                  ,0MMMMM
MMMMMW0l.                                  .:OWMMM
MMMMMWKl.                                    cNMMM
MMWXK0o.                                     '0MMM
MMXl..                                       'OMMM
MMWx.                                         ,OWM
MMK;                                        ...dWM
MMK:                                    .,cx0OkNMM
MMMK;                              .;:oOXWMMMMMMMM
MMMO'                             :0WMMMMMMMMMMMMM
MMMXl.                            .xWMMMMMMMMMMMMM
MMMMK;                             ,kNMMMMMMMMMMMM
MMMMW0l;,'..                        .,oKWMMMMMMMMM
MMMMMMMWWNX0o.                         .cOWMMMMMMM
MMMMMMMMMMWk'                          .,xWMMMMMMM
MMMMMMMMMM0,                         'ckNMMMMMMMMM
MMMMMMMMMWd.                        .xWMMMMMMMMMMM
MMMMMMMMMWx,'';oolc,..   ...  .',:lc;cKMMMMMMMMMMM
MMMMMMMMMMWXXNNWMMMX00l.c00d:ckXWWMMNKNMMMMMMMMMMM

The first thing I learned is that I wanted to show a negative image:



                 -::=-:.:.
                 .::*@@@@@%-                 .=:.
                   -+@@@@@@%++=:-*-      +#**+@#-
                    :%@@@@@@@@@@#-  :++*@@@@@@%=+=.
                   =-*%%@@@@@@@@#%%#@@@@@@@@@@@@@%*-
          :*####::=@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@%.
          +@@@@@%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#
           %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+
          -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
        =*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
         .%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.
        :#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*
    +#*#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-
    :%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#.
     #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-
   .*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@##@@.
    +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%#+-  =:
     -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%%*-:
     =@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%*:
     %@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#-
      -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#
      -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@*.
       .**%%%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%=
             .::*@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+-
               =%@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@-
              *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%:.
             -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@#-:
             *@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@.
             *%%%%*:==+*@@@@@@@@@@@@@%##=++#%.
                        .. -%%..:++-.       :

This was better, but using actual ASCII characters was a little too limited. The coastlines and boarders were not sharp or crisp, making it very difficult to determine the country. The "busyness" of all the different characters was distracting as well. If you render the images very wide, like 150-200 characters, it was better, but this was too large for a Gemini client. You were too "zoomed in" and couldn't really get a gauge of what you are looking at, making it frustrating to guess the country. I wanted the text art no wider than about 60 characters.


From all my work on Gemipedia, specifically working with math symbols, subscripts, and superscripts, I knew that there was a much wider range of Unicode characters that could be used which might make the pictures more detailed.


I found a project that used Unicode block drawing characters. Each "character" consisted for a 2 by 2 grid of dots, with 16 different combinations of those individual dots being present or not. This provided a kind of "subpixel", making each character act like 4 pixels.


Drewish: Image to Unicode


                 ▐ ▗▖▖
                 ▗ ▝██████▟                   ▗
                     ▜████▛▙▖    ▄▖      ▗▖  ▚██▘
                   ▀▜███████▟██▄█▌      ▄████▟█▀▀
                    ▐▐██████████▀   ▗██████████▄█▙▗
                   ▄▄██▛██████████████████████████▀▘
          ▗▄▄▄██▖▄▝█████▟▜██████████████████████████
          ██████▄▟▙█████████████████████████████████▌
           █████████████████████████████████████████▛
           ████████████████████████████████████████▛▘
          ▐████████████████████████████████████████▙▖
        ▟████████████████████████████████████████████▙
          ███████████████████████████████████████████▙
        ▗▄████████████████████████████████████████████▌
     ▖▗▄▄█████████████████████████████████████████████▌
    ▜█████████████████████████████████████████████████▄
     ▜████████████████████████████████████████████████▙▄
    ▗▟██████████████████████████████████████████████████▖
   ▗▄███████████████████████████████████████████████▜███▘
    ▟██████████████████████████████████████████████▀▘ ▜▘
     ▜████████████████████████████████████████▛▛▘
      ████████████████████████████████████▀▀▀
     ▟█████████████████████████████████▙▝
     ▜███████████████████████████████████▄
      ▝██████████████████████████████████▌
      ▐███████████████████████████████████▌
       ▝███████████████████████████████████▙▄
        ▝ ▀▀▀▜███████████████████████████████▙▄
                ▗███████████████████████████████▄
              ▗▟█████████████████████████████████▌
              ▐████████████████████████████████ ▝
             ▗██████████████████████████████▀▀▘
             ▐█████████████████████████████▙
             ▟████▛▜████████████████████████▌
             ▜▀▜▀▀▘▘  ▝▀▜█▜███▛█████▛▀▀▀ ▝▀▐█
                            ▜▛  ▝▀▀

This was better. The uniformity of the characters make the art less distracting, and the sub pixels helped to provide more detail to the coastlines and borders. But I felt it was still too blocky. I looked for other options.


Finally, I found ascii-image-converter, which includes an option to use Braille characters. Each Braille character was a grid of 2 by 4 dots, allowing for denser "subpixels", which meant the borders could be more detailed.


⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⠁⢠⣤⣄⣀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠠⠐⠀⠻⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣬⣿⣿⣿⣿⣿⣿⣶⣦⣤⣄⣀⣠⡜⠓⠀⠀⠀⠀⠀⠀⣴⣶⣶⣶⣸⣿⡿⢥⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠃⠀⠀⢠⣤⣤⣴⣿⣿⣿⣿⣿⣿⣯⣤⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⣀⣙⣿⡻⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⠾⠆⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣴⣶⣶⣶⣶⡄⣠⡘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠛⢿⣿⣿⣿⣿⣷⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⣰⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡧⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣴⣶⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡃⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀
⠀⠀⠀⠠⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⣿⣿⠁⠀⠀⠀
⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⠛⠋⠁⠀⠙⠁⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡿⠟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⠻⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠉⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠺⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⠿⠿⠿⡿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⢛⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣿⣿⡿⢿⣯⠌⠛⠛⠛⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠿⠿⠛⠻⠟⢻⣧⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠀⠈⢿⡿⠀⠈⠘⠛⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀

ascii-image-converter via GitHub


I decided this was probably the best I could do with text. I wrote a quick script which converted all the country SVG images into PNGs, and those into Braille ASCII art. As a fall back I could include a link to a high resolution image, but the text art would be enough for the user to play. With that out of the way I could move on the next challenge.


Getting input from the user

WITW has 242 countries and territories. In Worldle, the interface uses an autocompleting text box powered by JavaScript. Clearly this was not an option for Gemini. With 242 countries, I could not just present the user a free-form text box for them to fill in via a 10 INPUT response code. That would create too much annoyance and confusion for the user:

Have to handle misspellings

Have to tell the user what valid input is, which would probably just be a big list anyway.

Typing in long country or territory name is tedious and error prone, making the game annoying.


Additionally, Gemini's 10 INPUT response code will cause the client to resend a request and replace the query string of the URL with what the user input. This means that you need to store game state somewhere else (see next section), which adds complexity.


Frustrated, I stepped back and thought more about about how I played Worldle. I would look at picture, and start to think where it might be. I usually knew roughly the country I wanted to guess, or at least I knew a few letters and I would recognize it if I saw the full name. Having just a list of available countries would solve many of the problems above:

All possible inputs are available for the user to see.

No need to make the user type.

No need to handle misspellings.

If you have an idea about what you are looking, you could go looking through the list or use your gemini client's search function.


I sorted the list alphabetically and made groups by the starting letter for readability. This also means that each country be a link, and clicking on it would be how the user makes a guess. But how do I track guesses?


Maintaining Game State


WITW is different from other Gemini services because it needs to maintain state. That is, a list of the actions the user has done, in a specific order. The actions they have done in the past determine what they are able to do now. So I need some way to track the guesses a user makes.


One approach would be to require a client-side certificate to play WITW. The client-side certificate would allow me to determine "who" is sending a request and allow me to track, on the server, what previous guesses they had made. However this adds a lot of complexity to the backend, and also increases the barriers to playing it.


Instead, I went with something easy: Using the query string. The query string holds the guesses the user makes, in order. Here is what that looks like, and how the URL grows as guesses are made.


User starts game
URL: gemini://gemi.dev/cgi-bin/witw.cgi/play

User guesses "Spain"
URL: gemini://gemi.dev/cgi-bin/witw.cgi/play?ES

User guesses "United States"
URL: gemini://gemi.dev/cgi-bin/witw.cgi/play?ES,US

User guesses "Belgium"
URL: gemini://gemi.dev/cgi-bin/witw.cgi/play?ES,US,BE

Guesses are just appended on to the query string. This works really well with my input method. I create a big block of link lines for all the countries and territories, where the URL for each link is the current query string, plus the country code for that territory. Using the example above, the user is on their 4th guess. The list of links to countries would look like this. You can see that the country codes for Afganistan and Albania are already appended on the the query string.


=> ?ES,US,BE,AF Afghanistan
=> ?ES,US,BE,AL Albania

Of course, this approach means the user could cheat. The service is trusting that the query string represents what the user has done. However the number one rule of web (and gemini) security is your can't trust the client. A user could alter query string. They could remove guesses from the query string manually, and try again. There are countermeasures to this, such as also sending a salted hash of the query string to prevent altering. However, at the end of the day, I can't stop cheating. The user can always just look at a map after all. Instead I optimized for what was easy for me to implement, and what would be least onerous to the user (not using client-side certificates).


Playing the game


The entire game is only around 400 lines of code, and much of that is nice formatting. The game logic itself is very simple. Since WITW is a CGI, it has no state and starts fresh as a new process with each request. So with each request, it does the same things, from a high level:


//Recreate the game state, using the guesses from the query string
var state = CreateStateFromQueryString(url);

//Process the state, modifying it with flags about how close the guesses are, if the user has won or not, etc.
GameEngine(state);

//draw the game state, including feedback about guesses, if they won or lost, and countries they can select
Renderer(state);

There is a clean separation between the game engine and the renderer.

The game engine is where the rules of the game are codified. The game engine just modifies/annotates the game state, based on the rules of the game.

The renderer draws the game, based on the state.


The engine itself is very simple. For each guess, it creates a "guess result" object. This includes what the user guessed, if they were correct or not, how far away their guess way, and what direction the guess was. It leverages a 3rd party geolocation library, that handles computing the distance and direction between 2 sets of longitude and latitude coordinates.


This is the actual C# code from the game engine:

public void Play(GameState state)
{
    //select target country for the game
    //currently derived from date, so consistent and changes once a day
    var targetCountry = PickCountryForPuzzle();
    state.TargetCountry = targetCountry;
    state.PuzzleNumber = ComputePuzzleNumber();

    var targetGeo = new GeoCoordinate(targetCountry.Latitude, targetCountry.Longitude);

    //score the results
    foreach (string guessedCode in state.InputGuesses)
    {
        //get details about the country they guessed
        var country = Countries[guessedCode];

        GeoCoordinate GuessGeo = new GeoCoordinate(country.Latitude, country.Longitude);
        bool isCorrect = (guessedCode == targetCountry.Code);

        state.GuessResults.Add(new Guess
        {
            Country = country,
            IsCorrect = isCorrect,
            Bearing = GuessGeo.RhumbBearingTo(targetGeo),
            Distance = Convert.ToInt32(GuessGeo.DistanceTo(targetGeo, DistanceType.KILOMETERS))
        });

        //if they are correct, no need to process more guesses
        if (isCorrect)
        {
            break;
        }
    }
}

The game state object looks like this:

public class GameState
{
    //country codes they have guessed
    public List<string> InputGuesses { get; set; } = new List<string>();

    //the results on each of those guesses
    public List<Guess> GuessResults { get; set; } = new List<Guess>();

    //the country they are trying to guess
    public Country TargetCountry { get; set; } = null;

    //did they win the game?
    public bool IsWin
        => GuessResults.Count > 0 && GuessResults.Last().IsCorrect;

    //is the game complete?
    public bool IsComplete
        => IsWin || GuessResults.Count == 6;
}

The renderer itself is also simple. Everything it needs to know is in the game state object. Here is what it does:

Draws the country ASCII art (from TargetCountry object)

Loops through and draws each GuessResult object as a list item

If IsWin == true, displays a "win" message and game summary

If IsComplete == true display a "lose" message and game summary

If IsComplete == false, display the list of countries to select, removing the ones the user already guessed


Source code for Where In the World? via Github


Things I learned

Building WITW was a ton of fun! I haven't actually written a game since playing around with BASIC games as a kid. What surprised me, in the end, was how simple the game could be. The primary challenge was how to "get out of the way" of the user to make playing the game as simple and clean as possible.


Other things I learned from this project:

Braille characters are a pretty good approach to ASCII art. It works on all clients without relying on crazy things like Sixels.

When building a game, create the game engine first. This ensures that game rules don't accidentally end up in your rendering logic.

Maintaining an ordered list of actions, and replaying all of them each time, is a reasonable way to maintain game state

When you have a fixed set of input options, presenting them as list works better than using a text box from a 10 INPUT response code

Games don't require writing complex middleware. You can run them as CGI's on normal capsules

Lowering the barriers to play is important


Gemipedia: Sixel

-- Response ended

-- Page fetched on Wed May 22 01:58:38 2024