One of my favorite board games is Hey, That's My Fish! It's a really cute game where color-coded teams of penguins slide around a hexagon-based ice field. Each piece of ice has 1 2 or 3 fish, and every time a penguin exits a piece the penguin claims the fish, but the piece melts, so the board is ever changing, and there's a lot of strategy in trying to block other penguins with melted ice and penguin bodies.
It's a strategy game, but not TOO think-y, or too much secret information to keep track of, which is a good balance for me.
There's an excellent version for iOS (it's ideal as an iPad party game) and Android, but also a free online version at yucata.de. The latter is at a really nice and congenial site (the Germans love their board games) but the interface is a little clunky for Zoom/video conference play... it's a bit geared at remote players taking turns at their leisure, and the whole site is a bit Web 1.5 feeling and requires accounts to be set up, so I'm not sure if I feel comfortable recommending it for casual work get togethers.
(From a UX standpoint, one really nice feature at the Yucata version is it uses colors to highlight the last move each player made... great when a game is going fairly slowly or people aren't being 100% attentive)
I'm not sure if it's worth coding my own version of the game (like I did for the NSFW card game Joking Hazard ) especially since it only goes up to 4 players. (If it went up to 6 or 8 it would be perfect for work game night...) But, the idea of teaching a computer to do a hex grid was interesting! Many strategy games I grew up with used hexes, (I especially remember Steve Jackson's OGRE)... there's more symmetry in the 6 directions (vs square grids, where diagonal moves go a lot further than the cardinal directions)x
As my friend Jon pointed out, a hex grid can kind of be represented as a plain old square grid, with every other row offset, but still since each hex has 6 neighbors and each square grid has 8, it was worth sketching things out and trying to get a sense of how things flowed when you treated a hex grid in terms of x,y coordinates...
|
I do love Apple Notes App for this kind of thing!
|
Interesting - every hex has East/West and North/South neighbors, but when coerced to a square grid, like... G has neighbors to the NE and SE (namely C and M) but M, one row down, has NW and SW. Every other row, then, acts a bit differently (which is why it seemed like mod arithmetic might do the trick, as we'll see later on.
I don't have super-strong spatial intuition, so I decided to just start coding incrementally rather than planning everything out. And I decided to code things up in P5, using its convenient editor.
My first program just drew a hex grid. To be frank I just dabbled numbers until things looked right.
I started using a convention of "i,j" for the hex location, and "x,y" for the coordinate on screen. Empirically I found out if "BASESZ" was the base width... every other row had to be offset, hence
x = (i + (j % 2) / 2) * BASESZ
and the y was packed a little tighter, based on how I was drawing hexes:
(I didn't bother to check if these were "perfect" hexes, frankly if they're a little squashed it's fine)
For my second program, I started using a new data structure, just a flat list of all hexes... not quite object oriented per se, but each hex knew where it was located and could draw itself. I also draw a circle inside each hex... it was easier to do a simple pythagorean dist() to see if the mouse was over the thing than any fancier math. On every mouse move, I reset each hex's "on" key to false, and then for each hex if it's in the distance it gets set to true.
Hex Experiment 3 finally gets a little interesting. Penguins in "Hey, That's My Fish!" move a bit like a queen or rook in chess, as far as they want in any of the 6 directions (but can be blocked by missing hexes or other penguins) so I thought I'd show what moves were available from whatever hex the mouse was on:
Besides the previous version's flat list, I stored each hex in an [i,j] two dimensional array, (It's a little weird when rolling your own 2D array, Rows come first, then Columns... at least the way I think about it) I then made 6 functions, each one returning an array of hexes leading out in that direction - basically a walker loop start at the base hex and proceeding whatever direction. East and West were easy, but NW/NE/SW/SE needed a little finagling... to look up the neighbor in the row above it was moving leftor right, like for southwest the change in i (for horizontal position changed)
i -=((j+1)%2);
where j is the current row. By making it visual, it was pretty quick to figure out what the math should be - a crude but effective way of getting it right.
Finally Hex Experiment 4 uses a slightly better syntax, letting each directional be a call to the same function with just a "incrementI, incrementJ" pair of functions. That way if I ever do make penguins, I only have to fix the loop to get blocked by other penguins or missing hexes in one function instead of 6.
For safety, here's the final code:
const BASESZ = 40;
const COLCOUNT = 8;
const ROWCOUNT = 8;
const hexList = [];
const hexGrid = [];
const SHIFTLEFT = 50;
const SHIFTDOWN = 100;
const OFF = 0;
const ON = 1;
const OVER = 2;
function setup() {
createCanvas(400, 400);
for (let j = 0; j < ROWCOUNT; j++) {
hexGrid[j] = [];
for (let i = 0; i < COLCOUNT; i++) {
const hex = makeHexForLoc(i, j);
hexList.push(hex);
hexGrid[j][i] = hex;
}
}
}
function draw() {
background(220);
push();
translate(SHIFTLEFT, SHIFTDOWN);
hexList.map((hex) => {
drawHex(hex)
});
pop();
noLoop();
}
function makeHexForLoc(i, j) {
const x = (i + (j % 2) / 2) * BASESZ;
const y = (BASESZ * 0.75) * j;
return {
i,
j,
x,
y,
mode: OFF
};
}
function mouseMoved() {
hexList.map((hex) => {
hex.mode = OFF;
});
const boardX = mouseX - SHIFTLEFT;
const boardY = mouseY - SHIFTDOWN;
let pickedHex = null;
hexList.map((hex) => {
if (dist(boardX, boardY, hex.x, hex.y) < BASESZ / 2) {
pickedHex = hex;
}
});
if (pickedHex) {
//Check Easts
getOpenHexesInDir(pickedHex,()=>1,()=>0)
.map(hex => hex.mode = ON);
//Check Wests
getOpenHexesInDir(pickedHex,()=>-1,()=>0)
.map(hex => hex.mode = ON);
//Check North Easts
getOpenHexesInDir(pickedHex,(i,j)=>j%2,()=>-1)
.map(hex => hex.mode = ON);
//Check North Wests
getOpenHexesInDir(pickedHex,(i,j)=>-(j+1)%2,()=>-1)
.map(hex => hex.mode = ON);
//Check South Easts
getOpenHexesInDir(pickedHex,(i,j)=>j%2,()=>1)
.map(hex => hex.mode = ON);
//Check South Wests
getOpenHexesInDir(pickedHex,(i,j)=>-(j+1)%2,()=>1)
.map(hex => hex.mode = ON);
pickedHex.mode = OVER;
}
loop();
}
function getOpenHexesInDir(hex,iTransform,jTransform){
let {
i,
j
} = hex;
const res = [];
while (i < COLCOUNT && j < ROWCOUNT &&
i >= 0 && j >= 0) {
res.push(hexGrid[j][i]);
i += iTransform(i,j);
j += jTransform(i,j);
}
return res;
}
function drawHex(hex) {
const {
x,
y,
mode
} = hex;
const sz = BASESZ / 2;
push();
translate(x, y);
//sides
line(-sz, -sz / 2, -sz, sz / 2);
line(sz, -sz / 2, sz, sz / 2);
//top
line(-sz, -sz / 2, 0, -sz);
line(0, -sz, sz, -sz / 2);
//bottom
line(-sz, sz / 2, 0, sz);
line(0, sz, sz, sz / 2);
fill(255);
if (mode === OVER) fill(255, 128, 128);
if (mode === ON) fill(128, 128, 255);
circle(0, 0, sz * 1.5);
pop();
}
I'm not sure if I'll ever make the online version of this game, but this was a satisfying bit of computer math to get done on a Saturday morning.