Building a Hexagon Game Board
2019-10-13
I wanted to build a dynamic gameboard in Javascript with the HTML canvas, but decided that a square grid was just too simple of a project. To step things up a notch I decided to build this basic game board a Hexagonal based system. It quickly turned into an interesting thought experiment.
A Conventional Grid
In a conventional 3x3 grid setting we use an multidimensional array to store the data for the map. This generates 9 easily addressable points.
const mapWidth = 3;
const mapHeight = 3;
const mapGrid = [];
for (let y = 0; y < mapHeight; y++) {
const row = [];
for (let x = 0; x < mapWidth; x++) {
const col = {
isOccupied: false,
x: x,
y: y
};
row.push(col);
}
mapGrid.push(row);
}
This generates for us a multidimensional array of objects like similar to this:
mapGrid = [
[{}, {}, {}],
[{}, {}, {}],
[{}, {}, {}]
]
If we want to access the Top Left, it would be addressed as mapGrid[0][0]
because javascript is a zero based system. The bottom right would be mapGrid[2][2]
. From here if we want to see if a specific cell has the isOccupied
flag set to true
, we can easily access the location with an array check.
console.log(mapGrid[y][x].isOccupied);
Assuming we have a Player Token at the center of the grid, and we wanted to move the character, we would use an coordinal system for direction (usually North, East, South, West). If we want our Token to move North, we would reduce the y
value of the token by one, and add one if moving South. Moving the Token East and West we would affect the x
value of our Token.
Although we can work with the code we have, it starts to get a little kludgy as we start to expand the functionality. Before we go to far lets, refactor what we have into classes.
class Cell {
constructor(x, y) {
this.isOccupied = false;
this.x = x;
this.y = y;
} // close constructor
} // close Cell
class Token {
constructor(name, startX, startY, map) {
this.name = name;
this.x = startX;
this.y = startY;
this.map = map;
this.map.occupyCell(this.x, this.y);
} // close constructor
move(direction) {
let newX = new Number(this.x);
let newY = new Number(this.y);
switch (direction.toUpperCase()) {
case "N":
newY = newY - 1;
break;
case "S":
newY = newY + 1;
break;
case "E":
newX = newX + 1;
break;
case "W":
newX = newX - 1;
break;
} // close switch
// Make sure our new position is within the contrains of our map
if (newY < 0) {
newY = 0;
} else if (newY > this.map.maxRows) {
newY = this.map.maxRows;
} // close if newY
if (newX < 0) {
newX = 0;
} else if (newX > this.map.maxCols) {
newX = this.map.maxCols;
} // close if newX
// Make sure out new position is not already occupied
if (!this.map.isCellOccupied(newX, newY)) {
// leave our current cell
this.map.clearCell(this.x, this.y);
// occupy our new cell
this.map.occupyCell(newX, newY);
// set our values
this.x = newX;
this.y = newY;
} // close if not occupied
} // close move
} // close Token
class MapGrid {
constructor(mapWidth, mapHeight) {
this.grid = [];
this.maxRows = mapHeight - 1;
this.maxCols = mapWidth - 1;
for (let y = 0; y < mapHeight; y++) {
const row = [];
for (let x = 0; x < mapWidth; x++) {
row.push(new Cell(x, y));
} // close for x++
this.grid.push(row);
} // for for y++
} // close constructor
getCell(x, y) {
return this.grid[y][x];
} // close getCell
isCellOccupied(x, y) {
return this.getCell(x, y).isOccupied;
} // close isCellOccupied
occupyCell(x, y) {
this.getCell(x, y).isOccupied = true;
} // close occupyCell
clearCell(x, y) {
this.getCell(x, y).isOccupied = false;
} // close clearCell
} // close MapGrid
const init = () => {
const gameMap = new MapGrid(3, 3);
const playerToken = new Token("Player 1", 1, 1, gameMap);
document.addEventListener("keydown", (event) => {
const key = event.key;
if (key === "ArrowLeft") {
playerToken.move("W");
} else if (key === "ArrowRight") {
playerToken.move("E");
} else if (key === "ArrowUp") {
playerToken.move("N");
} else if (key === "ArrowDown") {
playerToken.move("S");
}
}); // close addEventListener(keydown)
}; // close init
init();
This gives us everything we need from a data perspective to generate a map, and place a moveable token on it.
Stacking Hexagons in a Grid
This wasn’t as simple as building a grid like you would with squares as I required to offset both the x
and y
values to neatly stack each hexagon. I also added an extra cell to each even row to keep the map “balanced”. This lead to odd rows having 9 columns, and even having 10 which leads to moving issues later on.