A slick, fully playable browser version of Brandubh — the Irish 7×7 form of hnefatafl, the asymmetric Norse–Gaelic “king’s table” war game. Play the King’s desperate escape or the raiders’ hunt, against a friend (over the board) or a built-in AI, with two historical rule variants and a custom rule editor.
- ⚔️ Correct, tested tafl engine (custodial capture, hostile corners & throne, strong-king throne capture)
- 🤖 Iterative-deepening alpha–beta AI (transposition table + quiescence search) that runs in a Web Worker, so hard-level thinking never freezes the board — across four difficulty levels, up to Ollamh and its opening book
- 👑 Two rule variants: World Tafl Federation and Walker — plus a custom rule editor
- 🌐 Localised in English and Spanish
- ⏱️ Optional Lichess-style chess clock (off by default — no timer) — a time bank plus per-move Fischer increment (3+2 when enabled), bullet → rapid presets and a custom control, with flag-on-time
- 🧘 Zen mode — a calm, over-the-board layout showing only the board, whose turn it is, the clock and the move log; game controls are contextual (a minimal "Next game" / "Next set" prompt appears only when a game ends), and every other panel (scoreboard, captured tray, move nav, rules, takeback, resign, pause, settings) is an opt-in extra you can reveal from settings
- 📱 Mobile-first, no backend, works offline — pure static SPA
- 🎨 Carved-wood board, crown / shield / axe piece emblems, move log, undo
npm install
npm run dev # http://localhost:5173
npm run build # static bundle in ./dist (deploy to Vercel / Netlify / GH Pages)
npm run preview # preview the production build
npx tsx scripts/selftest.ts # headless engine sanity checksThe build is a self-contained static site (base: "./"), so dist/ can be
dropped onto any static host.
Brandubh (“black raven”, also spelt brandub / brannumh) is the Irish variant of hnefatafl, played on a 7×7 board with just thirteen pieces. Like most hnefatafl games its medieval rules were never written down — they survive only in poetry, legend, and a scatter of archaeological board finds — so what follows is the widely-used modern reconstruction (the version played competitively on Aage Nielsen’s hnefatafl site and by the World Tafl Federation).
a b c d e f g
7 ◉ . . A . . ◉ A = attacker (raider) × 8
6 . . . A . . . D = defender (warrior) × 4
5 . . . D . . . K = king × 1
4 A A D K D A A ◉ = corner (restricted + hostile)
3 . . . D . . . ⊕ = throne (centre, restricted)
2 . . . A . . .
1 ◉ . . A . . ◉
- The King’s side — the King on the central throne, ringed by 4 defenders. Outnumbered two-to-one.
- The attackers — 8 raiders massed at the middle of each edge, forming a cross with the defenders.
- The attackers move first. Turns then alternate.
- Every piece moves like a rook in chess: any number of empty squares orthogonally (up, down, left, right). No diagonals, no jumping.
- Only the King may stop on the throne or a corner. Ordinary soldiers may pass over the empty throne but may never rest on it, and may never enter a corner.
- In both reconstructions the King may return to the throne after leaving it.
- You capture an enemy soldier by flanking it: move a piece so the enemy is trapped between two of your own pieces along a rank or file. The trapped piece is removed immediately.
- You must move into the trap. A piece that moves between two enemies of its own accord is safe — captures only happen on the capturing player’s move.
- The four corners and the empty throne are hostile squares: they count as “your piece” for the purpose of completing a capture, for both sides.
- A single move can capture several pieces at once (one in each direction).
| Side | Wins by |
|---|---|
| 👑 King’s side (defenders) | Getting the King to any corner. |
| ⚔️ Attackers (raiders) | Capturing the King. |
- The King is captured by being surrounded on two opposite sides in the open (a raider, a hostile corner, or the board edge does not help here — the king needs actual flanking).
- Strong king by the throne: when the King stands on the throne or on a square next to it, the attackers must surround him on all four sides (the empty throne counts as one hostile side).
- A player who has no legal move loses.
- A position repeated three times ends the game (draw in Walker rules, loss for defenders in WTF rules).
Some tournament rule-sets add a shieldwall capture (bracketing a whole row of pieces against the edge) and an exit-fort win for the King. This implementation keeps to the core Brandubh rules above; those extensions are noted here for completeness and are candidate future options.
Brandubh has been reconstructed more than one way. The app ships two rule-sets sourced from aagenielsen.dk, selectable in the settings, plus a custom rule editor for mixing and matching flags:
-
Brandubh · World Tafl Federation (default) — official WTF tournament rules. The empty throne is hostile to soldiers but never to the king. The king on the throne requires all four sides surrounded. Encirclement wins. Threefold repetition is a loss for the defending side.
-
Brandubh · Walker — Damian Walker’s reconstruction (Cyningstan, 2011), based on MacWhite’s 1946 article. The throne is not hostile. The king is captured by two pieces anywhere on the board (no strong-king rule). Threefold repetition is a draw.
Both variants use an armed king (the king can participate in captures). They
share the same board, setup, movement, and corner rules; they differ in throne
hostility, strong-king behaviour, encirclement, and repetition handling. All
flags are wired through a declarative RuleSet in src/game/variants.ts, so
adding further Brandubh variants is a matter of flipping flags — or use the in-app
custom rule editor to experiment live.
Under the drawer's collapsed More games section. Tablut is 9×9, White moves
first, and the king wins by reaching any edge square — so it is a boardgame
rather than a Brandubh variant, and it has its own rules, engine, save file and
.tafl interchange format under src/game/tablut/. The reasoning for forking
rather than parameterising is
docs/adr/0006-tablut-forks-the-rules-rather-than-parameterising-them.md; the
presets, their sources and what is not verified about them are in
docs/tablut-rules.md.
Four presets ship — the undisputed baseline, the July 2025 gulo/Dimetr proposal
(the throne cannot be crossed by Black; the throne is friendly to White), an
⚠ unverified tournament reading, and a corner-escape reconstruction — plus a
custom rule editor covering every flag. Hiding a preset is one line in
VISIBLE_VARIANTS.
Playable against the engine or over the board. The shell features (clock,
analysis, review, match sets, import/export, puzzles, tutorials) are Brandubh's
for now and are waiting on App becoming generic in its ruleset — see the ADR
addendum. npm run check:tablut is the driven-browser check that the 9×9 board,
its coordinates, its worker and Brandubh's save all survive each other.
Also under More games. Copenhagen is 11×11 — twenty-four attackers against a king and twelve defenders — and is the modern tournament standard, played at the Hnefatafl World Championship. Unlike the other two it is not a reconstruction of a lost game but a deliberately drafted ruleset with an author, which changes what there is to be uncertain about: not what Copenhagen says, but whether this implementation copied it correctly, since every site publishing it is blocked by the same egress proxy described below.
Black moves first, the king escapes to a corner and takes four attackers to capture (three plus the empty throne beside it), and two rules are Copenhagen's own:
- the shieldwall — a bracketed row of men along the board edge, every one of them faced by an enemy, falls together; a corner may stand in for one bracket, and a king in the row survives while his soldiers do not;
- the exit fort — the defenders win with an unbreakable wall that gives the king contact with the edge and a move to make. The detector is deliberately one-sided: it declares the win only when the fort is provably unbreakable, so a fort it fails to recognise is merely played on, while a fort it wrongly recognised would end a game nobody had won.
Perpetual repetition loses for whoever is repeating, which is why this is the only board in the app that can end in a defender win by repetition.
Two presets ship — Copenhagen itself and an ⚠ unverified Fetlar reading (the
older championship rules it extends, without the three additions) — plus the
custom rule editor. docs/copenhagen-rules.md records the sourcing assertion by
assertion, and names the one rule where two sources flatly contradict each
other: whether a strong king can be captured against the board edge. Both
readings are reachable; the shipped default follows the two independently-worded
excerpts that agree.
docs/adr/0007-copenhagen-forks-a-third-time-and-defers-the-shared-core.md
records why this is a third fork rather than the shared core ADR-0006 predicted a
third game would justify — and what that defers.
npm run check:copenhagen is its driven-browser check.
Also under More games, and the first game here that is not tafl at all. The board is a graph of 24 points on three concentric squares joined by four spokes; each player has nine stones, places them one per turn, then moves them along the lines. Three in a line is a mill, and closing one takes an enemy stone off the board. A player down to three stones may fly — move to any empty point — and a player reduced below three, or with no legal move, loses. There is no king, no throne and no capture by surrounding, so none of the tafl vocabulary applies; it shares the app's furniture and nothing of its rules.
Nine Men's Morris is a draw with perfect play — Ralph Gasser solved it in
1996, with endgame databases of about 10¹⁰ positions computed by retrograde
analysis and an 18-ply search over the placing phase on top of them. That result
is cited here, not reproduced: the paper's full text is blocked by the same
egress proxy described below, so everything this app says about it came through
search excerpts and is marked ⚠ UNVERIFIED in docs/morris-rules.md, which also
carries the checklist for re-verifying it against the real text.
Ollamh ships small endgame databases generated by Gasser's method
(scripts/morris-solve.ts, with a verifier pass over every entry), and its claim
is deliberately two-part:
- once play reaches a shipped table, it is perfect — the value and the distance to it are looked up, not searched, which is what makes a won endgame actually get won instead of cycled;
- before that it is a deep alpha-beta search and nothing more. The shipped
tables cover small stone counts; the full set is ≈ 9.2 GB at one byte an entry,
so the opening and the whole placing phase are search, not proof. The word
proven is held to the same bar
docs/solving.mdset for Brandubh's opening book.
One preset ships, morris-gasser-1: Gasser's readings on the two points
published rules actually disagree about — closing two mills at once still takes
one stone, and when every enemy stone is in a mill then any of them may be
taken — plus two practical draw rules that are not in the paper and are
labelled as an owner decision: threefold repetition, and fifty moves by each side
without a mill being closed. Both are one flag away from "none" in the custom
rule editor, and "none" on both is the game Gasser actually solved.
Games save, export and import as their own .morris files (morris-1), not as
.tafl — a Morris move is not a tafl move and the extension should not claim
otherwise. The Ballinderry theme is kept on this board rather than falling
back as it does on the 9×9 and 11×11, because a peg board with a hole at each of
the 24 points is a truthful Morris board; it is not a claim that the Ballinderry
object itself is one. See
docs/adr/0008-nine-mens-morris-is-a-fourth-board-and-not-a-tafl-game.md for why
this is a fourth fork in spite of ADR-0007's advice, and what that cost.
npm run check:morris is its driven-browser check.
The original brief was to also scrape every recorded game of the two Brandubh
variants from Aage Nielsen’s archive (aagenielsen.dk/visallespil.php). That
site is blocked by this environment’s outbound network policy (the egress
proxy denies the host), so the game database could not be crawled here. The
rules above were reconstructed from the site’s public rule descriptions and the
World Tafl Federation ruleset via search.
If you want the recorded games imported (e.g. as replayable PGN-style game
records or an opening book for the AI), run the scrape from a machine with
network access to that domain and drop the parsed games into src/game/ — the
move-notation format (moveName() in rules.ts, e.g. d2-d4) is already
compatible with a simple game-record replay.
src/
game/
types.ts core types (board, move, state)
variants.ts rule presets (Walker, WTF) + RuleSet flags
rules.ts move generation, captures, king capture, win detection, notation
rules.test.ts vitest unit tests for the rules
matchSet.ts over-the-board set scoring (side swap, tiebreak by moves)
matchSet.test.ts vitest unit tests for set scoring
engine.ts iterative-deepening alpha–beta (TT, quiescence, ordering) + evaluation
engine.test.ts vitest tactics, quiescence, self-play & perf tests for the engine
ai.worker.ts runs the search off the main thread (bundled, offline)
useAiWorker.ts React hook: worker lifecycle, cancellation, sync fallback
components/
Board.tsx the board grid + piece emblems
RulesModal.tsx in-app how-to-play
i18n.ts translations (EN, ES)
App.tsx game state, controls, AI orchestration, custom rule editor
scripts/
selftest.ts headless engine assertions
aibench.ts AI depth-vs-time benchmark + new-vs-legacy self-play
The board ships with twelve colour themes, selectable in the settings and
remembered between visits, with Everforest as the first-visit default. Seven
are Omarchy-inspired (omarchy.org): Everforest,
Tokyo Night, Catppuccin, Gruvbox, Nord, Rosé Pine and
Kanagawa, alongside the original Carved Wood and four classic
chess-board palettes after Lichess (Brown, Blue, Green and Purple).
Everything is driven by CSS custom properties under a [data-theme] attribute, so
adding another theme is just one more block in src/index.css plus an entry in
src/theme.ts.
- Curved arrows under the board cycle back and forth through every move without discarding anything; Play from here branches the game at the position you are viewing (against the same opponent, or against the computer).
- Over-the-board play offers Propose takeback; either side may Resign.
- When a game ends you can step back and Play from here to explore variations.
Brandubh is asymmetric, so a single game never pits two people fairly against each other — whichever army is stronger has the edge. Over-the-board play is therefore scored as a set: a group of games in which the players swap sides, so each one sits behind both the king and the raiders an equal number of times. A scoreboard above the board tracks it live:
- Editable player names — type over “Player 1 / Player 2”; the names carry through every game, set, and the whole match.
- Match tally — sets won by each player across the running series.
- King’s side vs Raiders counters — games each army has won this set.
- Per-player standings — which side each player holds this game, their game wins, and their fastest victory (in moves).
- Each finished game — winner, the side they held, and the moves it took.
Because the stronger side is expected to win, a set usually finishes level. When it does, the move-count tiebreaker decides it: the player who won in fewer moves (totalled across their wins) takes the set. Next game swaps the sides and plays on; Next set banks the result and starts a fresh set, alternating who leads and continuing the match count; New match wipes the score. Set length (2, 4 or 6 games) is chosen in the settings.
The app is a static SPA, so any static host works. This repo ships a
vercel.json so a connected Vercel project
deploys automatically on every push — production from the default branch, and a
preview URL for every other branch.
One-time setup: import the repository at vercel.com → Add New → Project.
Vercel reads vercel.json (framework vite, build npm run build, output
dist/); no further configuration is needed. To deploy anywhere else, run
npm run build and serve the dist/ folder.
Piece and corner emblems are vector traces of supplied artwork of traditional
public-domain Celtic / Norse symbols — see NOTICE. A gallery of the
full set lives at docs/design/icons.html.
Source-available — all rights reserved, © Eoin Malone. Read it, clone it, build it; reuse needs permission. See LICENSE. Third-party assets keep their own terms — see NOTICE.
