The playground
The playground is the game board: a grid of cells, a walking mouse, a cheese
target, and the traps. It is rendered as a real <table> for the simple
reason that a table is the correct semantic model of a labelled grid, and
screen readers understand it for free.
Structure
components/Playground/Playground.tsxrenders the frame and the table: one header row of column letters (A, B, C...) plus a corner cell, then one row per map row, each prefixed by its row number. Letters come fromlib/column.ts(columnLetter, throws outside 0..25).components/Playground/GridCell.tsxrenders one cell. It receives the position, the block kind, the level's look, the player state and the target flag, and lays out the cell's layers: terrain background, the checkerboard tint, the trap art (lava pool or hole), the target cheese, and the player figure when the mouse is on that cell.- Every cell is a
<td>with an explicit width and height, so cells are exactly square whatever the map shape.
The sizing math (why cells are square)
Tables want to size themselves from content, which would make a 5x7 map's cells slightly tall. The app refuses that: all geometry is computed from two custom properties and applied explicitly.
.playground-boxcaps the board at--playground-limit(the largest square that fits the viewport, with a sane cap on huge screens)..playground-frameis an inline-size query container..playground-gridgets--playground-columnsand--playground-rows(the map size) and derives:--playground-label-size- the label track,clamp(1.5rem, 6% of the limit, min(2.25rem, half a tile)), so digits fit but never eat the board;--playground-tile-size- the smaller of (frame width minus label) per column and (limit minus label) per row.
- Each track type gets explicit sizes: the corner and the two label tracks
take the label size on one axis, and every data cell takes
--playground-tile-sizefor both width and height.
Because one value drives both axes of every data cell, cells are square by
construction, on any map shape and any viewport. The historical
aspect-ratio on the table was removed for exactly this reason: it ignored
the label row and column and drifted to 1:1.05 on non-square maps.
Terrain and textures
Each cell's background comes from its terrain identifier (data-terrain
data-kindattributes, derived from the level'slooks), mapped insrc/terrain.cssto a colour token and a procedural texture:
- Textures are asset-free, tileable inline SVG data URIs:
feTurbulencenoise withstitchTilesfor rock/sand/grass/frost/gravel, a jittered upright evergreen canopy for the forest wall, stripes for ice, polygons for the purple boss theme. - Every cell paints the same seamless texture from the same origin, with no per-tile transforms or background shifts, so every tile is identical in size and shape and the grid is perfectly respected. (An earlier per-tile variation experiment rotated and phase-shifted tiles; on fractional cell sizes the rotations bled a pixel or two past the cell and the shifted phases broke the pattern at tile seams, so it was removed.)
- Traps draw their marker on top of the level's ground look; ice always uses
the
iceterrain.
The full art direction is on the Design system page.
Entities
- The player -
components/PlayerMouse.tsx: a flat mouse seen from above (oval body, round ears, dot eyes, a tapering curled tail), the same component used for the skin previews. It walks between cells (.player-stepslides it in from the cell it came from), its accumulated rotation always takes the shortest way round a turn, it bobs while running and breathes while idle. - The target -
components/TargetCheese.tsx: a cheese wedge cut from the wheel, with holes rolled bycheeseHoles.ts(constrained to the wedge so they never clip or clump, lighter on top, darker on the cut face). It is hidden while the mouse stands on it. - Lava - a clipped, jagged molten puddle with fixed
.lava-glowpatches that fade in and out on independent linear clocks. On death the mouse evaporates upward (player-burn), and it never grows back. - Hole - a flat, uneven dark pit. On death the mouse spins and shrinks
away (
player-fall).
Only the player and the lava animate; everything else is deliberately still.
prefers-reduced-motion disables the player and lava animations.
Accessibility of the board
Every cell is a real table cell with an accessible name: column and row
headers announce the coordinates, and each cell carries a title and a
screen-reader-only label for its contents (ground, wall, ice, lava, hole,
target, player). Details on Accessibility.
Next: Design system.