💣 Yet another Minesweeper. Vanilla TypeScript, no framework. Live at mines.mellonis.ru.
mines-264-1240.mp4
npm run dev— Vite dev server with HMRnpm run build— typecheck (tsc -b) and buildnpm test— Jest with coverage
Framework-agnostic game engine. No DOM, no canvas — just Minesweeper and Cell. The field owns mine placement, win/lose state, and the marks-left counter; Cell handles flood-fill on zero-neighbour reveal via a neighbour-generator injected by the field, so cells never hold a back-reference to it. Mines are placed on the first reveal() with a 3×3 safe zone for first-click safety; marks placed before that are replayed when the field is filled.
The only read interface is getSnapShot(), which returns a flat row-major array of revealed digits or sentinel symbols (cell, mark, mine, missMark, explosion). Symbol equality is by reference — always import them from ./minesweeper.
Every cell, digit, and button variant is pre-rendered once into an offscreen canvas (prepareSprite). Render-time work collapses to a single drawSprite(ctx, index) blit per widget. Sprites are sized at devicePixelRatio at module load, so the main canvas must scale by DPR to match.
Sprite indices are positional — they're defined by the order of producers in fieldSpriteProducers / counterSpriteProducers / startButtonSpriteProducers, and consumers in game.ts hard-code those indices. Appending to a producer array is safe; reordering silently breaks rendering.
Hit-testing without geometry math. Each widget gets a unique numeric id mapped to a CSS color (#RRGGBB). The manager keeps an offscreen "pick" canvas and asks every widget to draw its mask there in its assigned color; on a mouse event it reads one pixel under the cursor and looks up the widget by color. Exact, resolution-independent, works for any shape a widget can draw.
It also tracks hovered / pressed widgets and re-picks at the last mouse position when widgets become disabled, so cursor and hover state self-correct without requiring mouse motion.