Back to Blog

Dev Diary: Building "WhereAreYou?"

May 15, 2023 Web Development
WhereAreYou Game Screenshot

When you're starting a small project, it's tempting to reach for the latest and greatest tech. But I decided to stick with a simple, robust stack that I knew would get the job done without a lot of ceremony.

  • Backend: Python and Flask. Flask is my go-to for small projects. It's lightweight, unopinionated, and lets you get a web server up and running in minutes. For this project, all I needed was a single API endpoint, so bringing in something like Django would have been like using a sledgehammer to crack a nut.
  • Frontend: Good ol' Vanilla JavaScript, HTML, and CSS. No React, no Vue, no Angular. The game's logic is fairly straightforward, and I wanted to avoid the overhead of a big framework. This decision forced me to be clean with my DOM manipulation and state management, which was a great exercise.
  • The Star Player: The Google Maps JavaScript API. This is what makes the whole thing possible. I specifically used the StreetViewPanorama service, the Maps service for the guess map, and a crucial helper, the StreetView Image Metadata API.
  • Deployment: Docker and Gunicorn. Even for a small project, I think it's crucial to plan for deployment from the start. Using Docker means my development environment is identical to my production environment. Gunicorn is a battle-tested WSGI server for Python, a massive step up from Flask's built-in development server. The Dockerfile is simple: start with a Python image, install dependencies from requirements.txt, copy the code, and fire up Gunicorn.

System Design: The Core Loop

At its heart, the application is a conversation between the client (the browser) and the server.

Here’s the high-level flow:

  1. Settings: The user first lands on an intro.html page to choose their game settings. This is a simple HTML form.
  2. Game Start: When the user hits "Start," the browser navigates to the main game page, index.html, passing the settings as URL parameters (e.g., ?countdownToggle=on&timer=30).
  3. The API Call: As soon as the game page loads, the frontend JavaScript (game.js) makes a fetch request to my one and only backend endpoint: /api/random-location.
  4. The Backend's Magic Trick: This is where the core challenge of the entire app lies. The backend's job is to find a random spot on Earth that actually has Google Street View data.
  5. Rendering the Game: The backend responds with the latitude and longitude. The frontend then uses this to initialize the StreetViewPanorama and the guessing map.

The Biggest Challenge: "Just Find a Random Place"

This sounds so easy, but it was the hardest part of the project. My first naive thought was: just generate a random latitude and longitude and feed it to Street View. I quickly discovered that this usually lands you in the middle of an ocean, a desert, or a forest with no Street View coverage.

The solution was to use the Street View Image Metadata API. This endpoint acts as a check. You give it coordinates, and it tells you if a panorama exists nearby.

My backend logic for /api/random-location ended up looking like this, in a while True loop:

  1. Generate a random lat between -90 and 90 and lng between -180 and 180.
  2. Make a GET request to the streetview/metadata endpoint with these coordinates.
  3. Crucially, I added a radius of 100,000 meters. This tells the API, "Look for a panorama within 100km of this random point." This dramatically increases the chances of finding something.
  4. I also added source: 'outdoor' to prioritize outdoor shots, which is what you want for a GeoGuessr game.
  5. The API response has a status field. If status is 'OK', it means a panorama was found. I break the loop and send the coordinates from the metadata (which are the actual panorama coordinates, not my random guess) back to the client.
  6. If the status is ZERO_RESULTS or there's an error, the loop continues, and it tries again with a new random point.

It feels a bit like brute force, and it is! But for a simple app, it's surprisingly effective. The try...except block is important to catch network errors so the server doesn't crash if the Google API call fails.

On the Frontend: Taming Asynchronicity and Making it Fun

The Game Loop in game.js

  1. Modern API Loading: I'm using the new async/await syntax with google.maps.importLibrary("maps"). This is way cleaner than the old callback-based approach and makes the initialization code much easier to read.
  2. State Management: I kept it simple with a few global variables: map, panorama, latLng for the correct answer, and guessMarker. No need for a complex state machine here.
  3. The Guess: When the user clicks the "Guess" button, the makeGuess function kicks in. This is where the payoff happens.
    • Calculating Distance: The Maps API has a geometry library that does the heavy lifting. spherical.computeDistanceBetween(actualLatLng, guessLatLng) calculates the great-circle distance between the two points in meters.
    • The Scoring Algorithm: A linear score felt boring. Getting within 1km should be worth way more than getting within 2000km. I settled on an exponential decay function: score = 10000 * Math.exp(-distance / 2000000). This gives a perfect 10,000 for very close guesses and drops off quickly, which feels more rewarding.
    • Visual Feedback: This is key to making it feel like a game.
      • I draw a red Polyline between the guess and the actual answer.
      • I drop a new green marker on the actual location.
      • I use the map's fitBounds method to automatically zoom and pan so both the guess and the answer are perfectly in view. This is a small touch that makes a huge difference to the user experience.

Final Thoughts and Learnings

This project was a great reminder that you don't need a mountain of frameworks to build something cool.

  • Learning 1: The power of utility APIs. The Street View Metadata API was the unsung hero of this project. Sometimes the key to solving a problem is finding that one small, simple tool that does exactly what you need.
  • Learning 2: Plan for production. Writing a Dockerfile from day one made the (eventual) deployment process a non-event. Gunicorn is a must for any real Flask app.
  • Learning 3: UX is in the details. Things like disabling buttons when they can't be used, providing fun feedback messages based on the score, and automatically adjusting the map view are what elevate a project from a technical demo to a fun user experience.

It was a blast to build, and it's a great little game to challenge friends with. Now, if you'll excuse me, I have a new idea for a feature... maybe a multiplayer mode?