The Tablut board is cut in stone: mottled, chipped at a few corners, and cracked twice, as a slab from a long hall would be
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
dcfbd38ae6
commit
c5d487edbf
@@ -48,6 +48,34 @@
|
||||
};
|
||||
const pieceWord = (p: string) => (p === 'k' ? 'the king' : p === 'a' ? 'an attacker' : 'a defender');
|
||||
|
||||
// --- Age: the Tablut board came out of a long hall. A few chipped corners
|
||||
// and two hairline cracks, placed by a fixed hand so the board is the same
|
||||
// board every visit; nothing here moves or changes with play.
|
||||
const chips = $derived.by(() => {
|
||||
if (!worn) return [] as { x: number; y: number; r: number; d: string }[];
|
||||
const spots = [
|
||||
[1, 0, 3], [size - 1, 2, 0], [3, size - 1, 1], [size - 2, size - 2, 2], [0, size - 3, 3], [size - 4, 0, 1], [2, 4, 0]
|
||||
] as const;
|
||||
return spots.map(([cx, cy, corner]) => {
|
||||
const x = PAD_L + cx * CELL + (corner === 1 || corner === 2 ? CELL : 0);
|
||||
const y = PAD_T + cy * CELL + (corner >= 2 ? CELL : 0);
|
||||
const sx = corner === 1 || corner === 2 ? -1 : 1;
|
||||
const sy = corner >= 2 ? -1 : 1;
|
||||
const a = 5 + ((cx * 7 + cy * 3) % 4);
|
||||
const b = 4 + ((cx * 5 + cy) % 3);
|
||||
return { x, y, r: 0, d: `M${x} ${y} l${sx * a} 0 l${-sx * (a - b)} ${sy * b} l${-sx * b} ${sy * (a - b + 1)} z` };
|
||||
});
|
||||
});
|
||||
const cracks = $derived.by(() => {
|
||||
if (!worn) return [] as string[];
|
||||
const w = PAD_L + size * CELL;
|
||||
const h = PAD_T + size * CELL;
|
||||
return [
|
||||
`M${PAD_L + CELL * 0.6} ${PAD_T + 2} c 6 14, -4 22, 8 36 s 2 18, 14 26 s 4 10, 2 22`,
|
||||
`M${w - 3} ${h - CELL * 2.4} c -12 4, -18 -6, -30 2 s -14 12, -26 8 s -12 -4, -18 6`
|
||||
];
|
||||
});
|
||||
|
||||
// --- Tokens: pieces with a life of their own, so a move slides and a capture fades ---
|
||||
interface Token {
|
||||
id: number;
|
||||
@@ -245,15 +273,23 @@
|
||||
<filter id="grain" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="2" seed="7" result="noise" />
|
||||
<feColorMatrix in="noise" type="saturate" values="0" result="grey" />
|
||||
<feComponentTransfer in="grey" result="soft"><feFuncA type="linear" slope={worn ? 0.42 : 0.14} /></feComponentTransfer>
|
||||
<feComponentTransfer in="grey" result="soft"><feFuncA type="linear" slope={worn ? 0.5 : 0.14} /></feComponentTransfer>
|
||||
<feBlend in="SourceGraphic" in2="soft" mode="multiply" />
|
||||
</filter>
|
||||
<filter id="worn" x="-5%" y="-5%" width="110%" height="110%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.07" numOctaves="2" seed="3" result="warp" />
|
||||
<feDisplacementMap in="SourceGraphic" in2="warp" scale={worn ? 2.4 : 0} xChannelSelector="R" yChannelSelector="G" />
|
||||
<feDisplacementMap in="SourceGraphic" in2="warp" scale={worn ? 2.8 : 0} xChannelSelector="R" yChannelSelector="G" />
|
||||
</filter>
|
||||
<filter id="mottle" x="0" y="0" width="100%" height="100%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.018" numOctaves="3" seed="11" result="blotch" />
|
||||
<feColorMatrix in="blotch" type="matrix" values="0 0 0 0 0.16 0 0 0 0 0.14 0 0 0 0 0.11 0 0 0 0.55 0" result="stain" />
|
||||
<feBlend in="SourceGraphic" in2="stain" mode="multiply" />
|
||||
</filter>
|
||||
</defs>
|
||||
<rect class="wood" x="0" y="0" {width} {height} rx="6" filter="url(#grain)" />
|
||||
{#if worn}
|
||||
<rect class="stain" x="0" y="0" {width} {height} rx="6" filter="url(#mottle)" />
|
||||
{/if}
|
||||
{#each showCoords ? Array.from({ length: size }, (_, n) => n) : [] as n (n)}
|
||||
<text class="coord" x={PAD_L + n * CELL + CELL / 2} y={height - 4} text-anchor="middle">{String.fromCharCode(97 + n)}</text>
|
||||
<text class="coord" x={PAD_L - 4} y={PAD_T + n * CELL + CELL / 2 + 3} text-anchor="end">{size - n}</text>
|
||||
@@ -287,6 +323,12 @@
|
||||
<circle class="hint" cx={x + CELL / 2} cy={y + CELL / 2} r={CELL * 0.14} pointer-events="none" />
|
||||
{/if}
|
||||
{/each}
|
||||
{#each chips as chip (chip.d)}
|
||||
<path class="chip" d={chip.d} />
|
||||
{/each}
|
||||
{#each cracks as crack (crack)}
|
||||
<path class="crack" d={crack} />
|
||||
{/each}
|
||||
{#each tokens as t (t.id)}
|
||||
{@const c = cellOf(size, t.at)}
|
||||
{@const cx = PAD_L + c.x * CELL + CELL / 2}
|
||||
@@ -375,10 +417,10 @@
|
||||
--crown: #6b4a0f;
|
||||
}
|
||||
|
||||
/* Tablut: the older game, on a board that has been in a barn. */
|
||||
/* Tablut: the older game, cut in stone and kept in a long hall for a thousand years. */
|
||||
.board[data-set='tablut'] {
|
||||
--wood: #8d8577;
|
||||
--wood-line: rgba(40, 34, 28, 0.4);
|
||||
--wood: #8a8578;
|
||||
--wood-line: rgba(36, 32, 27, 0.45);
|
||||
--coord: rgba(30, 26, 20, 0.5);
|
||||
--square: rgba(255, 250, 235, 0.05);
|
||||
--square-edge: rgba(255, 250, 235, 0.09);
|
||||
@@ -472,6 +514,26 @@
|
||||
transition: fill 1.2s ease;
|
||||
}
|
||||
|
||||
.stain {
|
||||
fill: rgba(120, 110, 96, 0.22);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.chip {
|
||||
fill: rgba(28, 24, 20, 0.42);
|
||||
stroke: rgba(230, 222, 205, 0.18);
|
||||
stroke-width: 0.6;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.crack {
|
||||
fill: none;
|
||||
stroke: rgba(24, 20, 16, 0.55);
|
||||
stroke-width: 0.9;
|
||||
stroke-linecap: round;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.coord {
|
||||
fill: var(--coord);
|
||||
font-family: var(--font);
|
||||
|
||||
Reference in New Issue
Block a user