Skip to content

Build1 publisher3 min readPublished

A full arcade redesign stopped at eight logic.ts files

An open-source browser arcade puts eight games behind one shell interface at about 35 kB gzipped with no runtime dependencies. The rules files import neither DOM nor Canvas, and a mid-project redesign never reached them.

The Engineer · Build desk

Photograph accompanying A full arcade redesign stopped at eight logic.ts files
Photo: dev.to

What happened

  • A browser arcade puts Snake, Tetris, Breakout, Flappy, 2048, Minesweeper, Sudoku and Gomoku behind one arcade-cabinet UI that takes both keyboard and touch, with no install and no sign-up.
  • The whole build is plain TypeScript and Canvas 2D with zero runtime dependencies, MIT licensed, and about 35 kB gzipped once fonts are excluded.
  • CI runs 269 unit tests and 28 Playwright end-to-end tests, 297 in total, according to the project's own write-up.
  • Every game implements one interface and receives a GameContext handing it synthesised audio, a localStorage wrapper, an input service, an overlay call, three DOM slots and a hints setter.
  • Each game carries extras past the base rules, including hold and hard drop in Tetris, single-step undo in 2048, and pencil notes with auto-save in Sudoku.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Banning DOM and Canvas imports from the rules files means no game can shortcut by reading the rendered board; every piece of state the renderer needs has to exist in the model first.
  • capability With three declared DOM slots and one interface, a new game is additive work, and the review question becomes whether GameContext needs another capability rather than whether shell code regressed.
  • decision The hybrid rendering choice is settled per widget here, so a team can paint the play area and still get focus and keyboard behaviour free on the controls around it.
  • exposure Centralising overlay, pause, mute and audio in the shell means one defect in that code path reaches all eight games at once.

Halfway through the project the arcade got a full visual redesign, from dark neon to the current warm paper look [11]. The dev.to write-up says the change touched the rendering code, the shared shell and the stylesheet, and that the eight logic.ts files and their tests did not change by a line [11][22]. The check is a command run against a base commit: git diff --stat over 'src/games/*/logic.ts' and 'tests/*-logic.test.ts' must come back empty [12].

That holds because of a rule about imports. logic.ts carries state and rules with no DOM and no Canvas, and the random source is passed in as a parameter, while index.ts draws the state and turns input into rule calls [8]. In 2048 the merge step is an ordinary function over an array: slideLine takes a line of numbers and returns the compacted line plus the score gained [9]. Its test asserts that slideLine([0,2,0,2]) returns { line: [4,0,0,0], gained: 4 }, and it runs in Node without a browser [10]. A rules file that receives its randomness as an argument can be pinned to a fixed sequence in that kind of test [8].

The shell owns the parts a player notices between games. A game calls ctx.overlay once with a title, a tone, some lines, actions and hints, and the shell draws the result card [6]. Pause, mute and back-to-hub then behave the same way in all eight games, and each game's code deals only with its own rules [7].

Rendering is split by widget. Grids, blocks and sprites are painted, and the boards take clicks on the Canvas, while the number pad, the difficulty menu and the undo key are real button elements [13]. The reason given is focus rings, keyboard access and touch targets that are easy to size [14]. "Buttons painted on a Canvas need all of that written by hand, and it never comes out as well," the author wrote [15].

Gomoku is the one game with more than two files. Its three difficulty levels are minimax searches at depths 1, 2 and 4, with alpha-beta pruning [17]. Depth 4 on the main thread freezes the UI, so the search runs in a module Worker [18]. Restarting mid-search creates a stale answer that must not land on the new board: every restart increments a token, and the Worker's reply carries that token back [19]. The post's text stops mid-sentence at that point, so the comparison step is not shown [19].

Whether the size figure means anything elsewhere depends on how much the app resembles this one. Split evenly across the eight games, the gzipped build is roughly 4.4 kB each, and the shell with its audio, storage and input services is counted once [21]. The figure excludes fonts, which the post states plainly; fonts are also the easiest thing to leave out of a bundle number [2]. These are eight self-contained state machines that persist through a localStorage wrapper [5]. None of them fetches from a server, and none has to keep a list on screen in sync with a remote one. Reconciling remote state with what is rendered is most of what a framework does in a line-of-business app, and that work is absent here. All of the figures come from one self-published post about the author's own project [22].

What to watch

  • A measured page load with fonts and any runtime assets included, to see how close the delivered bytes sit to the 35 kB build figure.
  • Whether the 28 Playwright tests exercise touch gestures on real devices or only emulated viewports, since the touch pad is a declared shell slot.
  • Someone shipping a ninth game against the published GameContext without editing shell code.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories