A minimalist, terminal-styled puzzle game. Flip cells with a 2×2 brush until the whole grid is a single color.
🎮 Play: https://radiant-radiation.netlify.app
The board is an N×N grid of black/white cells. A move picks an interior
vertex (where 4 cells meet) and flips the 2×2 block around it. There are
(N-1)×(N-1) possible moves. Tap/click where four cells meet — the center of the
highlighted square — to flip them. Win by making the grid a single shade —
either all white or all black.
Puzzles are generated by applying a known set of distinct moves to a monochrome
board (the base shade is chosen at random), so every puzzle is guaranteed
solvable. The displayed par is the genuine minimum number of moves — the
shorter of the all-white and all-black solutions, computed over GF(2), never an
overstatement. A move limit (par + margin) defines the challenge per
difficulty. The same board is never handed out twice in a row.
| Keyboard | Pointer | |
|---|---|---|
| Move cursor | arrows | hover |
| Flip the 2×2 | space / enter | tap / click |
| Undo | z |
right-click |
| Reset puzzle | r |
toolbar |
| New puzzle | n |
toolbar |
| Cycle difficulty | d / [ ] |
toolbar |
| Toggle theme | t |
top-right button |
| Zen mode | esc |
top-right button |
| Skip tutorial | s |
link |
| Change language | — | top-right button |
On touch devices the keyboard hints are replaced by an on-screen toolbar, and the history panel becomes a collapsible accordion.
- Three-step 3×3 tutorial before free play, ending with an unguided puzzle.
- Difficulty presets (easy/normal/hard/expert) bundling size + par + slack.
- Move limit with par comparison; auto-advance to the next puzzle after a win.
- Games history (result, moves, difficulty, board preview) with replay, persisted
in
localStorage; the in-progress game is autosaved and resumed on reload. Replaying a board in fewer moves upgrades its recorded result. - Lifetime stats panel (games played, win rate, current/best streak, average moves, par finishes, total and best solve time) — cumulative beyond the 20-game history cap. Solve time is the active time from the first move (paused while the tab is hidden). History and stats clear independently.
- Win-streak momentum line under the board (from two wins in a row): one mark per win, the newest pulsing as the streak grows; a loss clears it.
- Localized in English, French, Spanish, Japanese — auto-detected, switchable at runtime, and remembered (the Japanese title renders in katakana).
- Soft light/dark themes (follows
prefers-color-scheme); a distraction-free zen mode; on a win the grid melts into one solid shade. - Radial victory animation; respects
prefers-reduced-motion. - No accounts, no tracking, fully offline — all states are local.
Deep link: ?d=easy|normal|hard|expert starts free play at that difficulty.
Astro 6 + TypeScript (no UI framework). Pure game logic in src/game/engine.ts,
idempotent rendering in render.ts, input in input.ts, orchestration in
main.ts, the pure session→view mapping in view-model.ts, persistence in
history.ts, lifetime stats in stats.ts, and dependency-free runtime i18n in
i18n.ts. Static build, deployed on Netlify.
npm install
npm run dev # local dev server
npm run check # astro type-check
npm run lint # eslint
npm test # unit tests (Vitest)
npm run build # static build -> dist/Production — a multi-stage build compiles the static site (node:22-alpine)
and serves it with nginx:1.27-alpine, mirroring the production security headers.
docker compose up -d --build # build + run -> http://localhost:8080
docker compose down # stopDevelopment — a live Astro dev server with hot module reload, source bind-mounted from the host (edit locally, the browser refreshes itself).
docker compose -f docker-compose.dev.yml up --build # -> http://localhost:4321The dev container polls for file changes (CHOKIDAR_USEPOLLING) for reliable
watching on Docker Desktop; drop that env in docker-compose.dev.yml if native
file events work on your setup.
Contributions are welcome! Please read the contributing guide to get set up and learn the checks to run, and the code of conduct before taking part. Found a security issue? See the security policy — please don't open a public issue.
Created & maintained by Olivier Falahi (olivier.falahi@gmail.com).
Built with assistance from Claude (Anthropic); dependencies kept current by Dependabot.
MIT © 2026 Olivier Falahi