Designing a game that ends in 20 seconds
The shorter the loop, the more input feel is everything. Priorities for hit detection, feedback and sound.
Lattice Hunter, the game on this site, lasts 20 seconds a run. Here is what building something that short taught me.
There is no room to explain a system
Twenty seconds leaves no time to read rules. The player has to understand what to do within the first second.
In Lattice Hunter exactly one cell on the board is coloured and pulsing. Everything else is the same flat grey. That single difference carries the whole message: press the lit one. There is no tutorial text.
The flip side: a game whose rules cannot be reduced to one idea will not fit in 20 seconds. Every time I wanted to add a mechanic, the test was “does this read without explanation?”
Hit detection > feedback > sound
With limited time, work in that order and you will not go wrong.
Hit detection comes first. If the click lands somewhere other than what the player aimed at, nothing else matters. Hex grids in particular do not map back from coordinates as neatly as rectangles do. Odd rows are offset by half a cell, so the inverse is:
const row = Math.floor(y / cellH);
const col = Math.floor((x - (row % 2 ? cellW / 2 : 0)) / cellW);
Feedback is second. Whether a hit landed must be readable without looking at the score. A ring expands from the click point and fades over 0.6 seconds. Without it, the game feels like it is ignoring you.
Sound is last. It is lovely to have and the game works without it. As of launch, there still isn’t any.
Keep the timer off the render loop
The countdown ticks on setInterval once a second while drawing runs on
requestAnimationFrame.
My first version derived remaining time from elapsed frames, but requestAnimationFrame
pauses in a background tab — so time simply stopped whenever you switched away.
That is far too generous.
setInterval also stretches in background tabs, mind you. Doing this properly means
recording a start timestamp and diffing against wall-clock time, which is on the list.
Whether to punish a miss
A miss costs one point. Without that, mashing is the optimal strategy and the game collapses.
But the score never goes below zero. A player who misses repeatedly at the start should not feel the run is already unwinnable.
this.setState(st => ({ score: Math.max(0, st.score - 1) }));
In a short game each mistake weighs proportionally more. Keep the punishment to the minimum that stops mashing, and no more.