diff --git a/docs/artwork.md b/docs/artwork.md new file mode 100644 index 0000000..a7856e2 --- /dev/null +++ b/docs/artwork.md @@ -0,0 +1,19 @@ +# Board artwork + +`src/lib/components/BoardArtwork.svelte` owns both palettes, surface filters, +wear, grid, inlays and counters. `Board.svelte` supplies interactive state; +`Hall.svelte` supplies an opening position from the game engine. Update the +shared component to change either illustration and the game together. + +After editing the artwork, run: + +```sh +npm run art +``` + +This renders the same Svelte component into `static/board-copenhagen.svg` and +`static/board-tablut.svg`, then regenerates `docs/og-card.svg` and the 1200 × 630 +`static/og.png`. Preview the share composition in `docs/og-card.html`. +The share composition and copy live in `docs/render-art.mjs`. Commit generated +assets with source changes. The export uses Sharp as a development dependency; +it does not add any image-processing code to the shipped app. diff --git a/docs/og-card.html b/docs/og-card.html index b8bfcd2..2698be6 100644 --- a/docs/og-card.html +++ b/docs/og-card.html @@ -1,33 +1,13 @@ -
- - - - -
-

Hnefatafl

-

The Viking board game. 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.

-

tafl.kestrelsnest.social · free, no accounts

-
- + + + + + Hnefatafl share card + + + + + Hnefatafl. The Viking board game. Play a housecarl or a friend, Copenhagen or Linnaeus’s Tablut. Free, no accounts. tafl.kestrelsnest.social + + diff --git a/docs/og-card.svg b/docs/og-card.svg new file mode 100644 index 0000000..349cbda --- /dev/null +++ b/docs/og-card.svg @@ -0,0 +1,16 @@ + + + + + Hnefatafl + The Viking board game. + + One king, one throne, four corners, + and a ring of attackers closing in. + Play a housecarl or a friend. + Copenhagen or Linnaeus’s Tablut. + + tafl.kestrelsnest.social + Free · no accounts + + \ No newline at end of file diff --git a/docs/render-art.mjs b/docs/render-art.mjs new file mode 100644 index 0000000..deaf2bd --- /dev/null +++ b/docs/render-art.mjs @@ -0,0 +1,56 @@ +// Run with npm run art. Both illustrations originate in the live board component. +import { readFile, writeFile, unlink } from 'node:fs/promises'; +import { compile } from 'svelte/compiler'; +import { render } from 'svelte/server'; +import sharp from 'sharp'; +import { createGame } from '../src/lib/game/index.ts'; +const root = new URL('../', import.meta.url); +const source = await readFile(new URL('src/lib/components/BoardArtwork.svelte', root), 'utf8'); +const compiled = compile(source.replace("'$lib/game'", "'../src/lib/game/index.ts'"), { generate: 'server', css: 'injected', filename: 'BoardArtwork.svelte' }); +const temporary = new URL(`.board-art-${process.pid}.mjs`, import.meta.url); +try { + await writeFile(temporary, compiled.js.code); + const { default: BoardArtwork } = await import(temporary.href); + for (const set of ['copenhagen', 'tablut']) { + const result = render(BoardArtwork, { props: { g: createGame({ a: 'Defenders', b: 'Attackers' }, 0, undefined, { set }), decorative: true } }); + let svg = result.body.replace(//g, ''); + const css = [...result.head.matchAll(/]*>([\s\S]*?)<\/style>/g)].map(m => m[1]).join('\n'); + svg = svg.replace(/(]*>)/, '$1'); + // Resolve custom properties for standalone SVG rasterizers, which do not + // consistently implement CSS variable inheritance. Browsers use the component. + const paletteSource = source.slice(source.indexOf('/* Copenhagen:')); + const base = paletteSource.slice(0, paletteSource.indexOf('/* Tablut:')); + const overrides = set === 'tablut' ? paletteSource.slice(paletteSource.indexOf('/* Tablut:'), paletteSource.indexOf('\n\t.artwork {')) : ''; + const values = Object.fromEntries([...`${base}\n${overrides}`.matchAll(/(--[\w-]+):\s*([^;]+);/g)].map(m => [m[1], m[2]])); + values['--piece-rim'] = values['--defender-rim']; + values['--font'] = 'Georgia, serif'; + values['--frost'] = '#9ccbdd'; + svg = svg.replace(/:where\((\.[\w-]+)\)/g, '$1'); + svg = svg.replace(/var\((--[\w-]+)\)/g, (_, key) => values[key] ?? 'inherit'); + svg = svg.replace('', ['attacker', 'defender', 'king'].map(kind => `.piece.${kind} .base{fill:${values[`--${kind}-rim`]}}.piece.${kind} .body,.piece.${kind} .turned{stroke:${values[`--${kind}-rim`]}}`).join('') + ''); + await writeFile(new URL(`static/board-${set}.svg`, root), svg); + } + const board = await readFile(new URL('static/board-copenhagen.svg', root), 'utf8'); + const image = Buffer.from(board).toString('base64'); + const share = ` + + + + Hnefatafl + The Viking board game. + + One king, one throne, four corners, + and a ring of attackers closing in. + Play a housecarl or a friend. + Copenhagen or Linnaeus’s Tablut. + + tafl.kestrelsnest.social + Free · no accounts + + `; + await writeFile(new URL('docs/og-card.svg', root), share); + await sharp(Buffer.from(share)).png().toFile(new URL('static/og.png', root).pathname); + console.log('Generated both board illustrations and static/og.png (1200 × 630).'); +} finally { + await unlink(temporary).catch(() => {}); +} diff --git a/package-lock.json b/package-lock.json index 957b9ec..c305360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/node": "^26.6.2", "@types/ws": "^8.18.1", + "sharp": "^0.35.4", "svelte": "^5.56.1", "svelte-check": "^4.6.0", "tsx": "^4.23.15", @@ -24,6 +25,17 @@ "vitest": "^5.0.1" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", @@ -466,6 +478,533 @@ "node": ">=18" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -828,6 +1367,7 @@ "integrity": "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", @@ -880,6 +1420,7 @@ "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^1.0.0", @@ -1010,6 +1551,7 @@ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1708,6 +2250,19 @@ "node": ">=6" } }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", @@ -1715,6 +2270,56 @@ "dev": true, "license": "MIT" }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/siginfo": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", @@ -1767,6 +2372,7 @@ "integrity": "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.6.0", @@ -1860,12 +2466,21 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.23.15", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.28.0" }, @@ -1885,6 +2500,7 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1906,6 +2522,7 @@ "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", diff --git a/package.json b/package.json index 9c102fe..ccffe8c 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "test:watch": "vitest", "server": "tsx watch server/src/index.ts", "server:start": "tsx server/src/index.ts", - "check:server": "tsc --noEmit -p server" + "check:server": "tsc --noEmit -p server", + "art": "node --import tsx docs/render-art.mjs" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.10", @@ -21,6 +22,7 @@ "@sveltejs/vite-plugin-svelte": "^7.1.2", "@types/node": "^26.6.2", "@types/ws": "^8.18.1", + "sharp": "^0.35.4", "svelte": "^5.56.1", "svelte-check": "^4.6.0", "tsx": "^4.23.15", diff --git a/src/lib/components/Board.svelte b/src/lib/components/Board.svelte index f49068c..ecefdfa 100644 --- a/src/lib/components/Board.svelte +++ b/src/lib/components/Board.svelte @@ -7,12 +7,12 @@ // Tablut evokes an excavated stone board and worn counters. This is an // artistic treatment, not a claim to reconstruct a particular find. import TableTalk from './TableTalk.svelte'; - import { beyond, cellOf, cornersOf, isEdge, movesFrom, rulesetOf, sideOf, throneOf, type Move, type Side } from '$lib/game'; + import BoardArtwork from './BoardArtwork.svelte'; + import { beyond, cellOf, cornersOf, movesFrom, rulesetOf, sideOf, throneOf, type Move, type Side } from '$lib/game'; import type { Room } from '$lib/net/room.svelte'; import { prefs, reducedMotion } from '$lib/prefs.svelte'; let { room }: { room: Room } = $props(); - const artId = $props.id(); const g = $derived(room.state!); const set = $derived(rulesetOf(g)); @@ -36,47 +36,12 @@ const sideOfPly = (ply: number): Side => (ply % 2 === 1 ? 'attackers' : 'defenders'); // --- Coordinates --------------------------------------------------------- - const CELL = 40; - const PAD_L = 16; - const PAD_T = 6; - const PAD_R = 6; - const PAD_B = 16; - const width = $derived(PAD_L + size * CELL + PAD_R); - const height = $derived(PAD_T + size * CELL + PAD_B); const label = (i: number) => { const c = cellOf(size, i); return `${String.fromCharCode(97 + c.x)}${size - c.y}`; }; 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 = [ - [0, 0, 0], [size - 1, 2, 1], [3, size - 1, 2], [0, size - 3, 3], [size - 4, 0, 1] - ] 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 + 22} ${PAD_T} l3 11 -2 7 5 12 -1 9 4 8 -2 12 m-1 -29 -8 5 -3 7`, - `M${w} ${h - 62} l-12 3 -6 -2 -9 6 -7 1 -5 6 -10 3 m22 -10 -2 -7 -5 -4` - ]; - }); - // --- Tokens: pieces with a life of their own, so a move slides and a capture fades --- interface Token { id: number; @@ -268,123 +233,8 @@ {/if} {#if room.error}

{room.error}

{/if} -
- - - - - - - - - - - - - - - - - - - - - - - {#each ['attacker', 'defender', 'king'] as kind (kind)} - - - - - - {/each} - - - - {#if worn} - - {:else} - {#each [0.22, 0.51, 0.79] as part (part)} - - {/each} - {/if} - - {#each showCoords ? Array.from({ length: size }, (_, n) => n) : [] as n (n)} - {String.fromCharCode(97 + n)} - {size - n} - {/each} - - {#each g.cells as piece, i (i)} - {@const c = cellOf(size, i)} - {@const x = PAD_L + c.x * CELL} - {@const y = PAD_T + c.y * CELL} - tap(i)} - onkeydown={(e) => key(e, i)} - /> - {#if targets.includes(i)} - - {/if} - {/each} - {#each [throne, ...corners] as place (place)} - {@const c = cellOf(size, place)} - - {/each} - {#each tokens as t (t.id)} - {@const c = cellOf(size, t.at)} - {@const cx = PAD_L + c.x * CELL + CELL / 2} - {@const cy = PAD_T + c.y * CELL + CELL / 2} - - - - - - - - - {#if worn} - - {/if} - {#if t.piece === 'k'} - - - {/if} - - - {/each} - - +
+

{liveText}

@@ -436,51 +286,6 @@ padding: 0 var(--gutter); max-width: 1280px; margin: 0 auto; - /* Copenhagen: a crafted oak board, polished counters and brass inlay. */ - --surface-light: #bd935c; - --surface-dark: #79502d; - --rim-light: #e0bf84; - --attacker-light: #63564a; - --defender-light: #fff4d8; - --king-light: #ffe4a0; - --wood: #a77945; - --wood-line: rgba(30, 16, 6, 0.55); - --coord: #302216; - --square: rgba(255, 240, 210, 0.08); - --square-edge: rgba(255, 250, 235, 0.11); - --throne: rgba(240, 165, 58, 0.35); - --corner: rgba(109, 52, 29, 0.22); - --attacker: #2a2320; - --attacker-rim: #211a15; - --defender: #ece4d0; - --defender-rim: #b9ab8b; - --king: #f0d58a; - --king-rim: #a67c1a; - --crown: #6b4a0f; - } - - /* Tablut: an excavated-object aesthetic, with limestone and aged counters. */ - .board[data-set='tablut'] { - --surface-light: #aaa18b; - --surface-dark: #797564; - --rim-light: #c4b99e; - --attacker-light: #625e51; - --defender-light: #e2d5b3; - --king-light: #e5cc92; - --wood: #8a8578; - --wood-line: rgba(36, 32, 27, 0.45); - --coord: #37362c; - --square: rgba(255, 250, 235, 0.05); - --square-edge: rgba(255, 250, 235, 0.09); - --throne: rgba(120, 100, 70, 0.4); - --corner: transparent; - --attacker: #3d3530; - --attacker-rim: #29281f; - --defender: #cfc4ad; - --defender-rim: #8c8069; - --king: #c9b27a; - --king-rim: #7d6636; - --crown: #5a4a2a; } @media (max-width: 860px) { @@ -540,132 +345,11 @@ flex-wrap: wrap; } - .calm .piece, - .calm .square, - .calm .wood { - transition: none; - } - .frame { max-width: 620px; margin: 0 auto; } - svg { - width: 100%; - height: auto; - display: block; - } - - .wood { - stroke: var(--surface-dark); - stroke-width: 1.4; - } - - .stain { - fill: #fff; - 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(43, 39, 29, 0.48); - stroke-width: 0.5; - stroke-linecap: round; - pointer-events: none; - } - - .coord { - fill: var(--coord); - font-family: var(--font); - font-size: 9px; - font-weight: 600; - pointer-events: none; - } - - .square { - fill: var(--square); - stroke: var(--wood-line); - stroke-width: 0.65; - cursor: default; - transition: fill 0.3s ease; - } - - .square.edge { - fill: var(--square-edge); - } - - .square.throne { - fill: var(--throne); - } - - .square.corner { - fill: var(--corner); - } - - .square.last { - fill: rgba(140, 200, 224, 0.28); - } - - .square.target { - cursor: pointer; - } - - .square.fallen { - fill: rgba(224, 101, 92, 0.45); - } - - .square:focus-visible { - outline: none; - stroke: var(--frost); - stroke-width: 2.5; - } - - .hint { - fill: var(--frost); - opacity: 0.85; - } - - .piece { - transition: - transform 0.36s cubic-bezier(0.2, 0.7, 0.2, 1), - opacity 0.3s ease; - } - - .piece.dying { - opacity: 0; - } - - .board-rim { fill: none; stroke: var(--rim-light); stroke-width: 0.7; opacity: 0.65; pointer-events: none; } - .join { fill: none; stroke: #50351f; stroke-width: 0.6; opacity: 0.18; pointer-events: none; } - .surface-wear { pointer-events: none; } - .crack-light { fill: none; stroke: #e1d6bd; stroke-width: 0.55; opacity: 0.45; } - .inlay { fill: none; stroke: var(--rim-light); stroke-width: 0.8; opacity: 0.75; pointer-events: none; } - .board[data-set='tablut'] .inlay { stroke: #494638; opacity: 0.45; } - - .piece { --piece-rim: var(--defender-rim); } - .piece.attacker { --piece-rim: var(--attacker-rim); } - .piece.king { --piece-rim: var(--king-rim); } - .piece .shadow { fill: #16130e; opacity: 0.28; } - .piece .base { fill: var(--piece-rim); } - .piece .body { stroke: var(--piece-rim); stroke-width: 0.65; } - .bevel-light { fill: none; stroke: #fff1cc; stroke-width: 0.9; opacity: 0.48; stroke-linecap: round; } - .bevel-dark { fill: none; stroke: #211d16; stroke-width: 0.8; opacity: 0.3; stroke-linecap: round; } - .turned { fill: none; stroke: var(--piece-rim); stroke-width: 0.45; opacity: 0.35; } - .counter-wear { fill: none; stroke: #534b37; stroke-width: 0.5; opacity: 0.3; stroke-linecap: round; } - .attacker .counter-wear { stroke: #d6c6a3; opacity: 0.23; } - .board[data-set='tablut'] .bevel-light { opacity: 0.25; stroke-width: 1.1; } - .board[data-set='tablut'] .turned { opacity: 0.12; } - .piece .crown { fill: var(--crown); stroke: var(--crown); stroke-width: 0.7; stroke-linejoin: round; } - .piece .crown-gleam { fill: #f8e5b2; stroke: #f8e5b2; stroke-width: 0.9; opacity: 0.7; } - .piece.selected .body { stroke: var(--frost); stroke-width: 2.5; } .how { max-width: 620px; diff --git a/src/lib/components/BoardArtwork.svelte b/src/lib/components/BoardArtwork.svelte new file mode 100644 index 0000000..b2a08db --- /dev/null +++ b/src/lib/components/BoardArtwork.svelte @@ -0,0 +1,348 @@ + + + + + + + + + + + + + + + + + + + + + + + + {#each ['attacker', 'defender', 'king'] as kind (kind)} + + + + + + {/each} + + + + {#if worn} + + {:else} + {#each [0.22, 0.51, 0.79] as part (part)} + + {/each} + {/if} + + {#each showCoords ? Array.from({ length: size }, (_, n) => n) : [] as n (n)} + {String.fromCharCode(97 + n)} + {size - n} + {/each} + + {#each g.cells as piece, i (i)} + {@const c = cellOf(size, i)} + {@const x = PAD_L + c.x * CELL} + {@const y = PAD_T + c.y * CELL} + + + tap(i)} + onkeydown={decorative ? undefined : (e) => key(e, i)} + /> + {#if targets.includes(i)} + + {/if} + {/each} + {#each [throne, ...corners] as place (place)} + {@const c = cellOf(size, place)} + + {/each} + {#each artworkTokens as t (t.id)} + {@const c = cellOf(size, t.at)} + {@const cx = PAD_L + c.x * CELL + CELL / 2} + {@const cy = PAD_T + c.y * CELL + CELL / 2} + + + + + + + + + {#if worn} + {#each [0, 1, 2, 3, 4] as pit (pit)} + + {/each} + + {/if} + {#if t.piece === 'k'} + + + {/if} + + + {/each} + + + diff --git a/src/lib/components/Hall.svelte b/src/lib/components/Hall.svelte index 3d46b6f..cd4af8f 100644 --- a/src/lib/components/Hall.svelte +++ b/src/lib/components/Hall.svelte @@ -11,7 +11,8 @@ import { unreadTalk } from '$lib/net/talk'; import type { RoomView } from '$lib/net/view'; import type { State } from '$lib/game'; - import { game } from '$lib/game'; + import { game, createGame } from '$lib/game'; + import BoardArtwork from './BoardArtwork.svelte'; let name = $state(playerName()); let code = $state(''); @@ -22,15 +23,7 @@ ); let prefsOpen = $state(false); - /** The hall's still of the opening position, on whichever board the selector names. */ - const still = $derived( - options.set === 'tablut' - ? { n: 9, wood: '#8d8577', line: 'rgba(40,34,28,0.5)', dark: '#3d3530', darkRim: '#1c1714', pale: '#cfc4ad', paleRim: '#8c8069', king: '#c9b27a', kingRim: '#7d6636', corners: false, - a: [[3,0],[4,0],[5,0],[4,1],[0,3],[8,3],[0,4],[1,4],[7,4],[8,4],[0,5],[8,5],[4,7],[3,8],[4,8],[5,8]], d: [[4,2],[4,3],[2,4],[3,4],[5,4],[6,4],[4,5],[4,6]] } - : { n: 11, wood: '#8a5a2b', line: 'rgba(30,16,6,0.5)', dark: '#2a2320', darkRim: '#0d0a08', pale: '#ece4d0', paleRim: '#b9ab8b', king: '#f0d58a', kingRim: '#a67c1a', corners: true, - a: [[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]], d: [[5,3],[4,4],[5,4],[6,4],[3,5],[4,5],[6,5],[7,5],[4,6],[5,6],[6,6],[5,7]] } - ); - const mid = $derived((still.n - 1) / 2); + const opening = $derived(createGame({ preview: 'Defenders', opponent: 'Attackers' }, 0, undefined, options)); let busy = $state(false); let error = $state(''); let seats = $state(allSeats()); @@ -217,21 +210,7 @@
{#if seats.length} @@ -641,12 +620,8 @@ display: block; margin: 0 auto; border-radius: 4px; - transition: filter 0.9s ease; } - .still.worn { - filter: contrast(0.85) sepia(0.15); - } .tally { display: grid; grid-template-columns: max-content 1fr; diff --git a/static/board-copenhagen.svg b/static/board-copenhagen.svg new file mode 100644 index 0000000..53192ba --- /dev/null +++ b/static/board-copenhagen.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/static/board-tablut.svg b/static/board-tablut.svg new file mode 100644 index 0000000..d1a99cc --- /dev/null +++ b/static/board-tablut.svg @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/static/og.png b/static/og.png index 6196720..9694e9c 100644 Binary files a/static/og.png and b/static/og.png differ