Hnefatafl: Copenhagen and Tablut, a board that ages with the older game, and a bot that looks three moves ahead

The rules are data in rules.ts; the engine plays custodial capture, the
king's own capture rule on and beside the throne, corner and edge
escape, shieldwalls, edge forts, encirclement, repetition and stalemate;
twenty tests pin them and play a bot game to a verdict on both boards.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-23 13:06:23 -04:00
co-authored by Claude Fable 5.1
parent ed1dcad259
commit 0d2f54fe02
12 changed files with 1314 additions and 280 deletions
+27 -12
View File
@@ -149,8 +149,8 @@
<section class="lid">
<div class="lid-head">
<h1>Hnefatafl</h1>
<p class="pitch">One line that says what the game is.</p>
<p class="tag">Two or three sentences for someone who has never heard of it: what you do on a turn, what makes it fun, and that it is free to play here against the bot or with friends.</p>
<p class="pitch">The Viking board game. One king, one throne, four corners, and a ring of attackers closing in.</p>
<p class="tag">Hnefatafl was played across the Norse world for a thousand years before chess. Every piece moves like a rook and is taken by being sandwiched; the attackers must capture the king, the king must reach a corner. Play the bot, or open a table for a friend and play by turns. Copenhagen rules on the big board, or Tablut, the older game Linnaeus wrote down in 1732.</p>
<p class="links"><a href="/guide">how to play</a> · <a href="/rules">the rules</a> · <a class="about-link" href="#about" onclick={() => (aboutOpen = true)}>about this game a labor of love</a></p>
</div>
@@ -190,8 +190,21 @@
</div>
<div class="lid-look" aria-hidden="true">
<!-- A still of the game at its most characteristic: a few rounds of the ledger, a board mid-play. -->
<p class="caption">A picture of the game goes here.</p>
<svg class="still" viewBox="0 0 110 110" aria-hidden="true">
<rect x="0" y="0" width="110" height="110" rx="4" fill="#8a5a2b" />
{#each Array.from({ length: 11 }, (_, r) => r) as r (r)}
{#each Array.from({ length: 11 }, (_, c) => c) as c (c)}
<rect x={c * 10} y={r * 10} width="10" height="10" fill={(r === 0 || r === 10) && (c === 0 || c === 10) ? 'rgba(224,101,92,0.4)' : r === 5 && c === 5 ? 'rgba(240,165,58,0.4)' : 'rgba(255,240,210,0.08)'} stroke="rgba(30,16,6,0.5)" stroke-width="0.4" />
{/each}
{/each}
{#each [[3,0],[4,0],[5,0],[6,0],[7,0],[5,1],[0,3],[10,3],[0,4],[10,4],[0,5],[1,5],[9,5],[10,5],[0,6],[10,6],[0,7],[10,7],[5,9],[3,10],[4,10],[5,10],[6,10],[7,10]] as [x, y] (`${x},${y}`)}
<circle cx={x * 10 + 5} cy={y * 10 + 5} r="3.4" fill="#2a2320" stroke="#0d0a08" stroke-width="0.6" />
{/each}
{#each [[5,3],[4,4],[5,4],[6,4],[3,5],[4,5],[6,5],[7,5],[4,6],[5,6],[6,6],[5,7]] as [x, y] (`${x},${y}`)}
<circle cx={x * 10 + 5} cy={y * 10 + 5} r="3.4" fill="#ece4d0" stroke="#b9ab8b" stroke-width="0.6" />
{/each}
<circle cx="55" cy="55" r="3.6" fill="#f0d58a" stroke="#a67c1a" stroke-width="0.6" />
</svg>
</div>
{#if seats.length}
@@ -243,8 +256,8 @@
<details class="about" id="about" bind:open={aboutOpen}>
<summary>about this game</summary>
<div class="about-body">
<p>Where the game comes from, who made the original and when, and how the keeper of this page first met it.</p>
<p>What this version keeps and what it changes. Where the rules text it follows lives (the <a href="/rules">rules page</a>), and any borrowed art or text with its notice.</p>
<p>Hnefatafl is the tafl game of the Vikings, carried by them from Scandinavia to Britain, Ireland, Iceland and beyond, and played until chess arrived in the eleventh and twelfth centuries. No rules from that age survive; the game was reconstructed in modern times from a Sámi cousin, Tablut, whose rules the botanist Carl Linnaeus jotted down on his journey through Lapland in 1732.</p>
<p>This table plays Copenhagen, the modern tournament ruleset, on the eleven-by-eleven board, and Tablut on the nine-by-nine one, with the older board showing its age. The rules it follows are on the <a href="/rules">rules page</a>. The game is in the public domain; the board and pieces here are drawn, not borrowed.</p>
<p>It is free and keeps no accounts. A game between people lives on a small server as an append-only ledger of moves, so it can be replayed from its first move, and each player is shown only what the rules let them see.</p>
<h3>Send word</h3>
<p>A rule read wrong, a bug, a game you would like to tell of: write to <a href="mailto:eric@ericwagoner.com">eric@ericwagoner.com</a>, <a href="https://bsky.app/profile/kestrelsnest.social" target="_blank" rel="noreferrer">@kestrelsnest.social</a> on Bluesky, or <a href="https://toots.kestrelsnest.social/@eric" target="_blank" rel="noreferrer">@eric@toots.kestrelsnest.social</a> on Mastodon. The keeper of this hall roosts at <a href="https://kestrelsnest.social" target="_blank" rel="noreferrer">kestrelsnest.social</a>.</p>
@@ -256,7 +269,7 @@
{/each}
</ul>
{/if}
<p class="made">Hnefatafl is its designer's. This page was made by Eric and a very enthusiastic AI, 2026.</p>
<p class="made">Hnefatafl belongs to everyone; it is a thousand years old. This page was made by Eric and a very enthusiastic AI, 2026.</p>
</div>
</details>
</section>
@@ -389,11 +402,6 @@
border-radius: 6px;
}
.caption {
font-size: 0.85rem;
color: var(--bone-dim);
text-align: center;
}
/* the games ledger */
.ledger {
@@ -578,4 +586,11 @@
gap: 0.5rem;
font-size: 0.9rem;
}
.still {
width: 100%;
max-width: 18rem;
display: block;
margin: 0 auto;
border-radius: 4px;
}
</style>