Photo location guessing game in Go

Programming Snapshot – Go Geolocation Game

© Lead Image courtesy of Mike Schilli

© Lead Image courtesy of Mike Schilli

Article from Issue 262/2022

A geolocation guessing game based on the popular Wordle evaluates a player's guesses based on the distance from and direction to the target location. Mike Schilli turns this concept into a desktop game in Go using the photos from his private collection.

After the resounding success of the Wordle [1] word guessing game, it didn't take long for the first look-alikes to rear their heads. One of the best is the entertaining Worldle [2] geography game, where the goal is to guess a country based on its shape. After each unsuccessful guess, Worldle helps the player with information about how far the guessed country's location is from the target and in which direction the target country lies.

Not recognizing the outline of the country in Figure 1, a player's first guess is the Principality of Liechtenstein. The Worldle server promptly tells the player that the target location is 5,371 kilometers away from and to the east of this tiny European state. The player's second guess is Belarus, but according to the Worldle server, from Belarus you'd have to travel 4,203 kilometers southeast to get to the target. Mongolia, the third attempt, overshoots the mark, because from there you'd have to go 3,476 kilometers to the southwest to arrive at the secret destination.

Figure 1: The original Worldle geography guessing game.

Slowly but surely, the player realizes that the secret country must be somewhere near India. And then, on the fourth try, Pakistan turns out to be correct! Worldle is a lot of fun, with a new country posted for guessing every day.

Private Snapshots

Now, instead of guessing countries, I thought it would be fun to randomly select a handful of photos from my vast cell phone photo collection, which has grown wildly over the years. Analyzing each photo's GPS data, the game engine makes sure the photos in one round were taken a great distance from one another. Initially, the computer selects a random photo from the set as a solution. It keeps this a secret, of course, and then shows the player a randomly selected photo, along with details of how many kilometers lie between the place where the random photo was taken and the target location, along with the compass direction in which the player has to move across the world to get there.

Armed with this information, the player now has to guess which picture from the remaining selection is the secret photo. The player clicks on the suspected match and is given some feedback on the guess, again with the distance and compass direction leading to the secret location. The goal of the game is to find the solution in as few rounds as possible – a kind of treasure hunt, if you like. As a nod to the extra consonants in Wordle, my program goes by the name of Schnitzle. There are enough photos to choose from on my cell phone, and a random generator ensures that the game always selects new pictures, so it never gets boring.

And … Action!

Figure 2 shows Schnitzle in action. As a starting image, the computer has selected a snapshot depicting yours truly, hiking in the Bavarian Alps. According to the clues, the target is 9,457.8 kilometers to the northwest (NW) from the starting image. It seems highly likely that the secret photo was taken somewhere in North America! From the selection on the right, the player then clicks on the photo of Pinnacles National Park in California (Figure 3). Schnitzle reveals that the target is 168.5 kilometers in a northwest direction from this guess. What's north of the Pinnacles? Probably the San Francisco Bay Area, where I live!

Figure 2: Starting point: Schnitzle selects a photo that is 9,457.8 kilometers from the target.
Figure 3: The player clicks on a photo of Pinnacles National Park, which is still 168.5 kilometers from the target.

In Figure 4, the player then clicks on the photo of the parking lot at the beach in Pacifica, where I often go surfing. But you still have to travel 10.2 kilometers from the beach in a northeast direction (NE) to the location of the photo the game selected. If you know the area, you can probably guess: The destination must be somewhere in the suburbs of South San Francisco, where the giant Costco supermarket is located. In fact, that shelf filled with rotisserie chicken is the solution, as the   * WINNER  * message indicates (Figure 5). There are still two unclicked photos in the right-hand column, showing a bridge in Heidelberg, Germany, and one showing the sand at Esplanade Beach in the Bay Area.

Figure 4: The Pacifica State Beach parking lot is still 10 kilometers from the target.
Figure 5: The solution: a shelf with rotisserie chicken at the Costco in South San Francisco.

Seek and Ye Shall Find

So how does the game work as a Go program? To sift through the entire photo collection downloaded from my cell phone to my hard disk takes some time, even though it is on a fast SSD. That's why the finder.go helper program in Listing 1 plumbs the depths of the cell phone photo directory set in line 18, analyzing each JPEG image found there and reading its GPS data, if available, to cache it for later.

Listing 1


01 package main
03 import (
04   "database/sql"
05   "fmt"
06   _ ""
07   exif ""
08   "os"
09   "path/filepath"
10   rex "regexp"
11 )
13 type Walker struct {
14   Db *sql.DB
15 }
17 func main() {
18   searchPath := "photos"
20   db, err := sql.Open("sqlite3", "photos.db")
21   w := &Walker{ Db: db }
22   err = filepath.Walk(searchPath, w.Visit)
23   panicOnErr(err)
25   db.Close()
26 }
28 func (w *Walker) Visit(path string,
29   f os.FileInfo, err error) error {
30   jpgMatch := rex.MustCompile("(?i)JPG$")
31   match := jpgMatch.MatchString(path)
32   if !match {
33     return nil
34   }
36   lat, long, err := GeoPos(path)
37   panicOnErr(err)
39   stmt, err := w.Db.Prepare("INSERT INTO files VALUES(?,?,?)")
40   panicOnErr(err)
41   fmt.Printf("File: %s %.2f/%.2f\n", path, lat, long)
42   _, err = stmt.Exec(path, lat, long)
43   panicOnErr(err)
44   return nil
45 }
47 func GeoPos(path string) (float64,
48   float64, error) {
49   f, err := os.Open(path)
50   if err != nil {
51     return 0, 0, err
52   }
54   x, err := exif.Decode(f)
55   if err != nil {
56     return 0, 0, err
57   }
59   lat, long, err := x.LatLong()
60   if err != nil {
61     return 0, 0, err
62   }
64   return lat, long, nil
65 }

The program feeds the results into a table in an SQLite database so that the game program can quickly select new images in each round later on, without having to comb through entire filesystem trees on every round. You can create the required empty SQLite database with the required table that assigns GPS data to file names in next to no time with a shell command such as the one in Figure 6.

Figure 6: An empty photo database, generated using the sqlite3 client.

Before the game can begin, the program from Listing 1 needs to run once, compiled by typing:

go build finder.go

The program uses two libraries (go-sqlite3 and goexif2) from GitHub. One drives the flat-file database, and the other reads the GPS headers from the JPEG photos.

To make the Go compiler do this without any complaints, first type

go mod init finder; go mod tidy

to specify a Go module to parse the libraries included in the source code, fetch them from GitHub if needed, and define their versions. When this is done, the go build command produces a static binary finder including all the compiled libraries.

As shown in Figure 7, the finder utility from Listing 1 reads the 4,000 or so files in my phone folder in about 30 seconds and adds the photos' metadata into the files table in the SQLite photos.db flat-file database.

Figure 7: After running finder, there are 4,162 images with GPS coordinates in the database.

The call to the Walk function in line 22 of Listing 1 receives the w.Visit callback defined in line 28. The file browser calls this function for every file it finds. It always drags a type Walker data structure along with it as a receiver, which means that it can immediately access the db handle of the SQLite database opened previously.

For each file found, line 31 checks whether the file has a .jpg extension (upper- or lowercase) and then runs the GeoPos() function from line 47 to load the photo's Exif data. This will ideally include the longitude and latitude of the location where the photo was taken as floating-point numbers.

Line 39 feeds the path and GPS data into the database table with an INSERT statement in typical SQL syntax. Later, the main schnitzle program can pick up the image location and metadata from the database, when it is looking for new snapshots for a new game.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Wheat and Chaff

    If you want to keep only the good photos from your digital collection, you have to find and delete the fails. Mike Schilli writes a graphical application with Go and the Fyne framework to help you cull your photo library.

  • Straight to the Point

    With the Fyne framework, Go offers an easy-to-use graphical interface for all popular platforms. As a sample application, Mike uses an algorithm to draw arrows onto images.

  • Chip Shot

    We all know that the Fyne framework for Go can be used to create GUIs for the desktop, but you can also write games with it. Mike Schilli takes on a classic from the soccer field.

  • GUI Apps with Fyne

    The Fyne toolkit offers a simple way to build native apps that work across multiple platforms. We show you how to build a to-do list app to demonstrate Fyne's power.

  • Digital Shoe Box

    In honor of the 25th anniversary of his Programming Snapshot column, Mike Schilli revisits an old problem and solves it with Go instead of Perl.

comments powered by Disqus