The opponent the hall seats is a housecarl, from the Old Norse húskarl

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 17:19:06 -04:00
co-authored by Claude Fable 5.1
parent 8da83e64b0
commit c553902d8a
8 changed files with 18 additions and 18 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
</svg> </svg>
<div> <div>
<h1>Hnefatafl</h1> <h1>Hnefatafl</h1>
<p><b>The Viking board game.</b> One king, one throne, four corners, and a ring of attackers closing in. Play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut.</p> <p><b>The Viking board game.</b> One king, one throne, four corners, and a ring of attackers closing in. Play a housecarl or a friend, by Copenhagen rules or Linnaeus's Tablut.</p>
<p class="foot">tafl.kestrelsnest.social · free, no accounts</p> <p class="foot">tafl.kestrelsnest.social · free, no accounts</p>
</div></div> </div></div>
<script> <script>
+3 -3
View File
@@ -325,7 +325,7 @@ export class Rooms<State, Input> {
/** The host seats a bot in an empty chair. */ /** The host seats a bot in an empty chair. */
addBot(room: Room<State>, token: string | undefined): Seat { addBot(room: Room<State>, token: string | undefined): Seat {
const host = this.seatOf(room, token); const host = this.seatOf(room, token);
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may seat a bot.', 403); if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may seat a housecarl.', 403);
if (room.state) throw new RoomError('The game has begun; no more seats are taken.', 409); if (room.state) throw new RoomError('The game has begun; no more seats are taken.', 409);
const taken = new Set(room.seats.map((s) => s.name.toLowerCase())); const taken = new Set(room.seats.map((s) => s.name.toLowerCase()));
const pool = this.game.botNames.filter((n) => !taken.has(n.toLowerCase())); const pool = this.game.botNames.filter((n) => !taken.has(n.toLowerCase()));
@@ -352,10 +352,10 @@ export class Rooms<State, Input> {
/** The host sends a bot away before the game begins. People leave by not coming back. */ /** The host sends a bot away before the game begins. People leave by not coming back. */
unseat(room: Room<State>, token: string | undefined, seatId: unknown): void { unseat(room: Room<State>, token: string | undefined, seatId: unknown): void {
const host = this.seatOf(room, token); const host = this.seatOf(room, token);
if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may send a bot away.', 403); if (host.id !== room.seats[0]?.id) throw new RoomError('Only the player who opened the table may send a housecarl away.', 403);
if (room.state) throw new RoomError('The game has begun; the table is set.', 409); if (room.state) throw new RoomError('The game has begun; the table is set.', 409);
const seat = room.seats.find((s) => s.id === seatId); const seat = room.seats.find((s) => s.id === seatId);
if (!seat || !seat.bot) throw new RoomError('Only a bot can be sent away.', 400); if (!seat || !seat.bot) throw new RoomError('Only a housecarl can be sent away.', 400);
this.commit(room, { t: 'unseat', id: seat.id }); this.commit(room, { t: 'unseat', id: seat.id });
} }
+3 -3
View File
@@ -6,21 +6,21 @@
<meta name="color-scheme" content="dark" /> <meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#121a20" /> <meta name="theme-color" content="#121a20" />
<title>Hnefatafl</title> <title>Hnefatafl</title>
<meta name="description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." /> <meta name="description" content="Hnefatafl, the Viking board game, free in the browser: play a housecarl or a friend, by Copenhagen rules or Linnaeus's Tablut." />
<link rel="canonical" href="https://tafl.kestrelsnest.social/" /> <link rel="canonical" href="https://tafl.kestrelsnest.social/" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:site_name" content="Hnefatafl" /> <meta property="og:site_name" content="Hnefatafl" />
<meta property="og:title" content="Hnefatafl" /> <meta property="og:title" content="Hnefatafl" />
<meta property="og:description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." /> <meta property="og:description" content="Hnefatafl, the Viking board game, free in the browser: play a housecarl or a friend, by Copenhagen rules or Linnaeus's Tablut." />
<meta property="og:url" content="https://tafl.kestrelsnest.social/" /> <meta property="og:url" content="https://tafl.kestrelsnest.social/" />
<meta property="og:image" content="https://tafl.kestrelsnest.social/og.png" /> <meta property="og:image" content="https://tafl.kestrelsnest.social/og.png" />
<meta property="og:image:width" content="1200" /> <meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" /> <meta property="og:image:height" content="630" />
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Hnefatafl" /> <meta name="twitter:title" content="Hnefatafl" />
<meta name="twitter:description" content="Hnefatafl, the Viking board game, free in the browser: play the bot or a friend, by Copenhagen rules or Linnaeus's Tablut." /> <meta name="twitter:description" content="Hnefatafl, the Viking board game, free in the browser: play a housecarl or a friend, by Copenhagen rules or Linnaeus's Tablut." />
<meta name="twitter:image" content="https://tafl.kestrelsnest.social/og.png" /> <meta name="twitter:image" content="https://tafl.kestrelsnest.social/og.png" />
%sveltekit.head% %sveltekit.head%
</head> </head>
+3 -3
View File
@@ -109,7 +109,7 @@
const g = games[held.roomId]; const g = games[held.roomId];
if (g === undefined) return 'looking'; if (g === undefined) return 'looking';
if (g === null) return 'unreachable'; if (g === null) return 'unreachable';
const others = g.seats.filter((s) => s.id !== held.seat).map((s) => (s.bot ? `${s.name} (the bot)` : s.name)); const others = g.seats.filter((s) => s.id !== held.seat).map((s) => (s.bot ? `${s.name} (a housecarl)` : s.name));
if (!g.state) return `waiting to start${g.seats.length ? ` · ${g.seats.map((s) => s.name).join(', ')}` : ''}`; if (!g.state) return `waiting to start${g.seats.length ? ` · ${g.seats.map((s) => s.name).join(', ')}` : ''}`;
if (g.over) { if (g.over) {
if (g.over.winner === held.seat) return 'you won'; if (g.over.winner === held.seat) return 'you won';
@@ -150,7 +150,7 @@
<div class="lid-head"> <div class="lid-head">
<h1>Hnefatafl</h1> <h1>Hnefatafl</h1>
<p class="pitch">The Viking board game. One king, one throne, four corners, and a ring of attackers closing in.</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="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 a housecarl, 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> <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> </div>
@@ -171,7 +171,7 @@
{/each} {/each}
</div> </div>
{/if} {/if}
<button type="button" class="commit primary" disabled={!ready || busy} onclick={() => open(() => Room.createWithBot(name.trim(), options))}>Play now against the bot</button> <button type="button" class="commit primary" disabled={!ready || busy} onclick={() => open(() => Room.createWithBot(name.trim(), options))}>Play now against a housecarl</button>
<div class="ways"> <div class="ways">
<button type="button" class="quiet" disabled={!ready || busy} onclick={() => open(() => Room.create(name.trim(), game.seatIds.length, options))}>Open a table</button> <button type="button" class="quiet" disabled={!ready || busy} onclick={() => open(() => Room.create(name.trim(), game.seatIds.length, options))}>Open a table</button>
<form class="joinform" onsubmit={join}> <form class="joinform" onsubmit={join}>
+4 -4
View File
@@ -61,8 +61,8 @@
{#each v.seats as s (s.id)} {#each v.seats as s (s.id)}
<li> <li>
<span class="seat-name">{s.name}</span> <span class="seat-name">{s.name}</span>
<span class="muted">{s.id === v.host ? 'opened the table' : s.bot ? 'the bot' : 'seated'}{s.id === v.me ? ', you' : ''}{#if s.bot && room.isHost} <span class="muted">{s.id === v.host ? 'opened the table' : s.bot ? 'a housecarl' : 'seated'}{s.id === v.me ? ', you' : ''}{#if s.bot && room.isHost}
<button type="button" class="unseat" title="Send this bot away" aria-label="Send {s.name} away" onclick={() => room.unseat(s.id)}>×</button>{/if}</span> <button type="button" class="unseat" title="Send this housecarl away" aria-label="Send {s.name} away" onclick={() => room.unseat(s.id)}>×</button>{/if}</span>
</li> </li>
{/each} {/each}
{#each v.expected as name (name)} {#each v.expected as name (name)}
@@ -79,7 +79,7 @@
{#if !room.spectating} {#if !room.spectating}
<div class="ways"> <div class="ways">
{#if seatFree && room.isHost} {#if seatFree && room.isHost}
<button type="button" class="quiet" onclick={() => room.addBot()}>Seat a bot</button> <button type="button" class="quiet" onclick={() => room.addBot()}>Seat a housecarl</button>
{/if} {/if}
{#if room.isHost} {#if room.isHost}
<button type="button" class="commit small" disabled={v.seats.length < 2} onclick={() => room.begin()}>Begin</button> <button type="button" class="commit small" disabled={v.seats.length < 2} onclick={() => room.begin()}>Begin</button>
@@ -100,7 +100,7 @@
<p class="muted small">{v.keeper} keeps this site and answers every call, sometimes within the minute, sometimes after a night's sleep.{#if keeperHour}&nbsp;It is {keeperHour} where {v.keeper} lives.{/if} The seat is held either way.</p> <p class="muted small">{v.keeper} keeps this site and answers every call, sometimes within the minute, sometimes after a night's sleep.{#if keeperHour}&nbsp;It is {keeperHour} where {v.keeper} lives.{/if} The seat is held either way.</p>
</div> </div>
{:else if v.challenge} {:else if v.challenge}
<p class="muted small">{v.keeper} has been called to the table by {room.nameOf(v.challenge.by)}.{#if keeperHour}&nbsp;It is {keeperHour} there.{/if} A seat is held: if they can come now they will take it; if they are asleep or away they will leave a word below when they can. Meanwhile the table is yours: seat a bot, invite a friend, or open another game in a new tab and play on. This table keeps its place in your hall until you come back.</p> <p class="muted small">{v.keeper} has been called to the table by {room.nameOf(v.challenge.by)}.{#if keeperHour}&nbsp;It is {keeperHour} there.{/if} A seat is held: if they can come now they will take it; if they are asleep or away they will leave a word below when they can. Meanwhile the table is yours: seat a housecarl, invite a friend, or open another game in a new tab and play on. This table keeps its place in your hall until you come back.</p>
{/if} {/if}
{/if} {/if}
{#if room.error}<p class="warning">{room.error}</p>{/if} {#if room.error}<p class="warning">{room.error}</p>{/if}
+2 -2
View File
@@ -101,12 +101,12 @@ export class Room {
} }
addBot(): Promise<boolean> { addBot(): Promise<boolean> {
return this.act(() => api.addBot(this.roomId, this.token), 'The bot could not be seated.'); return this.act(() => api.addBot(this.roomId, this.token), 'The housecarl could not be seated.');
} }
/** Send a bot away before the game begins; only the host may. */ /** Send a bot away before the game begins; only the host may. */
unseat(seat: SeatId): Promise<boolean> { unseat(seat: SeatId): Promise<boolean> {
return this.act(() => api.unseat(this.roomId, this.token, seat), 'The bot could not be sent away.'); return this.act(() => api.unseat(this.roomId, this.token, seat), 'The housecarl could not be sent away.');
} }
say(text: string): Promise<boolean> { say(text: string): Promise<boolean> {
+2 -2
View File
@@ -21,7 +21,7 @@
<h2>The hall</h2> <h2>The hall</h2>
<p> <p>
Give your name and choose the rules: <em>Copenhagen</em>, the balanced modern game on the big board, or <em>Tablut</em>, the smaller and older one. Choose which side you play. Give your name and choose the rules: <em>Copenhagen</em>, the balanced modern game on the big board, or <em>Tablut</em>, the smaller and older one. Choose which side you play.
<em>Play now against the bot</em> seats a bot opposite you and begins. <em>Open a table</em> gives you a four-letter code and a link for a friend. The tables this browser <em>Play now against a housecarl</em> seats a housecarl opposite you and begins. <em>Open a table</em> gives you a four-letter code and a link for a friend. The tables this browser
holds a seat at are listed below, with whose move it is. holds a seat at are listed below, with whose move it is.
</p> </p>
</section> </section>
@@ -29,7 +29,7 @@
<section id="table"> <section id="table">
<h2>The table</h2> <h2>The table</h2>
<p> <p>
Whoever opened the table is its host, and begins the game once a second player has sat or a bot has been seated. A friend who opens the link takes the other seat. The Whoever opened the table is its host, and begins the game once a second player has sat or a housecarl has been seated. A friend who opens the link takes the other seat. The
masthead's <em>Transfer seat</em> gives four words that bring your seat to another device, and when a game is over anyone at the table may call for a rematch. masthead's <em>Transfer seat</em> gives four words that bring your seat to another device, and when a game is over anyone at the table may call for a rematch.
</p> </p>
</section> </section>
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 112 KiB