Snake Classic Game drives its animation with a recursiveDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ac-unefm/snake-game/llms.txt
Use this file to discover all available pages before exploring further.
setTimeout rather than requestAnimationFrame. This is a deliberate choice: because the tick interval must shrink as the player’s score grows, a fixed-rate requestAnimationFrame loop would require extra bookkeeping to throttle updates. Using setTimeout directly with a computed delay keeps the implementation simple — each tick schedules its own successor at exactly the right interval for the current speed.
Loop lifecycle
startGame()
startGame() calls init() to reset all per-attempt state, sets running = true, clears the status message, focuses the canvas, and calls scheduleNext() to kick off the first tick.scheduleNext()
setTimeout is getTick(score) — a value that decreases as the score rises. The returned handle is stored in loopId so it can be cancelled by endGame().tick()
update() calls endGame(), running is set to false and scheduleNext() returns immediately without queuing another tick.update() logic
update() is the game’s state machine — called once per tick, it advances the snake and checks every termination condition:
- Direction commit —
diris updated from the bufferednextDirat the start of each tick, not at input time. This prevents mid-tick direction changes from causing inconsistencies. - New head — the new head cell is computed by adding the direction vector to the current head position.
- Wall check — if the new head lies outside
[0, COLS)×[0, ROWS), the attempt ends immediately. - Self-collision check —
snake.some(...)scans every existing segment. A hit on any cell triggersendGame(false). unshift(grow) — the new head is prepended to thesnakearray. At this point the snake is one cell longer than it should be.- Step counter —
attemptStepsis incremented unconditionally; it counts every cell traversed regardless of food. - Speed tracking —
attemptMinTickrecords the shortest tick interval seen this attempt (used to compute peak speed in session stats). - Food collision — if the new head coincides with
food,scoreis incremented, level and best-score checks run, andplaceFood()is called. The tail is not removed, so the snake stays one cell longer — net growth of one segment. pop(no food) — when no food was eaten, the last tail segment is removed, keeping total length constant.
placeFood() algorithm
Rather than retrying random positions until a free cell is found (which would loop indefinitely on a nearly-full board), placeFood() builds an explicit list of all free cells and picks uniformly from it:
- A
Setof"x,y"strings is built from the current snake body in O(n). - Every cell in the 20×20 grid is tested against the set. Free cells are collected into
free[]. - If
freeis empty the board is completely filled —endGame(true)is called, signalling a win condition rather than a loss. - Otherwise a uniformly random free cell is selected.
Direction buffering
The game maintains two direction variables:dir— the direction that was actually applied in the last tick. Written only insideupdate().nextDir— the direction requested by the player. Written by all input handlers.
applyDirection() before updating nextDir:
d.x !== -dir.x || d.y !== -dir.y rejects any direction that is the exact opposite of the current direction. Without this guard, the player could reverse 180° into the snake’s own neck on the very next tick, causing an instant self-collision.
Because nextDir is only read at the start of update(), multiple key presses between ticks are collapsed into the last valid one — the buffer holds exactly one pending direction.
Speed functions
Two small pure functions encapsulate the entire speed schedule:getLevel maps score to level: every STEP_SCORE (5) points the level increments by 1, starting at level 1.
getTick converts a level to a tick interval in milliseconds: it subtracts STEP_MS (12 ms) per level above 1 from the BASE_TICK (130 ms) base, floored at MIN_TICK (45 ms). At level 1 the interval is 130 ms (~7.7 ticks/s); at level 8 and above it is 45 ms (~22 ticks/s). The Math.max guard ensures the interval never drops below MIN_TICK regardless of score.