Home » How to » How to easily build a GeoGuessr or OpenGuessr clone

How to easily build a GeoGuessr or OpenGuessr clone

Minimal illustration showing a GeoGuessr-style map guessing game built with WP Go Maps

We’ve all seen the viral videos of people pinpointing their exact location on a random dirt road in seconds! Games like GeoGuessr and OpenGuessr have had a huge impact, and turn geography into a high-stakes quiz. Have you ever wondered how they actually work?

In this post we’re going to step behind the curtain and show you how to build your very own “guess the location” clone using WP Go Maps. We’ll need to do a bit of setup, add a little CSS and write some custom JavaScript to handle the logic, but don’t worry, we’ll share everything you need to get started!

Let’s get to it!

What Makes a Great Geography Game?

Before we dive into the code, let’s consider what we need to achieve. Games like OpenGuessr rely on a simple but addictive loop, our player will be dropped into a random Street View location and must use visual clues to guess where they are on the world map.

The goal of our project is to replicate that loop, without getting too technical! We will be building a small scale version, focusing only on the core gameplay mechanic. For a full scale game, you would need to expand on what we’re building here today.

We’ll need a few things to get started! Firstly, we’ll need a blank map with a powerful API to act as our canvas, this is where WP Go Maps comes to the rescue. We’ll need a list of coordinates, that we know have a valid Street View. This could be replaced with something more automated later, but for now we’ll keep it simple.

Lastly, we need the interfaces that make the game fun to play, loading the map, allowing the user to pick their location and showing them their distance from the guess.

Setup Your Game Engine

As we’re building this all on top of WP Go Maps, we’ll consider this our game engine, and before we can extend the logic to allow gameplay, we need to set up our level.

Before we get started, we’ll need to set Map Engine to use Google Maps, if you haven’t done so already, take a look at this guide: Changing your Map Engine.

Why Google? Because it gives us native access to Street View without any additional logic. You could build something similar on OpenLayers or Leaflet, but it would add many additional steps to the project that we want to avoid for the moment.

Once that is done, we can go ahead and create our map, but with a bit of a twist! We won’t be adding any markers to the map. Instead we’ll add all of our locations dynamically when our game loads.

With the map created, go ahead and add the shortcode on to a page on your WordPress website, this is where we’ll play our game as we build things. Take note of your map ID in your shortcode:

In our case the map ID is 121, we’ll need this later to ensure our game only runs on this map.

Building the Game Logic with Custom JavaScript

One of the most powerful parts of WP Go Maps is the custom scripts section within the settings. This allows you to add logic to your maps, extend our core API, and build completely unique experiences for your community. A perfect place to start building our game!

All of your code will be added under Maps > Settings > Custom Scripts. In this case, we’ll use the Custom JavaScript block, but later on we’ll also use the CSS block to style things a bit more.

We’ll work through each section of the code below, and you can build along in order. At the bottom of this section, you will find the full script, which you can copy and edit as needed.

Building the Foundation

Before we get into building the game, we need to do some initial work, such as creating our class with empty methods to fill out later, and linking it to the WP Go Maps architecture.

We start by creating an empty class named WPGoMapsGuessr and we stub out a few placeholder methods, which we will build on moving forward.

Above this class, we set our target map ID, and link a init.wpgmza event to create an instance of our game when the correct map is loaded anywhere on the site.

/* Guess the location game demo */
jQuery(($) => {
    /* Limit our map logic to a specific map ID */
    const targetMapId = 121;
    let game = false;
    $(document.body).on('init.wpgmza', (event) => {
        if(event && event.target && event.target instanceof WPGMZA.Map && event.target.id && parseInt(event.target.id) === targetMapId){
            /* Our target map has loaded, and we can load our game */
            game = new WPGoMapsGuesser(event.target);
        }
    });

    class WPGoMapsGuesser { 
        constructor(map){ }

        prepare(){ }

        ui(){ }

        newGame(){ }

        next(){ }

        showMap(){ }

        showStreetView(){ }

        clear(){ }

        guess(){ }

        markGuess(event){ }

        markAnswer(location){ }

        markDistance(){ }
    }

});

Let’s continue working through each method within the class and make it fully functional as we go!

Class Constructor

For our constructor, we want to fire all the initial methods that we need for the game to run, this includes preparing our base locaitons, creating the user interface, linking the map instance, and finally starting the game.

constructor(map){
    this.map = map; // Link the map

    this.prepare(); // Prepare the game 
    this.ui(); // Build our user interface
    this.newGame(); // Start a new game
}

Preparing Locations

For our locations, we’re going to use a list of predefined locations, just for this demonstation. We’ll store these in a list (array) so that we can slice 10 random items from it for each new game.

A more mature version of this game might include a database connection or a realtime lookup for a known-good streetview location. In our case we’ve just used a list of random locations that should work in street view.

As a final step, we’ll use the prepare method to also link our map click listener, which will allow users to guess the location.

prepare(){
    /* Define a list of known locations */
    /* Why? We cannot pick random coordinates as they may not have a valid street view location */
    this.locations = [
      { name: "Times Square", coordinates: { lat: 40.7580, lng: -73.9855 } },
      { name: "The White House", coordinates: { lat: 38.8977, lng: -77.0365 } },
      { name: "Golden Gate Bridge", coordinates: { lat: 37.8324, lng: -122.4795 } },
      { name: "Las Vegas Strip", coordinates: { lat: 36.1126, lng: -115.1739 } },
      { name: "Hollywood Sign View", coordinates: { lat: 34.1266, lng: -118.3265 } },
      /* Additional locations - See full code for details */
      { name: "Biltmore Estate View", coordinates: { lat: 35.5406, lng: -82.5520 } },
      { name: "Sedona Red Rocks", coordinates: { lat: 34.8055, lng: -111.7665 } },
      { name: "Historic Route 66", coordinates: { lat: 35.2490, lng: -114.0530 } }
    ];

    this.map.on('click', (event) => {
      this.markGuess(event);
    });
}

User Interface

We’re creating our very simple user interface here in our JavaScript, but you could add these to your page, and add more controls as you see fit. For our purposes we only need a few things. Two few buttons to allow users to switch between map view and street view.

A button to make their guess, along with a notice to report the distance, and finally a button to move to the next location.

While we’re setting this all up, we’ll link click listeners to each of the buttons, these point to our class methods we stubbed earlier.

ui(){
  /* Create the elements */
  $(this.map.element).append(`
      <div class='wpgmza-guessr-map-view-btn'>Map</div>
      <div class='wpgmza-guessr-street-view-btn'>Street View</div>
      <div class='wpgmza-guessr-continue-btn'>Continue</div>
      <div class='wpgmza-guessr-guess-btn'>Make Guess</div>
      <div class='wpgmza-guessr-result'></div>
  `);

  /* Link the buttons to our game */
  $(this.map.element).find('.wpgmza-guessr-map-view-btn').on('click', () => {
      this.showMap();
  });

  $(this.map.element).find('.wpgmza-guessr-street-view-btn').on('click', () => {
      this.showStreetView();
  });

  $(this.map.element).find('.wpgmza-guessr-continue-btn').on('click', () => {
      this.next();
  });

  $(this.map.element).find('.wpgmza-guessr-guess-btn').on('click', () => {
      this.guess();
  });

  $(this.map.element).find('.wpgmza-guessr-result').on('click', '.wpgmza-guessr-new-game', () => {
      this.newGame();
  });
}

New Game

For our new game method, we want something that can be reused once the player reaches the end of game, to start another game with 10 more locations.

We start by copying our location master list, shuffling it, and picking 10 random items from it for this game and then immediately move to the next location.

newGame(){
    this.pool = [...this.locations].sort(() => Math.random() - 0.5).slice(0, 10); // Pick 10 random locations for this game
    this.next();
}

Next Location

Whenever the next method is called, our game will pick the next location from the pool and use it as the location the user has to guess. We reset the system here to clear any visual elements, set up street view and load into it, and allow them to play.

Setting up the street view is done by grabbing the coordinates from our location, setting a bearing and pitch, and pushing this into the WP Go Maps API to load the map. We hide the address from street view, we don’t want to give the player any hints!

If the pool is empty, we know the game has ended, and prompt them to start a new game instead.

next(){
  if(this.pool.length){
      this.clear();

      /* Pick the next location */
      this.question = this.pool.shift();

      /* Set up street view and load it */
      let streetViewOptions = {
        lat : this.question.coordinates.lat,
        lng : this.question.coordinates.lng,
        bearing: 0,
        pitch: 10
      }
      
      this.map.setStreetView(streetViewOptions);

      /* Hide the address control */
      this.map.panorama.setOptions({
          addressControl: false
      });

      this.showStreetView();
  } else {
      /* The game has ended */
      $(this.map.element).find('.wpgmza-guessr-result').show();
      $(this.map.element).find('.wpgmza-guessr-result').html(`End of game <div class='wpgmza-guessr-new-game'>New Game</div>`);
  }
}

Showing & Hiding Street View

We have two more methods that control showing/hiding streetview. These are triggered when the user clicks on the buttons we added, as you might remember.

These are very simple, they find the elements and show/hide them to prevent players from making guesses inside of streetview.

showMap(){
    this.map.panorama.setVisible(false);

    $(this.map.element).find('.wpgmza-guessr-map-view-btn').hide();
    $(this.map.element).find('.wpgmza-guessr-street-view-btn').show();
    $(this.map.element).find('.wpgmza-guessr-guess-btn').show();
    $(this.map.element).find('.wpgmza-guessr-continue-btn').hide();
    $(this.map.element).find('.wpgmza-guessr-result').hide();
}

showStreetView(){
    this.map.panorama.setVisible(true);

    $(this.map.element).find('.wpgmza-guessr-map-view-btn').show();
    $(this.map.element).find('.wpgmza-guessr-street-view-btn').hide();
    $(this.map.element).find('.wpgmza-guessr-guess-btn').hide();
    $(this.map.element).find('.wpgmza-guessr-continue-btn').hide();
    $(this.map.element).find('.wpgmza-guessr-result').hide();
}

Clearing Guesses

When a player moves to the next question, we want to clear out any markers/lines we have drawn on the map, that is done in the clear method.

clear(){
    if(this.guessMarker){
        this.map.removeMarker(this.guessMarker);
        this.guessMarker = false;
    } 

    if(this.answerMarker){
        this.map.removeMarker(this.answerMarker);
        this.answerMarker = false;
    } 

    if(this.distanceLine){
        this.map.removePolyline(this.distanceLine);
        this.distanceLine = false;
    } 
}

Guessing

We need our players to be able to make a guess, this method will handle that, by checking if they have placed a marker, and then checking the correct answer to determine the distance from the guess. We also visualize everything at this stage, allowing them to see how near/far they are from the answer.

guess(){
    if(this.guessMarker && this.question){
        this.markAnswer(this.question);
        this.markDistance();
        this.map.fitBoundsToMarkers();

        $(this.map.element).find('.wpgmza-guessr-map-view-btn').hide();
        $(this.map.element).find('.wpgmza-guessr-street-view-btn').hide();
        $(this.map.element).find('.wpgmza-guessr-guess-btn').hide();
        $(this.map.element).find('.wpgmza-guessr-continue-btn').show();

        let distance = WPGMZA.Distance.between(this.guessMarker.getPosition(), this.answerMarker.getPosition());
        distance = parseInt(distance);

        $(this.map.element).find('.wpgmza-guessr-result').show();
        $(this.map.element).find('.wpgmza-guessr-result').text(`You were ${distance}km away`);
    }
}

Marking Guesses & Answers

When our player clicks on the map, we’ll mark their guess by dropping marker on the map at that location. Then, once they submit their answer, we’ll mark the answer location as well. Both of these leverage WP Go Maps native methods to place markers, which allows us to add titles to them as well.

markGuess(event){
  if(this.answerMarker){
      return; 
  }

  this.guessMarker = WPGMZA.Marker.createInstance({
      title : "Your Guess",
      address: `${event.latLng.lat}, ${event.latLng.lng}`,
      map_id: this.map.id,
      lat: event.latLng.lat,
      lng: event.latLng.lng,
  });

  this.map.addMarker(this.guessMarker);
}

markAnswer(location){
    this.answerMarker = WPGMZA.Marker.createInstance({
        title : location.name,
        address: `${location.coordinates.lat}, ${location.coordinates.lng}`,
        map_id: this.map.id,
        lat: location.coordinates.lat,
        lng: location.coordinates.lng,
    });

    this.map.addMarker(this.answerMarker);
}

Marking Distance

Lastly, we need to show the player a line between the two points, to help visualize the distance. We do this by leveraging the WP Go Maps Polyline API to create a line between these two points.

markDistance(){
    this.distanceLine = WPGMZA.Polyline.createInstance({
        polydata : [
            { lat : this.guessMarker.lat, lng : this.guessMarker.lng },
            { lat : this.answerMarker.lat, lng : this.answerMarker.lng }
        ],
        strokeWeight: 2,
        strokeColor : "#000000",
        strokeOpacity : 0.7
    });

    this.map.addPolyline(this.distanceLine);
}

Done!

With all of these methods in place, we should have a very simple prototype ready to play. We still need to style things a bit, which we’ll do a bit later on, but you can see some screenshots of where we’re heading below!

Completed JavaScript

Hosted in a convenient Gist for you to use as a starting point!

Styling the Interface

For styling, we’ve put together a very simple design, just some basic button styles. You’ll probably want to go much further here to build something unique.

.wpgmza-guessr-map-view-btn,
.wpgmza-guessr-street-view-btn,
.wpgmza-guessr-continue-btn,
.wpgmza-guessr-guess-btn,
.wpgmza-guessr-result{
    padding: 5px 7px; 
    background: white;
    border-radius: 5px;
    box-shadow: 0 0 5px 2px #0000003d;
    position: absolute;
    z-index: 999;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    opacity: 0.8;
}

.wpgmza-guessr-map-view-btn:hover,
.wpgmza-guessr-street-view-btn:hover,
.wpgmza-guessr-continue-btn:hover,
.wpgmza-guessr-guess-btn:hover{
    opacity:1;
}

.wpgmza-guessr-map-view-btn,
.wpgmza-guessr-street-view-btn {
    bottom: 25px;
    right: 70px;
}

.wpgmza-guessr-continue-btn,
.wpgmza-guessr-guess-btn{
  	bottom: 36px;
    left: 9px;
  	background: #f53203;
    color: white;
    font-weight: 500;
}

.wpgmza-guessr-result{
    bottom: 25px;
    left: 50%;
    transform: translateX(-50%);
    cursor: none;
}

.wpgmza-guessr-result .wpgmza-guessr-new-game{
    padding: 5px;
    border-radius: 5px;
    font-weight: 500;
    background: #f53203;
    color: white;
    cursor: pointer;
    margin-top: 10px
}

Try it!

Take a look at the demo map below, this was built using the code shared here, and provides a simple prototype for you to build out further.

1 Comment

  • Milton says:

    If you want to add multiplayer, PlaySocket would be a good library for that. OpenGuessr uses it too.

Leave a Reply

Your email address will not be published. Required fields are marked *


This site uses Akismet to reduce spam. Learn how your comment data is processed.