chesspal 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -2
- package/dist/cli/chat-hook.d.ts +2 -0
- package/dist/cli/chat-hook.js +33 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +28 -0
- package/dist/cli/command-line.d.ts +8 -0
- package/dist/cli/command-line.js +178 -0
- package/dist/commands/ai-move.d.ts +37 -0
- package/dist/commands/ai-move.js +92 -0
- package/dist/commands/ai-say.d.ts +20 -0
- package/dist/commands/ai-say.js +40 -0
- package/dist/commands/analyse-game.d.ts +61 -0
- package/dist/commands/analyse-game.js +238 -0
- package/dist/commands/annotate-game.d.ts +35 -0
- package/dist/commands/annotate-game.js +78 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/context.d.ts +56 -0
- package/dist/commands/context.js +36 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +15 -0
- package/dist/commands/drawings.js +23 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +64 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +26 -0
- package/dist/commands/game-summary.d.ts +95 -0
- package/dist/commands/game-summary.js +239 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/levels.d.ts +9 -0
- package/dist/commands/levels.js +25 -0
- package/dist/commands/look.d.ts +31 -0
- package/dist/commands/look.js +61 -0
- package/dist/commands/new-game.d.ts +69 -0
- package/dist/commands/new-game.js +215 -0
- package/dist/commands/opening-book.d.ts +13 -0
- package/dist/commands/opening-book.js +84 -0
- package/dist/commands/page-link.d.ts +11 -0
- package/dist/commands/page-link.js +28 -0
- package/dist/commands/past-games.d.ts +11 -0
- package/dist/commands/past-games.js +47 -0
- package/dist/commands/play-move.d.ts +44 -0
- package/dist/commands/play-move.js +102 -0
- package/dist/commands/player-move.d.ts +17 -0
- package/dist/commands/player-move.js +44 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +42 -0
- package/dist/commands/ponder.js +167 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +128 -0
- package/dist/commands/profiles.d.ts +60 -0
- package/dist/commands/profiles.js +129 -0
- package/dist/commands/registry.d.ts +54 -0
- package/dist/commands/registry.js +61 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/status.d.ts +29 -0
- package/dist/commands/status.js +103 -0
- package/dist/commands/study-games.d.ts +5 -0
- package/dist/commands/study-games.js +40 -0
- package/dist/commands/study.d.ts +38 -0
- package/dist/commands/study.js +122 -0
- package/dist/commands/view.d.ts +14 -0
- package/dist/commands/view.js +125 -0
- package/dist/core/board-text.d.ts +7 -0
- package/dist/core/board-text.js +18 -0
- package/dist/core/book.json +1 -0
- package/dist/core/clock.d.ts +45 -0
- package/dist/core/clock.js +109 -0
- package/dist/core/facts.d.ts +55 -0
- package/dist/core/facts.js +175 -0
- package/dist/core/game.d.ts +93 -0
- package/dist/core/game.js +348 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +11 -0
- package/dist/core/levels.d.ts +20 -0
- package/dist/core/levels.js +33 -0
- package/dist/core/material.d.ts +9 -0
- package/dist/core/material.js +12 -0
- package/dist/core/moods.d.ts +23 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +37 -0
- package/dist/core/openings.js +98 -0
- package/dist/core/openings.json +1 -0
- package/dist/core/pgn.d.ts +10 -0
- package/dist/core/pgn.js +213 -0
- package/dist/core/profile.d.ts +117 -0
- package/dist/core/profile.js +232 -0
- package/dist/core/record.d.ts +21 -0
- package/dist/core/record.js +25 -0
- package/dist/core/review.d.ts +74 -0
- package/dist/core/review.js +126 -0
- package/dist/games/ai-model.d.ts +5 -0
- package/dist/games/ai-model.js +15 -0
- package/dist/games/analysis.d.ts +12 -0
- package/dist/games/analysis.js +26 -0
- package/dist/games/archive.d.ts +42 -0
- package/dist/games/archive.js +168 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/current-game.d.ts +54 -0
- package/dist/games/current-game.js +128 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +52 -0
- package/dist/games/index.d.ts +10 -0
- package/dist/games/index.js +11 -0
- package/dist/games/plans.d.ts +9 -0
- package/dist/games/plans.js +23 -0
- package/dist/games/table-talk.d.ts +12 -0
- package/dist/games/table-talk.js +25 -0
- package/dist/games/talk.d.ts +40 -0
- package/dist/games/talk.js +131 -0
- package/dist/games/thoughts.d.ts +15 -0
- package/dist/games/thoughts.js +60 -0
- package/dist/home/data-folder.d.ts +12 -0
- package/dist/home/data-folder.js +55 -0
- package/dist/home/index.d.ts +2 -0
- package/dist/home/index.js +3 -0
- package/dist/home/open-folder.d.ts +2 -0
- package/dist/home/open-folder.js +14 -0
- package/dist/host-files/host-files.d.ts +10 -0
- package/dist/host-files/host-files.js +167 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/host-files/ponder-agents.d.ts +12 -0
- package/dist/host-files/ponder-agents.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/page/app.js +576 -0
- package/dist/page/avatars/beanie.webp +0 -0
- package/dist/page/avatars/beard.webp +0 -0
- package/dist/page/avatars/bun.webp +0 -0
- package/dist/page/avatars/cat.webp +0 -0
- package/dist/page/avatars/curls.webp +0 -0
- package/dist/page/avatars/glasses.webp +0 -0
- package/dist/page/avatars/moustache.webp +0 -0
- package/dist/page/avatars/owl.webp +0 -0
- package/dist/page/avatars/short-hair.webp +0 -0
- package/dist/page/avatars/silver-bob.webp +0 -0
- package/dist/page/avatars.js +16 -0
- package/dist/page/badges.js +50 -0
- package/dist/page/board.js +566 -0
- package/dist/page/browse.js +17 -0
- package/dist/page/clock-time.js +12 -0
- package/dist/page/dialog.js +26 -0
- package/dist/page/ending.js +73 -0
- package/dist/page/explain.js +58 -0
- package/dist/page/fold-tab.js +9 -0
- package/dist/page/folds.js +43 -0
- package/dist/page/game-column.js +44 -0
- package/dist/page/header.js +27 -0
- package/dist/page/helps-view.js +51 -0
- package/dist/page/helps.js +90 -0
- package/dist/page/main.js +8 -0
- package/dist/page/model-name.js +20 -0
- package/dist/page/move-list.js +68 -0
- package/dist/page/new-game-dialog.js +42 -0
- package/dist/page/opening-book.js +14 -0
- package/dist/page/pieces/CREDITS.md +36 -0
- package/dist/page/pieces/cburnett-small.svg +20 -0
- package/dist/page/pieces/cburnett.svg +20 -0
- package/dist/page/pieces/nitida-small.svg +19 -0
- package/dist/page/pieces/nitida.svg +20 -0
- package/dist/page/pieces/torretta-small.svg +19 -0
- package/dist/page/pieces/torretta.svg +19 -0
- package/dist/page/pieces.js +28 -0
- package/dist/page/player-card.js +60 -0
- package/dist/page/plies.js +62 -0
- package/dist/page/profile-dialogs.js +123 -0
- package/dist/page/profile.js +95 -0
- package/dist/page/settings-panel.js +32 -0
- package/dist/page/settings.js +76 -0
- package/dist/page/sounds/capture.ogg +0 -0
- package/dist/page/sounds/check.ogg +0 -0
- package/dist/page/sounds/draw.ogg +0 -0
- package/dist/page/sounds/flag.ogg +0 -0
- package/dist/page/sounds/mate.ogg +0 -0
- package/dist/page/sounds/move.ogg +0 -0
- package/dist/page/sounds/offer.ogg +0 -0
- package/dist/page/sounds/resign.ogg +0 -0
- package/dist/page/sounds.js +105 -0
- package/dist/page/studied.js +14 -0
- package/dist/page/study-dialog.js +58 -0
- package/dist/page/style.css +4398 -0
- package/dist/page/talk.js +172 -0
- package/dist/page/text.js +593 -0
- package/dist/page/torretta.js +38 -0
- package/dist/page/voice.js +38 -0
- package/dist/profiles/avatar.d.ts +23 -0
- package/dist/profiles/avatar.js +84 -0
- package/dist/profiles/front-matter.d.ts +9 -0
- package/dist/profiles/front-matter.js +33 -0
- package/dist/profiles/index.d.ts +3 -0
- package/dist/profiles/index.js +5 -0
- package/dist/profiles/profiles.d.ts +58 -0
- package/dist/profiles/profiles.js +237 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/analyses.d.ts +17 -0
- package/dist/server/analyses.js +46 -0
- package/dist/server/bell.d.ts +60 -0
- package/dist/server/bell.js +209 -0
- package/dist/server/caller.d.ts +10 -0
- package/dist/server/caller.js +99 -0
- package/dist/server/http.d.ts +16 -0
- package/dist/server/http.js +57 -0
- package/dist/server/mcp.d.ts +14 -0
- package/dist/server/mcp.js +179 -0
- package/dist/server/page.d.ts +46 -0
- package/dist/server/page.js +657 -0
- package/dist/server/profile-routes.d.ts +19 -0
- package/dist/server/profile-routes.js +186 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analyst.md +8 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "preact/hooks";
|
|
3
|
+
/** A dialog over the page, as the settings' one: Esc and a click outside close it; a step inside it goes back with the arrow beside its title. */
|
|
4
|
+
export function Dialog({ open, title, onBack, onClose, text, children }) {
|
|
5
|
+
const dialog = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const element = dialog.current;
|
|
8
|
+
if (open && !element.open) {
|
|
9
|
+
element.showModal();
|
|
10
|
+
// The field marked autofocus takes the focus, which the dialog itself would keep.
|
|
11
|
+
element.querySelector("[autofocus]")?.focus();
|
|
12
|
+
}
|
|
13
|
+
else if (!open && element.open) {
|
|
14
|
+
element.close();
|
|
15
|
+
}
|
|
16
|
+
}, [open]);
|
|
17
|
+
// A click on the backdrop reaches the dialog itself, as one on its padding does: only the first is outside its box.
|
|
18
|
+
const closeOutside = (event) => {
|
|
19
|
+
const box = dialog.current.getBoundingClientRect();
|
|
20
|
+
const inside = event.clientX >= box.left && event.clientX <= box.right && event.clientY >= box.top && event.clientY <= box.bottom;
|
|
21
|
+
if (event.target === dialog.current && !inside) {
|
|
22
|
+
onClose();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return (_jsxs("dialog", { ref: dialog, class: "settings profiles", "aria-label": title, onClose: onClose, onClick: closeOutside, children: [_jsxs("header", { children: [onBack === undefined ? null : (_jsx("button", { class: "theme", type: "button", title: text.profile.back, "aria-label": text.profile.back, onClick: onBack, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M15 6L9 12L15 18" }) }) })), _jsx("h2", { children: title }), _jsx("button", { class: "theme", type: "button", title: text.close, "aria-label": text.close, onClick: onClose, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 6L18 18M18 6L6 18" }) }) })] }), open ? children : null] }));
|
|
26
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// How a game ended, as the page names it (T-77): by the board, or by a resignation or a draw agreed, which only the record's Termination tag says (D-37).
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
3
|
+
/** How the game ended, from the board or the record's Termination tag; undefined while it goes on, or when the record does not say. */
|
|
4
|
+
export function howEnded(outcome, termination) {
|
|
5
|
+
if (outcome.over) {
|
|
6
|
+
return outcome.ending;
|
|
7
|
+
}
|
|
8
|
+
// Ours, and those of games played elsewhere (D-68): "Game drawn by agreement", Lichess's "Time forfeit", chess.com's "X won by resignation" in any of
|
|
9
|
+
// its languages, "X ha vinto per abbandono".
|
|
10
|
+
if (/^draw agreed$|by agreement|per accordo/i.test(termination ?? "")) {
|
|
11
|
+
return "agreement";
|
|
12
|
+
}
|
|
13
|
+
// A flag that fell (T-37): "Black lost on time", "Black ran out of time and White cannot mate".
|
|
14
|
+
if (/ (lost on|ran out of|won on) time\b|time forfeit|per tempo/i.test(termination ?? "")) {
|
|
15
|
+
return "time";
|
|
16
|
+
}
|
|
17
|
+
// "White resigns", or a draw (D-65): "White resigns and Black cannot mate".
|
|
18
|
+
return / resigns\b|by resignation|per abbandono/i.test(termination ?? "") ? "resignation" : undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whether the game ended a moment ago under the player's eyes, from going on to over, for as long as the page's effects on it last; a game already over when
|
|
22
|
+
* the page reads it does not count. `state` is undefined until the page has the game.
|
|
23
|
+
*/
|
|
24
|
+
export function useJustEnded(state) {
|
|
25
|
+
const before = useRef(state);
|
|
26
|
+
const [just, setJust] = useState(false);
|
|
27
|
+
// Before the page paints: a frame of the end at rest would show before its effects.
|
|
28
|
+
useLayoutEffect(() => {
|
|
29
|
+
const ended = before.current === "going" && state === "over";
|
|
30
|
+
before.current = state;
|
|
31
|
+
if (!ended) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
setJust(true);
|
|
35
|
+
const timer = setTimeout(() => setJust(false), effectsLast);
|
|
36
|
+
return () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
setJust(false);
|
|
39
|
+
};
|
|
40
|
+
}, [state]);
|
|
41
|
+
return just;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Whether the page saw the game end, from going on to over, while it stays over (D-77): its final position stays on the board until the next game. A game
|
|
45
|
+
* already over when the page reads it does not count. `state` is undefined until the page has the game.
|
|
46
|
+
*/
|
|
47
|
+
export function useSawEnd(state) {
|
|
48
|
+
// Known in the render itself: a frame of the table set in place of the final position would cost the last move its slide.
|
|
49
|
+
const seen = useRef({ state, saw: false });
|
|
50
|
+
seen.current = seeEnd(seen.current, state);
|
|
51
|
+
return seen.current.saw;
|
|
52
|
+
}
|
|
53
|
+
/** What the page knows after reading the game again in `state`: it saw the end when the game went from going on to over, and while it stays over. */
|
|
54
|
+
export function seeEnd(before, state) {
|
|
55
|
+
return { state, saw: state === "over" && (before.saw || before.state === "going") };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Whether the AI declined the player's draw offer by its move (D-65): their offer gone with a move more and the game going on, until the next move. `moves`
|
|
59
|
+
* is how many the game has.
|
|
60
|
+
*/
|
|
61
|
+
export function useDeclined(offer, moves, over) {
|
|
62
|
+
const before = useRef({ offer, moves });
|
|
63
|
+
const [declinedAt, setDeclinedAt] = useState(undefined);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (before.current.offer === "player" && offer === undefined && moves > before.current.moves && !over) {
|
|
66
|
+
setDeclinedAt(moves);
|
|
67
|
+
}
|
|
68
|
+
before.current = { offer, moves };
|
|
69
|
+
}, [offer, moves, over]);
|
|
70
|
+
return declinedAt === moves && !over;
|
|
71
|
+
}
|
|
72
|
+
/** How long the effects of a game over last, the longest of the style's: the board's, then the card's light. */
|
|
73
|
+
const effectsLast = 2800;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The drawing for a move refused, `to` being where it went and `king` the square of the player's king. */
|
|
2
|
+
export function explain(illegal, to, king) {
|
|
3
|
+
const kind = kinds[illegal.reason];
|
|
4
|
+
const by = illegal.by;
|
|
5
|
+
if (by === undefined) {
|
|
6
|
+
return { kind };
|
|
7
|
+
}
|
|
8
|
+
const target = illegal.reason === "attacked" ? to : illegal.reason === "castle-through" ? illegal.at : arrowsToKing.has(illegal.reason) ? king : undefined;
|
|
9
|
+
return target === undefined ? { kind, ring: by } : { kind, arrow: { from: by, to: target } };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Where the bubble of a refused move goes (Zoe's rule, T-91), squares as seen, in columns and rows from the top left: over the piece moved, else under it,
|
|
13
|
+
* so as never to hide the squares to `keep` (the culprit, the arrow's point); else under the board on a narrow one, or beside the piece on a wide one.
|
|
14
|
+
*/
|
|
15
|
+
export function bubblePlace(piece, keep, narrow) {
|
|
16
|
+
const [column, row] = piece;
|
|
17
|
+
// On a phone's board the bubble is taller than a square: it takes two rows.
|
|
18
|
+
const rows = narrow ? 2 : 1;
|
|
19
|
+
const free = (first, step) => first + step * (rows - 1) >= 0 && first + step * (rows - 1) <= 7 && !keep.some(([, kept]) => (kept - first) * step >= 0 && (kept - first) * step < rows);
|
|
20
|
+
if (free(row - 1, -1)) {
|
|
21
|
+
return "above";
|
|
22
|
+
}
|
|
23
|
+
if (free(row + 1, 1)) {
|
|
24
|
+
return "below";
|
|
25
|
+
}
|
|
26
|
+
if (narrow) {
|
|
27
|
+
return "under-board";
|
|
28
|
+
}
|
|
29
|
+
const right = keep.some(([kept, keptRow]) => keptRow === row && kept > column);
|
|
30
|
+
const left = keep.some(([kept, keptRow]) => keptRow === row && kept < column);
|
|
31
|
+
return right === left ? (column < 4 ? "right" : "left") : right ? "left" : "right";
|
|
32
|
+
}
|
|
33
|
+
/** The squares a bubble must leave in sight: the culprit, with its ring or the start of its arrow, and the arrow's point. */
|
|
34
|
+
export function keptSquares(explanation) {
|
|
35
|
+
if (explanation === undefined) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return explanation.arrow === undefined ? (explanation.ring === undefined ? [] : [explanation.ring]) : [explanation.arrow.from, explanation.arrow.to];
|
|
39
|
+
}
|
|
40
|
+
const kinds = {
|
|
41
|
+
"own": "piece",
|
|
42
|
+
"shape": "piece",
|
|
43
|
+
"pawn-straight": "piece",
|
|
44
|
+
"pawn-double": "piece",
|
|
45
|
+
"pawn-diagonal": "piece",
|
|
46
|
+
"en-passant-late": "piece",
|
|
47
|
+
"blocked": "blocked",
|
|
48
|
+
"pinned": "pin",
|
|
49
|
+
"check": "king",
|
|
50
|
+
"attacked": "king",
|
|
51
|
+
"castle-lost": "castle",
|
|
52
|
+
"castle-rook-gone": "castle",
|
|
53
|
+
"castle-blocked": "castle",
|
|
54
|
+
"castle-check": "castle",
|
|
55
|
+
"castle-through": "castle",
|
|
56
|
+
};
|
|
57
|
+
// The reasons whose culprit aims at the player's king.
|
|
58
|
+
const arrowsToKing = new Set(["pinned", "check", "castle-check"]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
// The tab on a side column's outer edge, a drawer's handle, that slides the column under the board and pulls it out again (T-84).
|
|
3
|
+
import { foldKeys } from "./folds.js";
|
|
4
|
+
/** The tab of a column: open, on the column's outer edge; folded, peeking out from under the board, with a dot when the AI spoke meanwhile. */
|
|
5
|
+
export function FoldTab({ column, folded, label, unread = false, onFold }) {
|
|
6
|
+
// The chevron points where a click moves the handle: in under the board, or out again.
|
|
7
|
+
const left = (column === "talk") === folded;
|
|
8
|
+
return (_jsx("div", { class: `fold-tab of-${column}${folded ? " folded" : ""}`, children: _jsx("button", { id: `fold-${column}`, class: unread ? "unread" : undefined, type: "button", title: `${label} (${foldKeys[column]})`, "aria-label": label, "aria-controls": column, "aria-expanded": !folded, "aria-keyshortcuts": foldKeys[column], onClick: () => onFold(column), children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: left ? "M14 7L9 12L14 17" : "M10 7L15 12L10 17" }) }) }) }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The side columns the player folds (T-84): the talk on the left, which narrows to its two faces before it folds, the game on the right; each as this browser
|
|
2
|
+
// remembers it.
|
|
3
|
+
/** The key that folds each column, as in editors with two side bars. */
|
|
4
|
+
export const foldKeys = { talk: "[", game: "]" };
|
|
5
|
+
/** The columns as this browser left them; else both open. */
|
|
6
|
+
export function startFolds() {
|
|
7
|
+
return { talk: read("talk"), game: read("game") === "folded" ? "folded" : "open" };
|
|
8
|
+
}
|
|
9
|
+
/** What a click on a column's tab makes of it: the talk narrows to its faces, then folds, then opens whole again; the game folds and opens. */
|
|
10
|
+
export function nextFold(column, fold) {
|
|
11
|
+
if (fold === "open") {
|
|
12
|
+
return column === "talk" ? "rail" : "folded";
|
|
13
|
+
}
|
|
14
|
+
return fold === "rail" ? "folded" : "open";
|
|
15
|
+
}
|
|
16
|
+
/** Remembers how the player left a column. */
|
|
17
|
+
export function rememberFold(column, fold) {
|
|
18
|
+
try {
|
|
19
|
+
localStorage.setItem(keys[column], fold);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// The choice lasts as long as the page.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Whether the game's column is under the board, where three columns would leave the board small, and does not fold. */
|
|
26
|
+
export function gameUnderBoard() {
|
|
27
|
+
return matchMedia("(max-width: 1099px)").matches;
|
|
28
|
+
}
|
|
29
|
+
/** Whether the page is in one column, as on a phone, where nothing folds. */
|
|
30
|
+
export function oneColumn() {
|
|
31
|
+
return matchMedia("(max-width: 840px)").matches;
|
|
32
|
+
}
|
|
33
|
+
const keys = { talk: "chesspal-talk", game: "chesspal-column" };
|
|
34
|
+
function read(column) {
|
|
35
|
+
try {
|
|
36
|
+
const fold = localStorage.getItem(keys[column]);
|
|
37
|
+
return fold === "rail" || fold === "folded" ? fold : "open";
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// A browser that keeps nothing for the page: the defaults decide.
|
|
41
|
+
return "open";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { outOfBookMove } from "../core/index.js";
|
|
3
|
+
import { MoveList, Steps } from "./move-list.js";
|
|
4
|
+
import { Piece } from "./pieces.js";
|
|
5
|
+
/**
|
|
6
|
+
* The column, which the player folds with its tab (T-84); under the board, where three columns would leave the board small, always open. A game in analysis
|
|
7
|
+
* (D-69) says above the moves that the AI is going over it, until its word on the whole game comes, which the talk shows.
|
|
8
|
+
*/
|
|
9
|
+
export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
|
|
10
|
+
return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
|
|
11
|
+
? (_jsxs(_Fragment, { children: [_jsx("p", { class: "card empty", children: text.noGame }), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseLast, onAnalyse: onAnalyse })] }))
|
|
12
|
+
: (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps, onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, text: text }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })) }));
|
|
13
|
+
}
|
|
14
|
+
/** A button that saves the game as a file in the player's downloads. */
|
|
15
|
+
function SaveButton({ label, onSave }) {
|
|
16
|
+
return (_jsxs("button", { type: "button", onClick: onSave, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 4V15M7 10L12 15L17 10M5 19H19" }) }), label] }));
|
|
17
|
+
}
|
|
18
|
+
/** The button that opens the game over in analysis. */
|
|
19
|
+
function AnalyseButton({ label, onAnalyse }) {
|
|
20
|
+
return (_jsx("div", { class: "card analysis-actions", children: _jsxs("button", { type: "button", onClick: onAnalyse, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 4V20M8 20H16M5 7H19M5 7L2.5 13A2.5 2 0 0 0 7.5 13ZM19 7L16.5 13A2.5 2 0 0 0 21.5 13Z" }) }), label] }) }));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The game's card, as tall as a player's (T-84): a little board of four squares with the king to move, the move and when the game started; at the end, the result on the
|
|
24
|
+
* square, the winner's king standing over the loser's, laid down, or both standing in a draw, how it ended and how many moves it lasted; lit up a moment when the
|
|
25
|
+
* game ends under the player's eyes (T-77).
|
|
26
|
+
*/
|
|
27
|
+
function GameCard({ plies, result, how, lit, started, move, toMove, text }) {
|
|
28
|
+
const when = started === undefined ? undefined : new Date(started).toLocaleString(text.language, { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
|
|
29
|
+
if (result === undefined) {
|
|
30
|
+
return (_jsxs("section", { class: "card game-card", "aria-label": text.game, children: [_jsx("span", { class: "tile moving", "aria-hidden": "true", children: _jsx(Piece, { letter: toMove === "white" ? "K" : "k" }) }), _jsxs("div", { class: "what", children: [_jsxs("p", { class: "head", children: [text.move, " ", _jsx("b", { class: "big", children: move }), _jsxs("span", { class: "visually-hidden", children: [", ", text.toMove(toMove)] })] }), when === undefined ? null : _jsx("p", { class: "when", children: text.started(when) })] })] }));
|
|
31
|
+
}
|
|
32
|
+
const winner = result === "1-0" ? "white" : result === "0-1" ? "black" : undefined;
|
|
33
|
+
const first = plies[0];
|
|
34
|
+
const last = plies.at(-1);
|
|
35
|
+
const lasted = first === undefined || last === undefined ? 0 : last.number - first.number + 1;
|
|
36
|
+
return (_jsxs("section", { class: `card game-card${lit ? " lit" : ""}`, "aria-label": text.game, children: [winner === undefined
|
|
37
|
+
? (_jsxs("span", { class: "tile over draw", "aria-hidden": "true", children: [_jsx(Piece, { letter: "K" }), _jsx(Piece, { letter: "k" })] }))
|
|
38
|
+
: (_jsxs("span", { class: "tile over win", "aria-hidden": "true", children: [_jsx("span", { class: "down", children: _jsx(Piece, { letter: winner === "white" ? "k" : "K" }) }), _jsx("span", { class: "up", children: _jsx(Piece, { letter: winner === "white" ? "K" : "k" }) })] })), _jsxs("div", { class: "what", children: [_jsxs("p", { class: "head", children: [_jsx("b", { class: "big", children: result === "1/2-1/2" ? "½–½" : result.replace("-", "–") }), " \u00B7 ", winner === undefined ? text.draw : text.won(winner)] }), _jsxs("p", { class: "when", children: [how === undefined ? "" : `${text.ended[how]} · `, text.lasted(lasted)] })] })] }));
|
|
39
|
+
}
|
|
40
|
+
/** The opening by Lichess's book: a book, crossed out once the game left it, and the family; the variation under them, and the move the game left the book at. */
|
|
41
|
+
function OpeningCard({ opening, text }) {
|
|
42
|
+
const book = opening.inBook ? text.inBook : `${text.outOfBook} ${outOfBookMove(opening)}`;
|
|
43
|
+
return (_jsxs("section", { class: "card opening", "aria-label": text.opening, children: [_jsxs("p", { class: "family", children: [_jsx("span", { class: `book${opening.inBook ? "" : " out"}`, title: book, role: "img", "aria-label": book, children: _jsxs("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("path", { d: "M12 6.5C10 5 7 4.5 4 5V18C7 17.5 10 18 12 19.5C14 18 17 17.5 20 18V5C17 4.5 14 5 12 6.5ZM12 6.5V19.5" }), opening.inBook ? null : _jsx("path", { d: "M3.5 3.5L20.5 20.5" })] }) }), _jsx("b", { children: opening.family })] }), opening.variation === undefined ? null : _jsx("p", { class: "variation", children: opening.variation }), opening.inBook ? null : _jsxs("p", { class: "out", children: [text.outOfBook, " ", outOfBookMove(opening)] })] }));
|
|
44
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { Torretta } from "./torretta.js";
|
|
3
|
+
/**
|
|
4
|
+
* The header, with how a game over ended (T-77), or that none goes on (D-77), or the AI's draw offer with the player's answer, the player's draw
|
|
5
|
+
* and resignation in a game going on (D-65), a new game once the page has the folder's, the command that turns the board round and says whether it is
|
|
6
|
+
* turned, the switches of the sounds and the theme, and the gear of the settings (D-56).
|
|
7
|
+
*/
|
|
8
|
+
export function Header({ text, verdict, kind, who, actions, onNewGame, standOut = false, onStudy, flipped, onFlip, sound, onSound, onTheme, onSettings }) {
|
|
9
|
+
return (_jsxs("header", { class: "top", children: [_jsx(Torretta, { size: 34 }), _jsx("b", { class: "title", children: "Chesspal" }), _jsxs("p", { class: `verdict${kind === undefined ? "" : ` ${kind}`}`, role: "status", children: [verdict === undefined || kind === undefined ? null : _jsx(Icon, { class: "kind", children: kindIcons[kind] }), who === undefined || verdict === undefined ? verdict : (_jsxs(_Fragment, { children: [_jsx("span", { class: "ai-name", children: who }), _jsx("span", { children: verdict })] }))] }), actions === undefined ? null : _jsx(Actions, { actions: actions, text: text }), onNewGame === undefined ? null : (_jsxs("button", { class: standOut ? "command primary" : "command", type: "button", title: text.newGame, onClick: onNewGame, children: [_jsx(Icon, { class: "new", children: _jsx("path", { d: "M12 5V19M5 12H19" }) }), _jsx("span", { children: text.newGame })] })), onStudy === undefined ? null : (_jsxs("button", { class: "command", type: "button", title: text.study.title, onClick: onStudy, children: [_jsxs(Icon, { class: "study", children: [_jsx("circle", { cx: "10.5", cy: "10.5", r: "5.5" }), _jsx("path", { d: "M14.5 14.5L20 20" })] }), _jsx("span", { children: text.study.title })] })), _jsxs("button", { class: "command", type: "button", title: text.flip, "aria-pressed": flipped, onClick: onFlip, children: [_jsx(Icon, { class: "flip", children: _jsx("path", { d: "M7 4V17M7 17L4 14M7 17L10 14M17 20V7M17 7L14 10M17 7L20 10" }) }), _jsx("span", { children: text.flip })] }), _jsx("button", { class: "theme", type: "button", title: text.sounds, "aria-label": text.sounds, "aria-pressed": sound, onClick: onSound, children: _jsxs(Icon, { class: "speaker", children: [_jsx("path", { d: "M5 9.5H8.5L13 5.5V18.5L8.5 14.5H5Z" }), sound ? _jsx("path", { d: "M16.5 9A4 4 0 0 1 16.5 15M19 6.5A7.5 7.5 0 0 1 19 17.5" }) : _jsx("path", { d: "M15.5 9L21.5 15M21.5 9L15.5 15" })] }) }), _jsxs("button", { class: "theme", type: "button", title: text.theme, "aria-label": text.theme, onClick: onTheme, children: [_jsxs(Icon, { class: "sun", children: [_jsx("circle", { cx: "12", cy: "12", r: "4" }), _jsx("path", { d: "M12 2V4M12 20V22M2 12H4M20 12H22M4.9 4.9L6.3 6.3M17.7 17.7L19.1 19.1M4.9 19.1L6.3 17.7M17.7 6.3L19.1 4.9" })] }), _jsx(Icon, { class: "moon", children: _jsx("path", { d: "M20 14.5A8 8 0 1 1 9.5 4A6.5 6.5 0 0 0 20 14.5Z" }) })] }), _jsx("button", { class: "theme", type: "button", title: text.settings, "aria-label": text.settings, onClick: onSettings, children: _jsxs(Icon, { class: "gear", children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M9.72 5.38L10.19 2.67L13.81 2.67L14.28 5.38L15.07 5.71L17.31 4.12L19.88 6.69L18.29 8.93L18.62 9.72L21.33 10.19L21.33 13.81L18.62 14.28L18.29 15.07L19.88 17.31L17.31 19.88L15.07 18.29L14.28 18.62L13.81 21.33L10.19 21.33L9.72 18.62L8.93 18.29L6.69 19.88L4.12 17.31L5.71 15.07L5.38 14.28L2.67 13.81L2.67 10.19L5.38 9.72L5.71 8.93L4.12 6.69L6.69 4.12L8.93 5.71Z" })] }) })] }));
|
|
10
|
+
}
|
|
11
|
+
/** The answer to the AI's draw offer while it stands, else the player's own offer; then the resignation. */
|
|
12
|
+
function Actions({ actions, text }) {
|
|
13
|
+
const { offer, canOffer, onOffer, onAnswer, onResign } = actions;
|
|
14
|
+
return (_jsxs(_Fragment, { children: [offer === "ai"
|
|
15
|
+
? (_jsxs(_Fragment, { children: [_jsxs("button", { class: "command answer", type: "button", title: text.acceptDraw, onClick: () => onAnswer(true), children: [_jsx(Icon, { class: "accept", children: _jsx("path", { d: "M5 12.5L10 17L19 7" }) }), _jsx("span", { children: text.acceptDraw })] }), _jsxs("button", { class: "command answer", type: "button", title: text.declineDrawTitle, onClick: () => onAnswer(false), children: [_jsx(Icon, { class: "decline", children: _jsx("path", { d: "M7 7L17 17M17 7L7 17" }) }), _jsx("span", { children: text.declineDraw })] })] }))
|
|
16
|
+
: (_jsxs("button", { class: "command", type: "button", title: offer === "player" ? text.drawOffered : canOffer ? text.offerDraw : text.offerDrawWhen, "aria-pressed": offer === "player", "aria-disabled": !canOffer, onClick: offer === "player" ? undefined : onOffer, children: [_jsx(Icon, { class: "half", children: _jsx("path", { d: "M5 7L7.5 5V11M16 5L8 19M13.5 14.5C13.5 13 17.5 12.5 17.5 15C17.5 16.5 13.5 18 13.5 19H18" }) }), _jsx("span", { children: text.offerDraw })] })), _jsxs("button", { class: "command", type: "button", title: text.resign, onClick: onResign, children: [_jsx(Icon, { class: "resign", children: _jsx("path", { d: "M6 21V4M6 4H18L15.5 8.5L18 13H6" }) }), _jsx("span", { children: text.resign })] })] }));
|
|
17
|
+
}
|
|
18
|
+
// A pair of scales, a flag, two hands, a pause.
|
|
19
|
+
const kindIcons = {
|
|
20
|
+
analysis: _jsx("path", { d: "M12 4V20M8 20H16M5 7H19M5 7L2.5 13A2.5 2 0 0 0 7.5 13ZM19 7L16.5 13A2.5 2 0 0 0 21.5 13Z" }),
|
|
21
|
+
over: _jsx("path", { d: "M6 21V4M6 4H17L14.5 8L17 12H6" }),
|
|
22
|
+
offer: _jsx("path", { d: "M3 12L7 8L11 10L14 7L21 12M7 14L10 17M10 13L13 16M13 12L16 15" }),
|
|
23
|
+
idle: _jsx("path", { d: "M9 6.5V17.5M15 6.5V17.5" }),
|
|
24
|
+
};
|
|
25
|
+
function Icon({ class: name, children }) {
|
|
26
|
+
return _jsx("svg", { class: `icon ${name}`, viewBox: "0 0 24 24", "aria-hidden": "true", children: children });
|
|
27
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** The helps of a game's position, whatever the AI's level (D-79). */
|
|
2
|
+
export function helpsOf(game) {
|
|
3
|
+
const facts = game.facts;
|
|
4
|
+
const pieces = new Map(facts.pieces.map((piece) => [piece.square, piece]));
|
|
5
|
+
const on = (square) => pieces.get(square);
|
|
6
|
+
const atRisk = (squares, why) => squares.map((square) => ({ piece: on(square), why }));
|
|
7
|
+
return {
|
|
8
|
+
toMove: game.position.turn,
|
|
9
|
+
atRisk: [...atRisk(facts.undefended, "undefended"), ...atRisk(facts.attackedByCheaper, "cheaper"), ...atRisk(facts.outnumbered, "outnumbered")],
|
|
10
|
+
pins: facts.pins.map((pin) => ({ piece: on(pin.square), by: on(pin.by), to: on(pin.to) })),
|
|
11
|
+
checks: facts.checks,
|
|
12
|
+
otherChecks: facts.otherChecks ?? [],
|
|
13
|
+
loose: facts.loose.map(on),
|
|
14
|
+
strategy: game.profile,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Whether the helps mark a square's piece as at risk. */
|
|
18
|
+
export function atRiskOn(helps, square) {
|
|
19
|
+
return helps.atRisk.some((risk) => risk.piece.square === square);
|
|
20
|
+
}
|
|
21
|
+
/** The strategy's table: only the lines where a side has something; the pawn islands only when a side has more than one. */
|
|
22
|
+
export function strategyOf(profile) {
|
|
23
|
+
const lines = Object.entries(cells).map(([row, cell]) => ({ row: row, white: cell(profile.white), black: cell(profile.black) }));
|
|
24
|
+
const split = profile.white.pawns.islands.length > 1 || profile.black.pawns.islands.length > 1;
|
|
25
|
+
return lines.filter((line) => line.row === "islands" ? split : something(line.white) || something(line.black));
|
|
26
|
+
}
|
|
27
|
+
const cells = {
|
|
28
|
+
isolated: (side) => side.pawns.isolated,
|
|
29
|
+
doubled: (side) => side.pawns.doubled,
|
|
30
|
+
backward: (side) => side.pawns.backward,
|
|
31
|
+
passed: (side) => side.pawns.passed,
|
|
32
|
+
islands: (side) => side.pawns.islands,
|
|
33
|
+
halfOpen: (side) => side.halfOpenFiles,
|
|
34
|
+
heavyOnFiles: (side) => side.heavyOnFiles,
|
|
35
|
+
onSeventh: (side) => side.onSeventh,
|
|
36
|
+
outposts: (side) => side.outposts.map((outpost) => outpost.square),
|
|
37
|
+
bishopPair: (side) => side.bishopPair,
|
|
38
|
+
space: (side) => side.space,
|
|
39
|
+
undeveloped: (side) => side.undeveloped,
|
|
40
|
+
castled: castlingOf,
|
|
41
|
+
kingAttacks: (side) => side.king?.attacksNear ?? 0,
|
|
42
|
+
};
|
|
43
|
+
function castlingOf(side) {
|
|
44
|
+
if (side.castled === "O-O" || side.castled === "O-O-O") {
|
|
45
|
+
return side.castled;
|
|
46
|
+
}
|
|
47
|
+
return side.castlingRights.length > 0 ? "not-yet" : side.castled === "no" ? "no-more" : "unknown";
|
|
48
|
+
}
|
|
49
|
+
function something(cell) {
|
|
50
|
+
return typeof cell === "object" ? cell.length > 0 : typeof cell === "number" ? cell > 0 : typeof cell === "boolean" ? cell : cell !== "unknown";
|
|
51
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
+
// The helps on the page (D-78, Zoe's sketch B): the board says where, the card says what, the facts the AI sees at the game's level and only those.
|
|
3
|
+
import { Fragment } from "preact";
|
|
4
|
+
import { useState } from "preact/hooks";
|
|
5
|
+
import { strategyOf } from "./helps-view.js";
|
|
6
|
+
import { Figurines } from "./move-list.js";
|
|
7
|
+
import { Figurine } from "./pieces.js";
|
|
8
|
+
/** The corner folded in ink on a piece at risk: on the board, and before its line in the card. */
|
|
9
|
+
export function Notch() {
|
|
10
|
+
return (_jsx("span", { class: "help-notch", "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 10 10", children: [_jsx("path", { d: "M0.5 0.5V9.5H9.5Z" }), _jsx("circle", { cx: "2.9", cy: "7.1", r: "1.1" })] }) }));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The helps' card, between the opening and the moves: the tactics in lines, the strategy in a table and the control map, seen while its button
|
|
14
|
+
* or M is held. A line under the pointer lights its squares on the board.
|
|
15
|
+
*/
|
|
16
|
+
export function HelpsCard({ helps, onPoint, onMap, mapping, text }) {
|
|
17
|
+
const [tab, setTab] = useState("tactics");
|
|
18
|
+
const words = text.helpsCard;
|
|
19
|
+
return (_jsxs("section", { class: "card helps-card", "aria-label": words.title, children: [_jsx("h3", { class: "helps-title", children: words.title }), _jsxs("div", { class: "tabs", role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": tab === "tactics", onClick: () => setTab("tactics"), children: words.tactics }), _jsx("button", { type: "button", role: "tab", "aria-selected": tab === "strategy", onClick: () => setTab("strategy"), children: words.strategy }), _jsx(MapButton, { mapping: mapping, onMap: onMap, held: true, text: text })] }), tab === "tactics" ? _jsx(Tactics, { helps: helps, onPoint: onPoint, text: text }) : _jsx(Strategy, { profile: helps.strategy, text: text })] }));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The map's button (D-78): the mouse holds it to see the map; a finger, or the keyboard, turns it on and off. In the card's tabs it says it is held; on a
|
|
23
|
+
* phone it sits under the board.
|
|
24
|
+
*/
|
|
25
|
+
export function MapButton({ mapping, onMap, held, text }) {
|
|
26
|
+
// A click carries its pointer's type: none from the keyboard.
|
|
27
|
+
const mouse = (event) => event instanceof PointerEvent && event.pointerType === "mouse";
|
|
28
|
+
return (_jsxs("button", { type: "button", class: "map-button", "aria-pressed": mapping, title: text.helpsCard.mapHint, onPointerDown: (event) => mouse(event) && onMap(true), onPointerUp: (event) => mouse(event) && onMap(false), onPointerLeave: (event) => mouse(event) && onMap(false), onClick: (event) => !mouse(event) && onMap(!mapping), children: [_jsxs("svg", { viewBox: "0 0 16 16", "aria-hidden": "true", children: [_jsx("path", { d: "M2 2h5v5H2zM9 9h5v5H9z" }), _jsx("path", { d: "M9 2h5v5H9zM2 9h5v5H2z", opacity: ".35" })] }), text.helpsCard.map, held ? _jsxs("small", { children: ["(", text.helpsCard.hold, ")"] }) : null] }));
|
|
29
|
+
}
|
|
30
|
+
function Tactics({ helps, onPoint, text }) {
|
|
31
|
+
const words = text.helpsCard;
|
|
32
|
+
// A row lights its squares while the pointer is on it; a touch keeps them lit until the position changes, as a square in the AI's words (T-111).
|
|
33
|
+
const point = (squares) => ({
|
|
34
|
+
onPointerEnter: () => onPoint(squares),
|
|
35
|
+
onPointerLeave: (event) => event.pointerType === "mouse" && onPoint([]),
|
|
36
|
+
});
|
|
37
|
+
const other = helps.toMove === "white" ? "black" : "white";
|
|
38
|
+
const loose = helps.loose.map((piece) => piece.square);
|
|
39
|
+
return (_jsxs("ul", { class: "help-rows", children: [helps.atRisk.map(({ piece, why }) => (_jsxs("li", { class: "points", ...point([piece.square, ...piece.attackers]), children: [_jsx(Notch, {}), text.piece(piece.piece, piece.side), " ", _jsx(Squares, { squares: [piece.square] }), " \u00B7 ", words.attackers, ": ", _jsx(Squares, { squares: piece.attackers }), why === "undefended" ? null : (_jsxs(_Fragment, { children: [" ", "\u00B7 ", words.defenders, ": ", _jsx(Squares, { squares: piece.defenders })] })), " \u00B7 ", words[why]] }, `risk ${piece.square}`))), helps.pins.map(({ piece, by, to }) => (_jsxs("li", { class: "points", ...point([piece.square, by.square, to.square]), children: [_jsx("span", { class: "row-icon", "aria-hidden": "true", children: "\u26D3" }), text.piece(piece.piece, piece.side), " ", _jsx(Squares, { squares: [piece.square] }), " ", words.pinnedTo(to.piece), " ", _jsx(Squares, { squares: [to.square] }), ", ", words.pinnedBy(by.piece), " ", _jsx(Squares, { squares: [by.square] })] }, `pin ${piece.square}`))), helps.atRisk.length === 0 && helps.pins.length === 0 ? _jsx("li", { class: "quiet", children: words.nothingAtRisk }) : null, helps.checks.length === 0 ? null : _jsx(Checks, { name: words.checks(helps.toMove, true), checks: helps.checks, side: helps.toMove }), helps.otherChecks.length === 0 ? null : _jsx(Checks, { name: words.checks(other, false), checks: helps.otherChecks, side: other }), loose.length === 0 ? null : (_jsxs("li", { class: "points quiet", ...point(loose), children: [words.loose, ": ", _jsx(Pieces, { pieces: helps.loose.filter((piece) => piece.side === "white") }), _jsx(Pieces, { pieces: helps.loose.filter((piece) => piece.side === "black") })] }, "loose"))] }));
|
|
40
|
+
}
|
|
41
|
+
/** A side's checks in SAN, drawn with the move list's figures (D-50). */
|
|
42
|
+
function Checks({ name, checks, side }) {
|
|
43
|
+
return (_jsxs("li", { children: [_jsx("span", { class: "row-icon", "aria-hidden": "true", children: "+" }), name, ": ", checks.map((check) => (_jsx("span", { class: "help-move", children: _jsx(Figurines, { san: check, side: side }) }, check)))] }));
|
|
44
|
+
}
|
|
45
|
+
/** Pieces of one side, each its figure and its square. */
|
|
46
|
+
function Pieces({ pieces }) {
|
|
47
|
+
return pieces.length === 0 ? null : (_jsx("span", { class: "help-pieces", children: pieces.map((piece) => (_jsxs("span", { class: "help-move", children: [_jsx(Figurine, { letter: piece.piece, side: piece.side }), _jsx("b", { class: "square-name", children: piece.square })] }, piece.square))) }));
|
|
48
|
+
}
|
|
49
|
+
function Squares({ squares }) {
|
|
50
|
+
return _jsx("b", { class: "square-name", children: squares.join(", ") });
|
|
51
|
+
}
|
|
52
|
+
function Strategy({ profile, text }) {
|
|
53
|
+
const words = text.helpsCard;
|
|
54
|
+
const files = profile.openFiles.length === 0 ? "–" : profile.openFiles.join(", ");
|
|
55
|
+
const { draw, pawnEnding } = profile;
|
|
56
|
+
// A name's meaning shows under its line at a click or a tap, since a phone has no pointer for the title (Rosa, T-117).
|
|
57
|
+
const [explained, setExplained] = useState(undefined);
|
|
58
|
+
const notes = [
|
|
59
|
+
words.openFiles(files),
|
|
60
|
+
words.controlled(profile.controlled.white, profile.controlled.black, profile.controlled.contested),
|
|
61
|
+
words.phases[profile.phase],
|
|
62
|
+
profile.oppositeBishops ? words.oppositeBishops : undefined,
|
|
63
|
+
draw.repetitions > 1 ? words.repetitions(draw.repetitions) : undefined,
|
|
64
|
+
draw.fiftyMovesLeft <= fiftyMovesShown ? words.fiftyMoves(draw.fiftyMovesLeft) : undefined,
|
|
65
|
+
draw.insufficient ? words.insufficient : undefined,
|
|
66
|
+
...(pawnEnding?.squares ?? []).map((square) => words.pawnSquare(square.pawn, square.caught)),
|
|
67
|
+
pawnEnding?.opposition === undefined ? undefined : words.opposition(pawnEnding.opposition),
|
|
68
|
+
];
|
|
69
|
+
return (_jsxs("div", { class: "strategy", children: [_jsxs("table", { class: "profile-table", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", {}), _jsx("th", { children: words.white }), _jsx("th", { children: words.black })] }) }), _jsx("tbody", { children: strategyOf(profile).map((line) => {
|
|
70
|
+
const [name, meaning] = words.rows[line.row];
|
|
71
|
+
const open = explained === line.row;
|
|
72
|
+
return (_jsxs(Fragment, { children: [_jsxs("tr", { children: [_jsx("th", { children: _jsx("button", { type: "button", class: "term", title: meaning, "aria-expanded": open, onClick: () => setExplained(open ? undefined : line.row), children: name }) }), _jsx("td", { children: cellText(line.white, words) }), _jsx("td", { children: cellText(line.black, words) })] }), open
|
|
73
|
+
? (_jsx("tr", { class: "meaning", children: _jsx("td", { colSpan: 3, children: meaning }) }))
|
|
74
|
+
: null] }, line.row));
|
|
75
|
+
}) })] }), _jsx("p", { class: "profile-foot", children: notes.filter((note) => note !== undefined).join(" · ") })] }));
|
|
76
|
+
}
|
|
77
|
+
// The fifty-move count shows only when the draw is near: ten moves each, or less.
|
|
78
|
+
const fiftyMovesShown = 20;
|
|
79
|
+
function cellText(cell, words) {
|
|
80
|
+
if (typeof cell === "object") {
|
|
81
|
+
return cell.length === 0 ? "–" : cell.join(", ");
|
|
82
|
+
}
|
|
83
|
+
if (typeof cell === "number") {
|
|
84
|
+
return String(cell);
|
|
85
|
+
}
|
|
86
|
+
if (typeof cell === "boolean") {
|
|
87
|
+
return cell ? words.yes : "–";
|
|
88
|
+
}
|
|
89
|
+
return cell === "not-yet" ? words.notYet : cell === "no-more" ? words.noMore : cell === "unknown" ? "–" : cell;
|
|
90
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "preact/jsx-runtime";
|
|
2
|
+
// The page's start: the settings this browser remembers, shown before the first frame, then the app drawn into its place.
|
|
3
|
+
import { render } from "preact";
|
|
4
|
+
import { App } from "./app.js";
|
|
5
|
+
import { loadSettings, showSettings } from "./settings.js";
|
|
6
|
+
const settings = loadSettings();
|
|
7
|
+
showSettings(settings);
|
|
8
|
+
render(_jsx(App, { start: settings }), document.getElementById("app"));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A model's name as the player says it, on the AI's card and under its analysis (D-81, D-82): the game's PGN keeps the id the host wrote.
|
|
2
|
+
/** Who played or reviewed, as the page signs it: the model's name and the effort in the host's word, no other words (Stefano), «Opus 5.5 · xhigh». */
|
|
3
|
+
export function signature(model, effort) {
|
|
4
|
+
return effort === undefined ? modelName(model) : `${modelName(model)} · ${effort}`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The name of a Claude model, from its id however the host writes it: claude-opus-5-5, us.anthropic.claude-opus-5-5-20261101-v1:0 and
|
|
8
|
+
* claude-opus-5-5@20261101 are Opus 5.5, claude-3-5-sonnet-20241022 is Sonnet 3.5; an id of another model stays as it is.
|
|
9
|
+
*/
|
|
10
|
+
export function modelName(id) {
|
|
11
|
+
const [, family, major, minor] = currentId.exec(id) ?? [];
|
|
12
|
+
const [, oldMajor, oldMinor, oldFamily] = family === undefined ? oldId.exec(id) ?? [] : [];
|
|
13
|
+
const name = family ?? oldFamily;
|
|
14
|
+
const version = [major ?? oldMajor, minor ?? oldMinor].filter((part) => part !== undefined).join(".");
|
|
15
|
+
return name === undefined ? id : `${name[0].toUpperCase()}${name.slice(1).toLowerCase()} ${version}`;
|
|
16
|
+
}
|
|
17
|
+
// The family, then the version, whose minor never runs into the date after it: claude-opus-4-20250514 is Opus 4.
|
|
18
|
+
const currentId = /claude-(opus|sonnet|haiku|fable)-(\d{1,2})(?:-(\d{1,2}))?(?!\d)/i;
|
|
19
|
+
// Before the fourth generation the version came first.
|
|
20
|
+
const oldId = /claude-(\d)(?:-(\d))?-(opus|sonnet|haiku)/i;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "preact/hooks";
|
|
3
|
+
import { BadgeIcon, NoteIcon } from "./badges.js";
|
|
4
|
+
import { Figurine } from "./pieces.js";
|
|
5
|
+
/**
|
|
6
|
+
* The moves numbered in pairs, the one the board shows marked (`shown`, how many moves it shows), and the result once the game is over; a click on a move
|
|
7
|
+
* shows it. The list keeps its last move in sight, and the one shown while the player looks back.
|
|
8
|
+
*/
|
|
9
|
+
export function MoveList({ plies, result, shown, onShow, text }) {
|
|
10
|
+
const list = useRef(null);
|
|
11
|
+
const rows = rowsOf(plies);
|
|
12
|
+
const current = plies[shown - 1];
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
list.current?.scrollTo({ top: list.current.scrollHeight });
|
|
15
|
+
}, [plies.length]);
|
|
16
|
+
// Only the list scrolls to the move shown: the page under the board stays where the player has it.
|
|
17
|
+
useEffect(() => keepInSight(list.current), [shown]);
|
|
18
|
+
// The cards above it change height with the position, the helps' card above all: the list follows, its move still in sight.
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const element = list.current;
|
|
21
|
+
if (element === null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const observer = new ResizeObserver(() => keepInSight(element));
|
|
25
|
+
observer.observe(element);
|
|
26
|
+
return () => observer.disconnect();
|
|
27
|
+
}, []);
|
|
28
|
+
const move = (ply) => _jsx(Move, { ply: ply, current: ply !== undefined && ply === current, onShow: () => onShow(plies.indexOf(ply) + 1), text: text });
|
|
29
|
+
return (_jsxs("div", { ref: list, class: "moves card", role: "region", "aria-label": text.moves, children: [_jsx("ol", { children: rows.map(({ number, white, black }) => (_jsxs("li", { children: [_jsxs("span", { class: "number", children: [number, "."] }), move(white), move(black)] }, number))) }), result === undefined ? null : _jsx("p", { class: "result", children: result })] }));
|
|
30
|
+
}
|
|
31
|
+
/** The buttons that step through the moves: to the start, back, forward, and to the game as it is; each one off where it could not go. */
|
|
32
|
+
export function Steps({ shown, total, onStep, text }) {
|
|
33
|
+
const button = (step, path, off) => (_jsx("button", { type: "button", title: text.steps[step], "aria-label": text.steps[step], disabled: off, onClick: () => onStep(step), children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: path }) }) }));
|
|
34
|
+
return (_jsxs("nav", { class: "steps card", "aria-label": text.moves, children: [button("first", "M7 6V18M17 6L11 12L17 18", shown === 0), button("back", "M14.5 6L8.5 12L14.5 18", shown === 0), button("forward", "M9.5 6L15.5 12L9.5 18", shown === total), button("last", "M17 6V18M7 6L13 12L7 18", shown === total)] }));
|
|
35
|
+
}
|
|
36
|
+
function Move({ ply, current, onShow, text }) {
|
|
37
|
+
if (ply === undefined) {
|
|
38
|
+
return _jsx("span", { children: "\u2026" });
|
|
39
|
+
}
|
|
40
|
+
// A screen reader reads the move in SAN and the AI's badge by its name; the eye sees each piece's letter as its figure, and the badge's disc (D-69).
|
|
41
|
+
return (_jsxs("button", { type: "button", class: current ? "ply current" : "ply", "aria-current": current ? "step" : undefined, onClick: onShow, children: [_jsx("span", { class: "visually-hidden", children: ply.review?.badge === undefined ? ply.san : `${ply.san}, ${text.badges[ply.review.badge]}` }), _jsx("span", { "aria-hidden": "true", children: _jsx(Figurines, { san: ply.san, side: ply.side }) }), ply.review?.badge !== undefined ? _jsx(BadgeIcon, { badge: ply.review.badge }) : ply.review?.note !== undefined ? _jsx(NoteIcon, {}) : null] }));
|
|
42
|
+
}
|
|
43
|
+
/** A move in SAN with each piece's letter drawn as its figure, the same in every language (D-50), and a capture's x in grey, so the squares stand out. */
|
|
44
|
+
export function Figurines({ san, side }) {
|
|
45
|
+
return (_jsx(_Fragment, { children: san.split(/([KQRBNx])/).map((part, index) => index % 2 === 0 ? part : part === "x" ? _jsx("span", { class: "takes", children: "x" }, index) : _jsx(Figurine, { letter: part, side: side }, index)) }));
|
|
46
|
+
}
|
|
47
|
+
/** Scrolls the list, and only the list, as little as it takes to show its current move. */
|
|
48
|
+
function keepInSight(list) {
|
|
49
|
+
const box = list?.getBoundingClientRect();
|
|
50
|
+
const move = list?.querySelector(".current")?.getBoundingClientRect();
|
|
51
|
+
if (box === undefined || move === undefined) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
list.scrollTop += Math.min(0, move.top - box.top) + Math.max(0, move.bottom - box.bottom);
|
|
55
|
+
}
|
|
56
|
+
/** The plies in rows of one number: White's move and Black's. */
|
|
57
|
+
function rowsOf(plies) {
|
|
58
|
+
const rows = [];
|
|
59
|
+
for (const ply of plies) {
|
|
60
|
+
let row = rows.at(-1);
|
|
61
|
+
if (row?.number !== ply.number) {
|
|
62
|
+
row = { number: ply.number };
|
|
63
|
+
rows.push(row);
|
|
64
|
+
}
|
|
65
|
+
row[ply.side] = ply;
|
|
66
|
+
}
|
|
67
|
+
return rows;
|
|
68
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { useState } from "preact/hooks";
|
|
3
|
+
import { aiLevels } from "../core/index.js";
|
|
4
|
+
import { Dialog } from "./dialog.js";
|
|
5
|
+
import { Piece } from "./pieces.js";
|
|
6
|
+
/** The clocks offered: long enough for an AI that takes 10 to 30 seconds a move (D-13), with the seconds added at each move. */
|
|
7
|
+
export const clocks = ["10+5", "15+10", "30+20"];
|
|
8
|
+
/** The dialog the + opens, with the choices of the game before; the game going on, if any, goes to the archive unfinished when the new one starts. */
|
|
9
|
+
export function NewGameDialog({ open, choice, going, onStart, onClose, text }) {
|
|
10
|
+
return (_jsx(Dialog, { open: open, title: text.newGame, onClose: onClose, text: text, children: _jsx(NewGameForm, { choice: choice, going: going, onStart: onStart, text: text }) }));
|
|
11
|
+
}
|
|
12
|
+
const colours = ["white", "black", "random"];
|
|
13
|
+
/** The colours by their king, the chosen set's silhouette in fixed colours, which both themes show (T-94); either colour by both kings. */
|
|
14
|
+
const colourIcons = {
|
|
15
|
+
white: _jsx(Piece, { letter: "K", small: true }),
|
|
16
|
+
black: _jsx(Piece, { letter: "k", small: true }),
|
|
17
|
+
random: (_jsxs("span", { class: "kings", children: [_jsx(Piece, { letter: "K", small: true }), _jsx(Piece, { letter: "k", small: true })] })),
|
|
18
|
+
};
|
|
19
|
+
/** A clock face with the minutes each side has filled in, as a slice of the hour; with no minutes, the face crossed out: a game without a clock. */
|
|
20
|
+
function ClockIcon({ minutes }) {
|
|
21
|
+
const angle = Math.min(minutes ?? 0, 59.9) / 60 * 2 * Math.PI;
|
|
22
|
+
const end = `${(12 + 8 * Math.sin(angle)).toFixed(2)} ${(12 - 8 * Math.cos(angle)).toFixed(2)}`;
|
|
23
|
+
return (_jsxs("svg", { class: "icon clock-icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: [_jsx("circle", { cx: "12", cy: "12", r: "8" }), minutes === undefined ? _jsx("path", { d: "M5 19L19 5" }) : _jsx("path", { class: "slice", d: `M12 12V4A8 8 0 ${angle > Math.PI ? 1 : 0} 1 ${end}Z` })] }));
|
|
24
|
+
}
|
|
25
|
+
/** The form, made again at each opening with the choices it starts from. */
|
|
26
|
+
function NewGameForm({ choice, going, onStart, text }) {
|
|
27
|
+
const words = text.start;
|
|
28
|
+
const [colour, setColour] = useState(choice.colour);
|
|
29
|
+
const [level, setLevel] = useState(choice.level);
|
|
30
|
+
const [clock, setClock] = useState(choice.clock);
|
|
31
|
+
// A clock of the game before that the dialog does not offer, such as one asked in the chat, is offered too.
|
|
32
|
+
const offered = choice.clock === undefined || clocks.includes(choice.clock) ? clocks : [...clocks, choice.clock];
|
|
33
|
+
const start = (event) => {
|
|
34
|
+
event.preventDefault();
|
|
35
|
+
onStart({ colour, level, ...(clock === undefined ? {} : { clock }) });
|
|
36
|
+
};
|
|
37
|
+
return (_jsxs("form", { class: "profile-form new-game", onSubmit: start, children: [_jsxs("fieldset", { children: [_jsx("legend", { children: words.colour }), _jsx("div", { class: "row", children: colours.map((id) => _jsx(Option, { group: "colour", on: colour === id, onPick: () => setColour(id), label: words.colours[id], icon: colourIcons[id], first: choice.colour === id }, id)) })] }), _jsxs("fieldset", { children: [_jsx("legend", { children: words.level }), _jsx("div", { class: "levels", children: aiLevels.map((id) => _jsx(Option, { group: "level", on: level === id, onPick: () => setLevel(id), label: words.levels[id][0], note: words.levels[id][1] }, id)) }), _jsx("p", { class: "sees", "aria-live": "polite", children: words.levels[level][2] })] }), _jsxs("fieldset", { children: [_jsxs("legend", { children: [words.clock, " ", _jsx("small", { children: words.clockNote })] }), _jsxs("div", { class: "row", children: [_jsx(Option, { group: "clock", on: clock === undefined, onPick: () => setClock(undefined), label: words.noClock, icon: _jsx(ClockIcon, {}) }), offered.map((id) => _jsx(Option, { group: "clock", on: clock === id, onPick: () => setClock(id), label: id, icon: _jsx(ClockIcon, { minutes: Number(id.split("+")[0]) }) }, id))] })] }), going ? _jsx("p", { class: "note", children: words.abandons }) : null, _jsx("button", { class: "start", type: "submit", children: words.start })] }));
|
|
38
|
+
}
|
|
39
|
+
/** One choice of a group, as the levels of the profile's form show them: an icon over its label, or a note under it; the `first` takes the focus when the dialog opens. */
|
|
40
|
+
function Option({ group, on, onPick, label, note, icon, first = false }) {
|
|
41
|
+
return (_jsxs("label", { class: "level", children: [_jsx("input", { type: "radio", name: group, checked: on, onChange: onPick, autofocus: first }), _jsxs("span", { children: [icon, _jsx("b", { children: label }), note === undefined ? null : _jsx("small", { children: note })] })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The book of the openings (D-16), fetched once the game has a move: the page names the opening itself, from the moves it replays.
|
|
2
|
+
import { useEffect, useState } from "preact/hooks";
|
|
3
|
+
let fetched;
|
|
4
|
+
/** The book, once it is needed and has come; undefined before, or when it cannot be had. */
|
|
5
|
+
export function useOpeningBook(needed) {
|
|
6
|
+
const [book, setBook] = useState(undefined);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (needed && book === undefined) {
|
|
9
|
+
fetched ??= fetch("/core/openings.json").then((response) => response.ok ? response.json() : undefined).catch(() => undefined);
|
|
10
|
+
void fetched.then(setBook);
|
|
11
|
+
}
|
|
12
|
+
}, [needed]);
|
|
13
|
+
return book;
|
|
14
|
+
}
|