The World at PlayWorld Games · Ready to Play

AI engineering note · 02

Putting board-game AI in the browser: from TypeScript and Wasm to neural engines

The AI in this project is not one algorithm wearing dozens of boards. We began with TypeScript searches that could produce a legal move, ran into shallow horizons, explosive branching, hidden-information leaks, and badly chosen Wasm boundaries, and gradually arrived at a system that chooses an algorithm per game, adapts to the device, and computes entirely inside the browser.

  • Game AI
  • WebAssembly
  • On-device computing

The first job was not strength; it was one correct move

The earliest engines were deliberately plain. Rules and positions lived in TypeScript. The AI generated legal moves, scored positions with a handcrafted evaluation, and ran depth-limited Minimax or Alpha-Beta. Gomoku cared about open threes, fours, and central control; Othello cared about corners, mobility, and stable discs; Connect Four first checked immediate wins and forced defenses. The evaluator was never universal, and that became our first durable lesson: game AI starts by stating what matters in this particular game, not by choosing a fashionable search framework.

The browser adds product constraints to the algorithm. Search must not stall board animation. An undo, restart, or page change must invalidate the previous calculation immediately. A low difficulty setting cannot suddenly think for seconds because one position branches more widely. Search therefore moved into Web Workers early, used iterative deepening under a time budget, and published only the last fully completed iteration. A half-searched layer is discarded rather than allowing moves examined first to receive an accidental advantage.

TypeScript was invaluable at this stage. Rules, search, and interface shared the same data structures, so an illegal move could be debugged without tracing bytes across a language boundary. It gave us an authoritative rules layer, reproducible fixtures, and a complete fallback before any optimization began.

One Alpha-Beta cannot absorb every game

For deterministic, fully observed, zero-sum games, Alpha-Beta remains the dependable backbone. We gradually added PVS, transposition tables, Zobrist hashing, killer and history ordering, quiescence, and controlled extensions. The useful order was usually less glamorous: reduce position copies, make mutation reversible, place good moves first, then make the cache store correct bounds. Completing one more full depth within the same budget is often worth more than a slightly more elaborate evaluator.

A change in rules can demand a change in search semantics. Kalah's extra turn must not mechanically flip the score. A Santorini move, build, and god-power effect form one search turn. Two-player Blokus can use PVS, while four-player Blokus needs MaxN and a separate utility for every seat. Othello is well served by Alpha-Beta in the middle game and an exact solver once few empty squares remain. Much of writing an AI per game is getting turn boundaries, terminal states, and evaluation perspective exactly right.

Some boards do not reward fixed-depth search alone. The first Hex engine was root UCB plus rollouts: it carried the MCTS name without a real opponent-level tree. It became a multi-level UCT and RAVE search with bridge knowledge, bounded H-search, and small exact endings. The initial Amazons position has 2,176 complete queen-move-plus-arrow actions, so its tree now expands queen movement and arrow placement in two stages and solves separated chambers when territories split. An algorithm is not a label selected from a menu; it has to follow the structure of the game's branching.

Hidden information is not just another random variable

Banqi, Junqi, and Stratego forced us to redefine a position. A fully observed board has one state; a hidden-information board represents a set of worlds that could still be true. The AI must never read the real concealed identities from page memory and call the result inference. Its input is limited to the player's public observation: revealed pieces, eliminated identities, combat history, and publicly remaining inventory. Replacing an unseen true identity must not change the decision.

The three games eventually needed three approaches. Banqi uses public-information-set MCTS for reveal chance nodes, then belief Negamax or joint-world enumeration as uncertainty shrinks. Junqi maintains identity domains and runs Alpha-Beta across a weighted batch of possible worlds while carrying beliefs between turns. Stratego uses RIS-MCTS and re-determinizes for the acting player, preventing a player inside the search from inheriting information they should not possess. Only when a game becomes fully revealed do these engines return to ordinary Alpha-Beta.

There is also a subtle fairness rule. One sampled world must cover every public root move atomically. If time expires after only half the candidates are evaluated, the whole world is discarded; moves ordered early cannot receive more favorable worlds. We therefore compare visits, sums, squared sums, and worst outcomes for every root under fixed seeds when checking TypeScript and Wasm—not merely the final selected move.

The TypeScript-to-Wasm breakthrough was moving the boundary

We first tried the obvious shortcut: keep the tree in TypeScript and call Wasm only for leaf evaluation. It performed poorly. Every leaf required board encoding and a JS/Wasm crossing, while the evaluation itself was too short to repay that cost. In same-machine tests, the isolated Wasm evaluator for Dots and Boxes ran at 0.79 times TypeScript speed; Blokus reached 0.06 times and Kamisado 0.21 times. Loading a Wasm file and accelerating an AI are entirely different achievements.

The real change came when complete search moved across the boundary. Move generation, make/unmake, rule adjudication, evaluation, ordering, transposition tables, and recursion all remained inside one Wasm call. On the same fixtures, complete search reached about 22.55 times TypeScript speed for Dots and Boxes, 9.62 times for Blokus, and 8.65 times for Kamisado. The gain came from avoiding thousands of object allocations, position copies, and boundary crossings as much as from native numeric execution.

The result varies too much by game for us to claim that ‘Wasm is ten times faster.’ Gomoku's Rust engine was about 4.43, 4.50, and 12.84 times faster than TypeScript at fixed depth six under its three rule sets. Surakarta reached about 28.83 times the nodes per second and moved from depth two to depth four in the same roughly 950 ms budget. Chinese Checkers was about 15.33 times faster at fixed depth four. Kalah and Oware measured about 7.81 and 6.95 times at fixed depth nine, turning a 180 ms middle-game search from depth ten into depth thirteen and fourteen respectively. These are directional results from fixed positions on one machine, not promises for another device and certainly not Elo conversions.

TypeScript did not disappear. It remains the rules referee, serialization entry, legal-move verifier, and complete fallback search. A Wasm download, self-test, memory failure, Worker failure, or illegal result switches back to TypeScript at the healthiest available execution tier. Optimization made the main road faster without dismantling the escape route.

  • Fixed-depth tests answer how much faster the algorithm runs; equal-budget tests answer what extra work reaches the player.
  • Alpha-Beta publishes only the previous complete iteration on timeout; MCTS merges only complete statistical rounds.
  • Every move returned by Wasm is validated again by the TypeScript rules layer.
  • Throughput and color-swapped self-play are reported separately; speed is not presented as playing strength.

Neural networks belong where they solve a real problem

We did not train one generic network for every game. Chess, Xiangqi, Makruk, and Shogi already have mature engines shaped by years of specialist work. Replacing them with a much weaker home-grown model would add novelty, not value. Chess uses Stockfish 18 Lite NNUE. Xiangqi can choose between Fairy-Stockfish's roughly 10.7 MB network and Pikafish's roughly 49 MB dedicated NNUE. Makruk uses Fairy-Stockfish's dedicated network. Shogi runs YaneuraOu with Suisho 5 and keeps Fairy-Stockfish as a backup. Each engine runs as Wasm in its own Worker.

A same-device Xiangqi check illustrates why model size, speed, and strength must be kept separate. On one position with six threads and one second per engine, Fairy searched around 3.0 million nodes per second to depth 17, while Pikafish reached about 3.9 million and depth 22; both selected b1c3. That proves both browser engines worked on that machine. It does not establish a rating gap.

Go and Backgammon pose two different neural problems. Go uses KataGo b6, b10, and b18 models and falls through WebGPU, multithreaded TensorFlow.js Wasm, single-threaded Wasm, and CPU backends. It can reuse a searched descendant, but only after verifying move count, exact path, board, side to move, ko point, and superko history. Backgammon prefers GNU Backgammon's contact, race, and crashed networks. GNUbg evaluates moves, Gammon and Backgammon probabilities, cubeful equity, doubles, takes, and passes. A simpler local threshold is allowed only when the engine is unavailable; it never overrides a successful professional equity result.

These specialist engines share a boundary. We optimize loading, threads, cancellation, context delivery, and fallback without silently changing their features, weights, or output meaning. The mature engine owns its specialist judgment; our rules layer confirms that the result is legal in the position on screen.

The server only puts the engine in your hands

All of this computation ends up inside the browser. On the first visit to a game, the page downloads the Worker, Wasm program, and neural weights needed by that game, much like downloading an image. Positions, trees, evaluations, and game records remain on the device; no move has to be sent to a server. Leaving the page destroys the Worker and releases that game's linear memory and search tables with it.

Local execution does not mean unlimited execution. A phone may expose one or two usable cores, a desktop may support SIMD, shared memory, and WebGPU, and a background tab may be throttled at any time. Automatic thread selection reserves at least one logical processor for the interface and at least a quarter of processors on machines with five or more, normally using no more than six search threads. Changing the count stops search and rebuilds the pool instead of resizing a live engine. KataGo also selects model and batch behavior from measured inference latency rather than applying WebGPU assumptions to a CPU.

That is why the fallback chain matters. A failed multi-Worker pool can fall back to one Worker. A failed Wasm backend can retain the Worker and switch to TypeScript. A tightly bounded main-thread emergency remains at the bottom. The player should see a game that continues, not a message saying the device is too old. Keeping AI local is a privacy decision, but it also forces serious engineering around cancellation, memory, modest devices, and recovery.

The engine can be downloaded from a server without the game ever returning to one. The compute belongs to the device in front of the player, and the game record belongs to the person playing it.

What remained was an order of judgment

Looking back, the most reusable result is not a pruning formula but an order of work: make the rules and complete turn correct; measure the real hot path; preserve a reproducible TypeScript baseline before moving anything to Wasm; prove legal-move, fixed-depth, and statistical parity; then ask what extra complete work fits in the same product budget; only after that use color-swapped self-play to discuss strength. Reversing that order lets speed hide rule errors and impressive node counts hide a tree that never completed fairly.

Board games have already taken us through perfect information, chance nodes, multiplayer utility, extremely wide compound turns, concealed identities, and neural models. Card games tighten every one of those constraints: hands are hidden, deals introduce chance, bidding leaks information, and partners have their own knowledge and objectives. That deserves its own engineering story.

Next: card-game AI—hidden hands, determinization, information-set search, bidding, and partnership play.

JOURNAL

Keep reading

All notes