Slay – tutorial three – the map generator

Small onslaught hexesThis is the third tutorial for the Slay game on Linux (called Onslaught). Follow the link for the first tutorial in this series.

This tutorial is about the map generator. It was derived from an earlier one I wrote for the Empire game. You can see that on GitHub in the file aboutmpire.zip. Click that file then click download on the page that opens. Then in that open the empire9src.zip and the original map generator files in that are are mapgen.h/.c, common.h/.c and data.h.  Those are just for the curious; we won’t need them for this tutorial. There is a copy of data.h in the source files for this tutorial.

Download the source files for this tutorial from GitHub. They are in the file Onslaught2.zip.

Note the file contains both a Windows .sln file and a .vscode folder with JSON files for compiling with clang under Linux.

Now that we can display hexagons (which in true wargame style I will call hexes from now on) , it’s time to create the map generator. The purpose is purely to create a playable map. As part of the generator, the player’s starting positions are also determined.

The maps in the game are 20 across by 15 deep (small), 30 x 20 across (medium) and 40 across x 30 deep (large). We want these to fit on a screen say 1024 x 768 so a hex size of 34 pixels high by 32 pixels wide almost suits our purposes. It gives a large map of 31 hexes across by 28 down. We’ll need a bigger window for large maps and I used 1300 x 760.

Generating a Map

In the empire map generator (in the zip file AboutEmpire.zip) on GitHub  I used multiple land points and sea points on an empty map.  These are randomly placed on the map then a process of building up layers around each point adds land and sea to the map. The only rule being you can only add a layer point to an empty cell. So when the points grow into each other you get the interesting shapes.

For our map the only difference is that we want to have one continent created not 2 or 3 like in Empire and the total size must be between 50% and 80% of the available space. So a small map of 300 hexes (15 x 20) will have a continent varying in size between 150 and 240 hexes.

How it Works

Ontion text map generator

A very long time ago I created a text file which looks a bit like this on the left. What that is, is 36 layers of text. Start with one point. Add 8 letters around it then 12 Bs and so on. The shape looks a bit weird but it is a circle except that text characters aren’t square so it looks longer and narrower. Here’s what the centre bit looks like. So it goes outward A-Z then a-o but works out as 35 layers.

Circle-text I’ve converted all these characters into relative offsets in the file data.h. There are 35 NumLayerPoints.

#define MaxLayers 35
int NumLayerPoints[MaxLayers]=
{8,12,16,32,18,28,40,44,60,52,54,56,72,80,76,100,86,92,96,96,128,128,106,
120,136,140,140,168,126,160,168,164,172,180,156};

So 8 As, 12 B’s and ending with 156 o’s. Then there are the offsets, sayiong where in a large array of points, each layer starts.

int Offset[MaxLayers]=
{0,16,40,72,136,172,228,308,396,516,620,728,840,984,1144,1296,1496,1668,
1852,2044,2236,2492,2748,2960,3200,3472,3752,4032,4368,4620,4940,5276,5604,
5948,6308};

Then finally there is the very large array circlepoints.

const int circlepoints[6620]={
-1,-1,0,-1,1,-1,-1,0,1,0,-1,1,0,1,1,1,

That shows the first 8 points- all the A’s relative to the * in the centre. There are 8 points for A that start at offset 0. Each two values are the x and y offsets. The offset for the 12 B points is 16 (8 points for A x 2). So by using this data, it’s possible to build up up to 35 layers around every point.

The map uses a struct with the island field (read it as is land) using an enum value. ltEmpty is the default and the map is initially set to all ltEmpty. I use landtype (a typedef for the enum maptype) so I don’t have to prefix everything with enum.

enum maptype {ltEmpty, ltLand, ltSea};
typedef enum maptype landtype;
struct hex {
	int map;       	
	int continent; 
	landtype island;  // ltempty, ltland or ltsea
};

Going Cross-platform

Though I said initially this was only going to be a Linux game, I decided to make it Windows for one main reason. The debugging is a lot better in Visual Studio than Visual Studio Code when it comes to SDL programs. So the few places that differ now have compiler #ifdef _WIN32 directives. For example this is the start of onslaught.c now.  I’ve shown the Windows-only code show in Bold and the Linux-only in italic. The rest is common to both.

#include "hr_time.h"
#include <time.h>
#ifdef _WIN32
<strong>#include <SDL.h>   // All SDL App's need this 
#include <SDL_image.h></strong>
#else
<em>#include<linux/time.h>
#define __timespec_defined 1
#define __timeval_defined 1
#define __itimerspec_defined 1
#include <SDL2/SDL.h>   // All SDL App's need this 
#include <SDL2/SDL_image.h></em>
#endif
#include <stdio.h>
#include <stdlib.h>
#include "data.h"

Due to laziness, I have a slightly different path for SDL on Windows compared to Linux. I’m sure I will fix it one day so the two are the same. The Linux one seems fixed, but as I put the Windows include path into Visual Studio, it should be possible to change it.

The only other places where Windows and Linux really diverge are things like the c string functions and fopen which Visual Studio gets very antsy over, if you don’t use the _s versions. So don’t use strcat but strcat_s on Windows and strncat on Linux. I use them so infrequently, it’s not a big thing anyway.

Fine tuning Map generation

On problem I found initially was the Empire map generator developed multi continent maps. These three #defines are what control map generation and I had to play with them quite a bit till it started producing the type of map I wanted with one continent.

#define LANDDISTANCE 7
#define LANDPOINTS 25
#define SEAPOINTS 15

The LANDDISTANCE is used when placing land points. It is used in the function NearLand() which scans the whole map and uses Pythagoras to calculate a distance.

// Return 1 if distance is <= LANDDISTANCE
int NearLand(int xo, int yo) {
	for (int y = 0; y < MAPHEIGHT; y++) {
		for (int x = 0; x < MAPWIDTH; x++) {
			if (map[x][y].island == ltLand) {
				int distance = ((xo - x) * (xo - x)) + ((yo - y) * (yo - y));
				if (distance <= LANDDISTANCE) return 1;
			}
		}
	}
	return 0;
}

I don’t calculate the square root, so LANDDISTANCE is really the square of the distance. It’s a minor optimisation. There’s possibly a case for just picking random points; it might be quicker and wouldn’t have an inherent bias (This always goes from top left to bottom right) but it is pretty fast. Press N a few times to see that.

Calculating map size and polishing the map

Before the function CountContinents() is called, the function FillInBlanks() looks for all empty map locations and sets them to sea.

I use a recursive function to measure the map size.  The function CountContinents() goes through the map and calls the function FillIn(). This uses the array AllContinents to count all contiguous hexes (I’m being lazy and using all 8 locations rather than six for hexes). It uses the continent field to track these. Initially the conmt9inent field is set to -1 for every location. As soon as CountContinents finds a location with a continent of -1, it starts finding all contiguous locations of the same type (sea or land) and continent = -1 and sets continent = the numbner of continents.

Note that the sea is continent 0.

After CountContinents has finished, the variable NumContinents has the number of continents and the array AllContinents has an entry for each continent with the type and count.

Next is the pruning stage. This iterates through the AllContinents table and for any land with less than 50 squares, it is converted to sea by calling the recursive function Sinkat. Finally the AllContinents array is checked to see that there is only one large land mass with between 35% and 60% of the game area as land. If not the map is rejected and regenerated.

Setting the players starting position

The function SetPlayers() is passed the number of players (8 for now) . It calls FindRandomLand() and then tries to allocate 4 or 5 blocks of troops out of the hexesPerPlayer that is has calculated for each player. This is somewhere in the region of 55-65 hexes for each player. The blocks are placed by calling AddBlockPerPlayer() which then distributes the remainder of unplaces hexes randomly across the map. The idea is to have a few clumps.

I found a couple of bugs.

  1. There were a few, typically a dozen or less hexes that never got players allocated.  So I added a function FixErrors that looked for them and set them to a random player. I’d added an error hexagon into the graphics to highlight them.
  2. Occasionally, Sinking a continent leaves a few single hexes. I had added the isvalidhex() function to fix and earlier problem where there were two continents very close together.  So roughly 1 in 6 maps has this. I’ll get it sorted for the next tutorial.

So this is a typical Onslaught map. As you can see it has one isolated cyan hex.

Onslaught map

In the next tutorial, I’ll be adding in forts, trees and wild spaces. I’ll also add the mouse hit detection code so you can detect the coordinates of the hexagon you click on.

I tested this on Linux and it compiled and ran without any issues.

(Visited 306 times, 1 visits today)