Simulation

domain/simulation.ts is the heart of the game: it executes your program exactly the way the on-screen mouse does, and it decides whether you won. It is pure TypeScript, no JSX, no i18n, fully unit-tested.

The pieces

Outcomes

OutcomeWhen
wonthe player lands on the target cell.
blockeda move ran into a wall or out of bounds.
not-reachedthe program ran out of actions before the target.
trappedthe player stepped onto lava or a hole.
infinite-loopa while kept repeating the same state.

The state machine and its animation timings are described in The playground and in the outcome handling of useSimulation.

Sliding on ice

ice cells are slidable. When a move lands on ice, collectSlidingTrail keeps advancing in the same direction while the next cell is in bounds and passable, and stops when the floor stops being slippery. Two rules make slides fair:

The slide trail feeds the animation (getMoveTrail), so what you see is exactly what the simulation computed.

Traps

lava and hole are passable but lethal. That distinction is the point of a trap: a path condition (path-ahead and friends) reads them as open, because "can I move there" and "is it safe to stand there" are different questions. isTrapped(position, level) answers the second one, and the run ends in trapped the moment the player stands on a trap.

The trap design rule follows from this: a trap is only meaningful when it sits on a route that would otherwise reach the target. A trap on a dead end is not a trap, the run would fail there anyway. So each level uses at most one trap, never placed on the authored optimal solution path, always where a plausible algorithm turns in.

Loop detection

A while block is only bounded by its condition ("target not reached"), so a wrong program can loop forever. The simulator stops that two ways:

Both limits are constants at the top of the file, and the threshold is shown in the user-facing message ("repeated the same state 3 times").

Execution vs counting

The same tree is walked three times for three different purposes:

How useSimulation animates it

features/level/hooks/useSimulation.ts is the bridge between the pure simulator and the screen:

  1. run() records the block and cycle counts, marks the state running, computes the outcome up front, and starts a timeout-driven animation.
  2. The animation replays collectExecutedActions step by step: turns rotate the mouse the shortest way (shortestTurn, so it never spins 270 degrees), moves slide it between cells (--step-x/--step-y), ice slides walk the trail waypoint by waypoint.
  3. The final status is held back briefly (WIN_SETTLE_MS 650 ms, TRAP_SETTLE_MS 850 ms, FAILURE_SETTLE_MS 250 ms) so the last frame, the mouse on the cheese or the trap animation, is visible before the outcome modal or toast appears.
  4. Editing the program or switching levels clears the pending timer and resets to idle, so a stale run can never fire.

Next: Cycles and optimization.