Eurosapper.eu - Not such a fast gamejam as I thought

It was a simple project that was not supposed to take more than 72h. It took almost a month but turned out a silly meme in to a game.

Original meme - the idea

I found this picture and thought, hey why not! Cool idea for a game and opportunity to try Phaser.

The beginnings

Politicization of the sapper may not be the best idea in the world, but if you want to do it right you can't just put a map of Europe on a sapper board. You need the data. Reliable data.

And reliable data is not so easy to find

However, in the end I managed to find the Global Terrorism Index 2016 report from Institute of Economics and Peace, which contains a huge dataset about terrorism of 2000-2015.

To make matters even more interesting, the report lists countries with an index called the GTI (Global Terrorism Index), which determines the level of terrorist threat for a given country at a 10-point scale.

It was even more than I expected. So another but very important preparation point.

What the hell are the rules of this game?

We all know the sapper from Windows, which for quite some time had it as a preinstalled game. But did you know that the values displayed on the fields are not points? I don't. I never looked into the instructions of the game as all the finished one were just lucky luck.

For those who still don't know the rules: the number on revealed areas indicates the number of mines in adjacent tiles. Brilliant right? Now you can finally finish this game!

Or go higher.

And use hexagons instead of squares

And that means that the number of adjacent fields increases for each by two. And that means that the game requires a bit more thinking, which again means that it is more interesting. With all this excitement you forget that it will also mean a slightly more difficult generation of a map, where you are likely to blow yourself up before you even put the first bomb.

Let's move on - Design and code

For those of you who have not yet looked into the game, you can find the full code on my Github.

At the foundation of this game was to avoid unnecessary libraries. I did not want to load jQuery, SQLite libraries, or other things I would probably use normally. I wanted to write this in Common JS with Phaser, just because I wanted to try it again in a much simpler project than Leviathan.

The structure of the project looks as follows.

  • Game object extends from Phaser.Game
    • HexMap object extends from Phaser.Group
      • Hexagon object extends from Phaser.Sprite
    • GameState object
    • SoundFx Object
  • Utils like visual effects, popup handlers, ajax requests and tile mapping.
  • DB Utils (PHP) with three simple classes:
    • Config wich contains SQLite DB file path
    • SQLiteConnection
    • SQLiteManager

We will not discuss the PHP part because the code is self-explanatory and the article would be a bit too long. If you have any questions, I will answer them in the comments.

I also point out that it will not be a guide how to make a game, only a small case-study of the more interesting problems I encountered while creating the game.

So let's do this boring part quickly

So you will have a general idea how it all works.

Game object - As I said before, it extends the Phaser.Game object. It takes care about all shitty part of building basic mechanisms and rendering the game on Canvas. It also handling the boot process. So in this case, it is responsible for loading the assets, creating a board, managing the input of the map move, and initializing those interesting objects.

GameState object - It is responsible for the game states. Manages points, kills us when we go to the bomb, throws popups and restarts the game.

SoundFx object - An ordinary screamer, it is more often heard than seen. It simply play the sounds when something asks for it.

And the boring part is done.

Now it will be more interesting - Hexmap

HexMap object - Creates a grid and finds adjacent tiles. So basically the mechanics of the game.

Creating a hexagon grid is a bit more difficult than square fields. It's worth thinking about it when planning your game but hey! It was a gamejam, there is no time to think. However, I should find some time for this because later things became complicated.

But first. Let's make a hexmap. Quite efficient one so we don't need any preloadings and other unnecessary scatterers.

To cover the 2500px/2128px board we need a grid of 205/110 boxes assuming our hex has 24px at 26px. Given values just fit my design and are not a case here. The problem is to create 22 550 (205 * 110) fields, in a way that will not slow down the page load significantly or worse, it will not suspend the browser.

Option A - easier. We generate each line separately, moving every second by width of hex / 2. Simple to use but requires looping through each line and field.

Hexmap Row A

Let's try another way.

Option B - more difficult but more efficient. Due to the use of hexes instead of squares we have 2 additional sides. So we don't have to stick to the XY axis anymore.

Hexmap Row B

By building a map this way we create 2 lines in each loop step and that means we've cut the number of steps in the loop by half. Instead of 110 steps in option A we have 55. This doesn't mean, of course, that half of the tiles have evaporated. But we will explain this using code!

I mentioned earlier that we will not stick to the XY axis but consider the example above as a 'virtual axis'. In fact, we are still working on 2 dimensions so we physically use the XY axis.

Let's create two nested loops that will allow us to move along those axes.

for (var i = 0; i < self.config.gridSize.height / 2; i ++) { 
for (var j = 0; j < self.config.gridSize.width; j ++) { } }

So we have loops that go through half the height of the grid (remember - we generate 2 rows at a time) and across the whole width for each row.

To better understand this, let's take a look at the graphic below.

Hexmap Overall

As you can see, each step of the first loop creates two virtual rows, which we count on the XY axis as one line. Now let's reflect on the conditions that must be fulfilled by our loops to create a tile.

Our map has an even number of rows - In this case, we always create a tile. There is no risk that any of them will go beyond the assumed range. You probably also noticed that in our case this condition always returns true, so we don't need it. Fact but 'think forward' what if for some reason I will want to go to an odd number of lines in the future? First let's look at our code.

for (var i = 0; i < self.config.gridSize.height / 2; i ++) { 
for (var j = 0; j < self.config.gridSize.width; j ++) { if (self.config.gridSize.height % 2 =​=​= 0) { /* When our map has an even number of rows */ } } }

Our map does not has an even number of rows - So we have to deal with the last line. Remember that we generate them in pairs? In this case, the last line will not have its own pair, so we can't generate each of the fields. The easiest way is to ignore the last row (2 virtual lines)

for (var i = 0; i < self.config.gridSize.height / 2; i ++) { 
for (var j = 0; j < self.config.gridSize.width; j ++) { if (self.config.gridSize.height % 2 =​=​= 0 || i + 1 < self.config.gridSize.height / 2) { /* When our map has an even number of rows OR if a given row has its own pair (Virtual Row B) */ } } }

WHAT? We lose a whole row of tiles! - Yes. And that is why we need to add another condition. Instead of deleting a row, we'll generate it in the form of the mentioned before option A but retaining our numbering. We will only take an even value of j so that we only fill the first line when both conditions fail.

for (var i = 0; i < self.config.gridSize.height / 2; i ++) { 
for (var j = 0; j < self.config.gridSize.width; j ++) { if (self.config.gridSize.height % 2 =​=​= 0 || i + 1 < self.config.gridSize.height / 2 || j % 2 =​=​= 0) { /* When our map has an even number of rows OR if a given row has its own pair (Virtual Row B) */ } } }

And that's actually enough. Now it is time to replace our values in the XY axes with the desired values on the screen. But first, let's see the whole code.

var tileId = 0; 
for (var i = 0; i < self.config.gridSize.height / 2; i ++) {
for (var j = 0; j < self.config.gridSize.width; j ++) { if (self.config.gridSize.height % 2 =​=​= 0 || i + 1 < self.config.gridSize.height / 2 || j % 2 =​=​= 0) { var hex = createHex(self, i, j, tileId); tileId += 1; self.add(hex); } } }

As you can see, our createHexMap method does a bit more than described here. It will also center the map at the center of the view, but we will not deal with every detail at this time.

The createHexMap method is invoked in the constructor of the HexMap object which self variable refers.

The createHex method executed in a loop creates an object with Hex parameters. Then it returns the Hexagon object, which is hopefully self-explanatory, and we do not have to dive over it. What is important here is how to count the actual XY position on the screen.

X position - The fact that our line has two virtual rows made up of hexagons causes every second tile (odd in our case) to be moved by half of its width. It's pretty simple but let's look at image below.

Hexmap X offset

And final code:

x: self.config.hexSize.width * j / 2

Y position - It's a bit more tricky because we have two virtual lines in each real one, plus the second one must have an offset. Let's look at the picture first.

Hexmap Y offset

Ok, let's build our equation step by step. As in the case of the X axis, we need to multiply the height of our tile by the value i (Y axis). But in this case, as we know our row has two virtual lines. So theoretically has a height of 2 tiles. In practice, however, the fields overlap.

We have to include this in tile offset. I marked the mutual field of both lines with green color. We need to take into account the 1/4 of the tile height which is lost between virtual lines and in addition, we lose the same amount in every real row. 1/4 + 1/4 gives us 1/2, which of course means 0.5 in reasonable units. Actual row height will therefore have a 150% tile height. Which means tileHeight * 1.5

So how does it look in the code?

y: self.config.hexSize.height * i * 1.5

Not so hard yep? Nope We've not finished yet. As you can see on the image I've marked both odd and even columns. Not without a reason. We have not dealt with the Y position of the second virtual line yet. These rows must be shifted by 3/4 the height of the tile. Let's look at the code.

y: self.config.hexSize.height * i * 1.5 + (self.config.hexSize.height / 4 * 3) * (j % 2)

Tile height divided by 4 equals 0.25 height. Multiplied by 3 equals 0.75 wich is 3/4. Modulo returns whether there is the rest of the divide or not. There are 0 for even and 1 for odd. In case 0, as we know, the multiplication by 0 gives 0, so for odd lines this is unnoticeable.

This way we've generated a hexmap.

So let's find adjacent tiles

The last important thing is the way we find adjacent fields. Due to the use of hexagons we can't move around the XY axes because we have to check each side. We should also keep in mind that in some cases the number of fields to reveal will be quite large.

Let's start with the position of the fields relative to the current tile. The way we generate the map makes it a little difficult for us, but first let's look at the picture.

Tile relative position

In each of the actual lines we have two virtual lines. We need to keep this in mind because depending on which row our field is, we will have different values for the surrounding tiles. Two fields will be from the previous or next real line. In this case, our map has 205 tiles, each field in the virtual line is numbered by 2. This means that the first row will have fields 0, 2, 4, 6... and the second 1, 3, 5, 7...

Not so hard! Remember that this was a gamejam so it will not be the nicest solution you've seen but sometimes you just want to get shit done. Fast. Here are our methods.

function sameParity(hex) { 
var result = [ hex.index - 206, hex.index - 204, hex.index - 2, hex.index - 1, hex.index + 1, hex.index + 2 ]; return result; }

function diffParity(hex) {
var result = [ hex.index - 2, hex.index - 1, hex.index + 1, hex.index + 2, hex.index + 204, hex.index + 206 ]; return result; }

Methods return simple arrays with calculated adjacent field numbers. Let's write a simple method that will work on calling the appropriate method.

function calcAdjecent(hex) { 
var result = null; if (hex.index % 2 =​=​= 0) { result = sameParity(hex); } else { result = diffParity(hex); } return result; }

So if the index of our field is even, we return the sameParity otherwise diffParity.

But what about the actual odd lines? There the numbering is reversed, numbers in first row are odd, then comes even indexes. This is the fuckup I did at the beginning but there was no time to re-map the fields. I have to deal with this.

Fortunately, the solution is quite simple, let's swap this methods when real row is odd.

function calcAdjecent(hex) { 
var result = null; if (hex.x % 2 =​=​= 0) { if (hex.index % 2 =​=​= 0) { result = sameParity(hex); } else { result = diffParity(hex); } } else { if (hex.index % 2 =​=​= 0) { result = diffParity(hex); } else { result = sameParity(hex); } } return result; }

Hex.x is in fact Hex.y. Another fuckup.

Okay, we know the IDs of the surrounding fields, let's take a look at the actual action that takes place after unveiling the field.

When the field is clicked, if it's not flagged, it's not revealed and is not a field with bomb it call revealAdjecentTiles method from HexMap object.

hexMap.revealAdjacentTiles(hex.hexIndex)

Let's take a look at this method.

HexMap.prototype.revealAdjacentTiles = function(hex) { 
var self = this; var adjecentTilesIndexes = calcAdjecent(hex); var tiles = []; var points = 0; var selectedTile = self.children[hex.index];

adjecentTilesIndexes.forEach(function(tileId) { var tile = self.children[tileId]; if (tile.hasBomb) { points += 1; } tiles.push(tile); });

selectedTile.reveal(points);

self.addToChecklist(tiles, points); self.updateChecklist(selectedTile);
};

Let's start with the hex parameter. This is an object with the position of our hexagon. We need only hex.index, which is the field number here.

At the beginning we use methods created earlier. As you can see, the variable adjecentTilesIndexes calls our calcAdjecent method.

We already know the adjacent fields. Time to check if they have a bomb. If so, we increment the points counter that will be displayed in the field and will report the number of bombs in the adjacent fields.

adjecentTilesIndexes.forEach(function(tileId) { 
var tile = self.children[tileId]; if (tile.hasBomb) { points += 1; } tiles.push(tile); });

Pretty simple but why we add the current field to the tiles array? This is where the fun begins.

Finding all adjacent tiles can take a while. In addition, when the field is adjacent to the tile with bomb, we should stop revealing the next fields and display bomb counter.

But first let's let hex know that it was revealed.

selectedTile.reveal(points);

The hex object has a reveal method that takes as a parameter the number of points to display. If the number of these points is 0 then the hex is revealed otherwise we will display the number of points that tell us the number of bombs in the adjacent fields.

Creating a queue.

An the beginning, let's take a look at two class variables that hold pending fields to check and those already checked.

this.tileCheckList = []; 
this.tileCheckedList = [];

Simple plaques. Let's take care of their content. AddToChecklist() method.

HexMap.prototype.addToChecklist = function(tiles, points) { 
var self = this; if (points =​=​= 0) { tiles.forEach(function(tile) { if (self.tileCheckList.indexOf(tile) =​= -1 && self.tileCheckedList.indexOf(tile) =​= -1 && tile.hexInfo.tiletype !== 0 && !tile.isRevealed && !tile.isFlagged) { self.tileCheckList.push(tile); } }); } else { self.sound.reveal(); } };

As you remember, in the revealAdjecentTiles() method we build an array of adjacent fields. We pass it along with the points to the addToChecklist() method.

If the number of points is 0, which means that none of the adjacent fields has a bomb, we can deal with adding fields to our queue. For each field we check that:

self.tileCheckList.indexOf(tile) =​= -1

Does tile exist in the queue. We don't need to check something twice.

self.tileCheckedList.indexOf(tile) =​= -1

Does tile exist in the already checked list. Once again - we don't need to check something twice or worst, have endless loop.

tile.hexInfo.tiletype !=​= 0 && !tile.isRevealed && !tile.isFlagged

Just in case. Tiletype 0 means ocean fields. We do not need to check them.

If everything returns the truth then we add a field to the queue.

Let's look at the next method. updateChecklist().

HexMap.prototype.updateChecklist = function(selectedTile) { 
var self = this; self.tileCheckedList.push(selectedTile); self.tileCheckList.splice(self.tileCheckList.indexOf(selectedTile), 1);

if (self.tileCheckList.length > 0) { var next = self.tileCheckList[0]; self.revealAdjacentTiles(next.hexIndex); } else { self.tileCheckList = []; self.tileCheckedList = []; } };

SelectedTile is a field that has been given the revealAdjecentTiles() method. As you've probably noticed, revealAdjecentTiles() is a recursive method, using updateChecklist() it calls itself and does it as long as it has fields in the queue. But, let's not overtake facts.

At first, the method places current tile in the list of checked fields. Then removes it from the queue.

Then check if there are any other fields in the queue. If so, it retrieves the first one and call revealAdjecentTiles() method with retrieved tile index as parameter. Otherwise, just to be sure, it clears the queue and a list of checked tiles.

So the flow of all the fun with revealing the fields looks something like this:

Queue schema

Conclusions

Sometimes a matter that seems pretty straightforward turns out to be more complicated than we thought. The cool thing about gamejams is that we don't have time to look for complex solutions and we'll create solutions on-fly to allow us to develop the game quickly.

Looking at how simple a decision to change squares into hexes can complicate a project, we're become more cautious in adding unplanned changes on the fly.

However, Gamejam has its own rights. A quick, cursory vision of the game, developing it using ideas that just came to our minds and eternal uncertainty as to how our work will look in a few hours.

But enough of my mumble. I would recommend to each of you at least once to try your hand at a gamejam. Where from the disclosure of the topic to the release of the game usually takes 48-72 hours.

You will have a great fun and learn a lot of new things.

Eurosapper by now have been played more than 13000 times. Thanks!