English | 日本語
A browser game written in TypeScript with RxJS, built in a purely functional reactive style. Hexadecimal numbers fall from the top of the canvas; flip the bits of an 8-digit binary row at the bottom so that its value matches the lowest target before it reaches the dead line.
Built by Haruto Iriyama.
The row reads 1000 1010 — matching 8A, the lowest target, just before it lands.
| Input | Action |
|---|---|
1 – 8 |
Flip the corresponding bit (leftmost is the most significant) |
| Click a digit box | Flip that bit |
P |
Pause / resume |
R |
Restart (during play or after game over) |
M |
Sound on / off |
Click or press Enter to start. Targets fall in random columns, and each is judged the moment its bottom edge touches the dead line: a match scores a point and removes the target, a mismatch ends the game. Every 5 points you reach a new level; targets fall faster with each level and the longer you survive.
Requires Node.js.
npm install
npm run dev # serve on localhost, ctrl-click the URL in the console
npm test # vitest
npm run build # type-check and bundleEvery push to main runs the tests and deploys the build to GitHub Pages
(see .github/workflows/deploy.yml).
The whole game lives in src/main.ts and follows a
Model–View–Update architecture on top of RxJS.
- State is immutable.
StateandTargetareReadonlytypes; every update produces a new object with spread syntax. - Every input is an
Action. The 20 ms clock (Tick), keyboard and mouse bit flips (FlipBit),PauseandRestartall implement one interface with anapply(s: State): Statemethod. Keyboard and mouse produce the sameFlipBitaction, so the model cannot tell the two inputs apart. - One fold drives everything. The input observables are
merged into a single stream of actions and reduced withscan(reduceState, initialState). Because actions are applied sequentially, there is no concurrent modification and adding a new kind of input never touches the reducer. - Randomness is pure. Target values, columns and spawn gaps come from a seeded
linear congruential generator whose seed is carried in the state, so
tickis a pure function and a whole run can be reproduced from a single seed. - Derived values are not stored. The numeric value of the digit row, the level and the current fall speed are computed from the state where needed, so they can never disagree with it.
- Side effects are confined to the view.
renderis the only place that touches the DOM; it is a plain projection of state onto SVG. Sound effects come from a puresoundEvents(prev, next)over consecutive states (pairwise), played with synthesised Web Audio tones — no asset files. Targets that leave the canvas are reported through anexitlist so the view can remove their elements. - The stream never completes. On game over or pause,
Tickholds the state still instead of unsubscribing, which is what lets restart and resume be ordinary actions rather than special cases.
src/main.ts game logic, actions, input streams and rendering
src/style.css stylesheet
index.html SVG canvas scaffold
test/main.test.ts vitest tests
The build configuration and the SVG scaffold came from a starter template; the game logic, actions, input handling and rendering are my own.
