chesspal 0.0.1 → 0.2.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 +99 -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 +7 -0
- package/dist/cli/command-line.js +177 -0
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +28 -0
- package/dist/commands/ai-move.js +85 -0
- package/dist/commands/ai-say.d.ts +22 -0
- package/dist/commands/ai-say.js +58 -0
- package/dist/commands/analyse-game.d.ts +65 -0
- package/dist/commands/analyse-game.js +265 -0
- package/dist/commands/annotate-game.d.ts +39 -0
- package/dist/commands/annotate-game.js +131 -0
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +67 -0
- package/dist/commands/context.js +37 -0
- package/dist/commands/delete-game.d.ts +7 -0
- package/dist/commands/delete-game.js +30 -0
- package/dist/commands/delete-review.d.ts +8 -0
- package/dist/commands/delete-review.js +34 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +13 -0
- package/dist/commands/drawings.js +25 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +67 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +27 -0
- package/dist/commands/game-summary.d.ts +78 -0
- package/dist/commands/game-summary.js +102 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.js +64 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -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 +64 -0
- package/dist/commands/new-game.d.ts +68 -0
- package/dist/commands/new-game.js +218 -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 +37 -0
- package/dist/commands/play-move.js +94 -0
- package/dist/commands/player-move.d.ts +13 -0
- package/dist/commands/player-move.js +51 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +52 -0
- package/dist/commands/ponder.js +179 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +130 -0
- package/dist/commands/profiles.d.ts +69 -0
- package/dist/commands/profiles.js +163 -0
- package/dist/commands/registry.d.ts +59 -0
- package/dist/commands/registry.js +62 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +35 -0
- package/dist/commands/status.js +128 -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 +106 -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 +95 -0
- package/dist/core/game.js +352 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +12 -0
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -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 +21 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +35 -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 +80 -0
- package/dist/core/review.js +146 -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 +63 -0
- package/dist/games/archive.js +242 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +56 -0
- package/dist/games/current-game.js +134 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +53 -0
- package/dist/games/index.d.ts +11 -0
- package/dist/games/index.js +12 -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 +36 -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/agents.d.ts +20 -0
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +15 -0
- package/dist/host-files/host-files.js +182 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/app/app.js +458 -0
- package/dist/page/app/keys.js +94 -0
- package/dist/page/app/phase.js +30 -0
- package/dist/page/app/served.js +73 -0
- package/dist/page/avatars/avatars.js +19 -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/thought.js +10 -0
- package/dist/page/avatars/torretta.js +38 -0
- package/dist/page/base.css +160 -0
- package/dist/page/board/board.css +959 -0
- package/dist/page/board/board.js +125 -0
- package/dist/page/board/explain.js +58 -0
- package/dist/page/board/marks.js +160 -0
- package/dist/page/board/motion.js +71 -0
- package/dist/page/board/pointer.js +239 -0
- package/dist/page/board/squares.js +26 -0
- package/dist/page/dialogs/analyses.js +195 -0
- package/dist/page/dialogs/dialog.js +26 -0
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/dialogs/new-game.js +42 -0
- package/dist/page/dialogs/profiles.js +152 -0
- package/dist/page/dialogs/search.js +31 -0
- package/dist/page/dialogs/settings.js +42 -0
- package/dist/page/dialogs/studied.js +39 -0
- package/dist/page/dialogs/study.js +51 -0
- package/dist/page/game/badges.js +50 -0
- package/dist/page/game/browse.js +17 -0
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +51 -0
- package/dist/page/game/ending.js +73 -0
- package/dist/page/game/game.css +577 -0
- package/dist/page/game/move-list.js +72 -0
- package/dist/page/game/opening-book.js +14 -0
- package/dist/page/game/plies.js +82 -0
- package/dist/page/game/result-tile.js +14 -0
- package/dist/page/helps/helps.css +689 -0
- package/dist/page/helps/helps.js +220 -0
- package/dist/page/helps/view.js +127 -0
- package/dist/page/layout/fold-tab.js +11 -0
- package/dist/page/layout/folds.js +43 -0
- package/dist/page/layout/header.js +28 -0
- package/dist/page/layout/layout.css +834 -0
- package/dist/page/lead/lead.js +82 -0
- package/dist/page/lead/view.js +38 -0
- package/dist/page/main.js +8 -0
- package/dist/page/pieces/CREDITS.md +19 -0
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +20 -0
- package/dist/page/pieces/nitida.svg +21 -0
- package/dist/page/pieces/pieces.js +28 -0
- package/dist/page/pieces/torretta-small.svg +20 -0
- package/dist/page/pieces/torretta.svg +21 -0
- package/dist/page/players/card.js +56 -0
- package/dist/page/players/clock-time.js +12 -0
- package/dist/page/players/model-name.js +20 -0
- package/dist/page/profile/profile.js +99 -0
- package/dist/page/profile/settings.js +77 -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/sounds.js +105 -0
- package/dist/page/style.css +9 -0
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/talk/talk.js +165 -0
- package/dist/page/talk/voice.js +38 -0
- package/dist/page/text.js +711 -0
- package/dist/page/theme.css +292 -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 +69 -0
- package/dist/profiles/profiles.js +254 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/bell.d.ts +68 -0
- package/dist/server/bell.js +225 -0
- package/dist/server/caller.d.ts +21 -0
- package/dist/server/caller.js +132 -0
- package/dist/server/mcp.d.ts +13 -0
- package/dist/server/mcp.js +207 -0
- package/dist/server/page/analyses.d.ts +23 -0
- package/dist/server/page/analyses.js +65 -0
- package/dist/server/page/files.d.ts +5 -0
- package/dist/server/page/files.js +92 -0
- package/dist/server/page/game.d.ts +6 -0
- package/dist/server/page/game.js +381 -0
- package/dist/server/page/http.d.ts +29 -0
- package/dist/server/page/http.js +68 -0
- package/dist/server/page/page.d.ts +41 -0
- package/dist/server/page/page.js +229 -0
- package/dist/server/page/profiles.d.ts +19 -0
- package/dist/server/page/profiles.js +215 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +4 -0
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// The move shown, kept in the page's address as #2026-11-04-184025/31: the game by its name and how many of its moves are shown, so that a reload keeps it and
|
|
2
|
+
// a link brings the player back to it.
|
|
3
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
4
|
+
/** The place an address's hash names, or none: #2026-11-04-184025/31 a move of a game, #2026-11-04-184025 the game as it is. */
|
|
5
|
+
export function anchorOf(hash) {
|
|
6
|
+
const [, game, plies] = new RegExp(`^#?(${stem})(?:/(\\d{1,4}))?$`).exec(hash) ?? [];
|
|
7
|
+
return game === undefined ? undefined : plies === undefined ? { game } : { game, plies: Number(plies) };
|
|
8
|
+
}
|
|
9
|
+
/** The hash of a place in a game, as anchorOf reads it. */
|
|
10
|
+
export function hashOf(anchor) {
|
|
11
|
+
return anchor.plies === undefined ? `#${anchor.game}` : `#${anchor.game}/${anchor.plies}`;
|
|
12
|
+
}
|
|
13
|
+
/** A game's name in the address, from its name in the player's folder: games/2026-11-04-184025.pgn and study/2026-11-04-184025.pgn are 2026-11-04-184025. */
|
|
14
|
+
export function anchorName(name) {
|
|
15
|
+
return new RegExp(`(${stem})\\.pgn$`).exec(name)?.[1];
|
|
16
|
+
}
|
|
17
|
+
// A game's name without its folder and .pgn, the time it started, as games/archive.ts writes it: the page cannot import that module.
|
|
18
|
+
const stem = /\d{4}-\d{2}-\d{2}-\d{6}/.source;
|
|
19
|
+
/**
|
|
20
|
+
* What the page does with the place the address names: goes to its move on the game shown; drops it while a game goes on; waits while its game opens in
|
|
21
|
+
* analysis, `asked` already; else opens its game in analysis, but the game just played, which opens on the player's yes alone (D-89): the link waits for
|
|
22
|
+
* it while the table is set (D-77), and goes once another game shows (Zoe).
|
|
23
|
+
*/
|
|
24
|
+
export function anchorAction(wanted, games, asked) {
|
|
25
|
+
if (wanted.game === games.shown) {
|
|
26
|
+
return "view";
|
|
27
|
+
}
|
|
28
|
+
if (games.going) {
|
|
29
|
+
return "drop";
|
|
30
|
+
}
|
|
31
|
+
if (wanted.game === games.played) {
|
|
32
|
+
return games.shown === undefined ? "wait" : "drop";
|
|
33
|
+
}
|
|
34
|
+
return wanted.game === asked ? "wait" : "open";
|
|
35
|
+
}
|
|
36
|
+
/** The hash of the move shown: the game alone at its end, and while it goes on, when the player wants the position as it is (Zoe). */
|
|
37
|
+
export function shownHash(games, viewing) {
|
|
38
|
+
return games.shown === undefined ? "" : hashOf(viewing === undefined || games.going ? { game: games.shown } : { game: games.shown, plies: viewing });
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Keeps the move shown in the address, and goes to the place the address names as the page opens or as the player writes it (anchorAction); `open` opens a
|
|
42
|
+
* game in analysis and says whether it did. A link never shows its move on another game.
|
|
43
|
+
*/
|
|
44
|
+
export function useAnchor(games, viewing, view, open) {
|
|
45
|
+
const { ready, shown, played, going } = games;
|
|
46
|
+
const [wanted, setWanted] = useState(() => anchorOf(location.hash));
|
|
47
|
+
const asked = useRef(undefined);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const heard = () => {
|
|
50
|
+
asked.current = undefined;
|
|
51
|
+
setWanted(anchorOf(location.hash));
|
|
52
|
+
};
|
|
53
|
+
window.addEventListener("hashchange", heard);
|
|
54
|
+
return () => window.removeEventListener("hashchange", heard);
|
|
55
|
+
}, []);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!ready || wanted === undefined) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const action = anchorAction(wanted, games, asked.current);
|
|
61
|
+
if (action === "view") {
|
|
62
|
+
view(wanted.plies);
|
|
63
|
+
}
|
|
64
|
+
if (action === "view" || action === "drop") {
|
|
65
|
+
setWanted(undefined);
|
|
66
|
+
}
|
|
67
|
+
if (action !== "open") {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const game = wanted.game;
|
|
71
|
+
asked.current = game;
|
|
72
|
+
void open(game).then((opened) => {
|
|
73
|
+
// A place written since stays wanted (Federico).
|
|
74
|
+
if (!opened && asked.current === game) {
|
|
75
|
+
setWanted(undefined);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}, [ready, shown, played, going, wanted]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (!ready || wanted !== undefined) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const hash = shownHash(games, viewing);
|
|
84
|
+
if (hash !== location.hash) {
|
|
85
|
+
history.replaceState(history.state, "", hash === "" ? location.pathname + location.search : hash);
|
|
86
|
+
}
|
|
87
|
+
}, [ready, shown, going, viewing, wanted]);
|
|
88
|
+
}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
|
3
|
+
import { aiModelIn, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
|
|
4
|
+
import { PlayerPicture } from "../avatars/avatars.js";
|
|
5
|
+
import { Thinker, Thought } from "../avatars/thought.js";
|
|
6
|
+
import { Torretta } from "../avatars/torretta.js";
|
|
7
|
+
import { Board } from "../board/board.js";
|
|
8
|
+
import { explain } from "../board/explain.js";
|
|
9
|
+
import { piecesOf } from "../board/squares.js";
|
|
10
|
+
import { NewGameDialog } from "../dialogs/new-game.js";
|
|
11
|
+
import { NameButton, PictureButton, PictureDialog, ProfilesDialog, Wizard } from "../dialogs/profiles.js";
|
|
12
|
+
import { SettingsPanel } from "../dialogs/settings.js";
|
|
13
|
+
import { playerIn, studiedSides } from "../dialogs/studied.js";
|
|
14
|
+
import { StudyDialog } from "../dialogs/study.js";
|
|
15
|
+
import { BadgeCounts } from "../game/badges.js";
|
|
16
|
+
import { stepped } from "../game/browse.js";
|
|
17
|
+
import { GameColumn } from "../game/column.js";
|
|
18
|
+
import { howEnded, useDeclined, useJustEnded, useSawEnd } from "../game/ending.js";
|
|
19
|
+
import { Steps } from "../game/move-list.js";
|
|
20
|
+
import { useOpeningBook } from "../game/opening-book.js";
|
|
21
|
+
import { aiSpoke, aiWords, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
|
|
22
|
+
import { BareButton, HelpsCard, HintLine, MapButton } from "../helps/helps.js";
|
|
23
|
+
import { helpsOf } from "../helps/view.js";
|
|
24
|
+
import { FoldTab } from "../layout/fold-tab.js";
|
|
25
|
+
import { nextFold, rememberFold, startFolds } from "../layout/folds.js";
|
|
26
|
+
import { Header } from "../layout/header.js";
|
|
27
|
+
import { standingsOf } from "../lead/view.js";
|
|
28
|
+
import { ChosenSet } from "../pieces/pieces.js";
|
|
29
|
+
import { ClockFace, PlayerCard } from "../players/card.js";
|
|
30
|
+
import { signature } from "../players/model-name.js";
|
|
31
|
+
import { keepSettings } from "../profile/profile.js";
|
|
32
|
+
import { languagesOf, saveSettings, settingsFrom, showSettings } from "../profile/settings.js";
|
|
33
|
+
import { endSoundOf, playSound, soundOf, wakeSounds } from "../sounds/sounds.js";
|
|
34
|
+
import { AiNote, AiWords, Compose, PointSquare, TalkFaces } from "../talk/talk.js";
|
|
35
|
+
import { textFor } from "../text.js";
|
|
36
|
+
import { anchorName, useAnchor } from "./anchor.js";
|
|
37
|
+
import { useBrowseKeys, useFoldKeys, useMapKey, useWheelSteps } from "./keys.js";
|
|
38
|
+
import { phaseOf, verdictOf } from "./phase.js";
|
|
39
|
+
import { post, sendMessage, sendMove, useServed } from "./served.js";
|
|
40
|
+
/**
|
|
41
|
+
* The page: the board of the game in the player's folder, where the player moves on their turn, the talk with the AI beside it, and the game's column; in the
|
|
42
|
+
* settings the player chose, `start` as this browser remembered them (D-56).
|
|
43
|
+
*/
|
|
44
|
+
export function App({ start }) {
|
|
45
|
+
const [settings, setSettings] = useState(start);
|
|
46
|
+
const [setting, setSetting] = useState(false);
|
|
47
|
+
const text = useMemo(() => textFor(languagesOf(settings)), [settings.language]);
|
|
48
|
+
const sound = settings.sound;
|
|
49
|
+
useEffect(() => void (document.documentElement.lang = text.language), [text]);
|
|
50
|
+
const apply = (next) => {
|
|
51
|
+
setSettings(next);
|
|
52
|
+
saveSettings(next);
|
|
53
|
+
showSettings(next);
|
|
54
|
+
};
|
|
55
|
+
// A choice of the player's is kept in the profile in use too, so that it follows them (D-56).
|
|
56
|
+
const change = (next) => {
|
|
57
|
+
apply(next);
|
|
58
|
+
if (served !== undefined) {
|
|
59
|
+
keepSettings(served.profile.id, next);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const [dialog, setDialog] = useState(undefined);
|
|
63
|
+
const { data: served, problem, offline, reload } = useServed(() => setDialog("new-game"));
|
|
64
|
+
const [selected, setSelected] = useState(undefined);
|
|
65
|
+
const [refusal, setRefusal] = useState(undefined);
|
|
66
|
+
// A pawn taken to the last rank waits for the player to choose what it becomes.
|
|
67
|
+
const [promoting, setPromoting] = useState(undefined);
|
|
68
|
+
const [flipped, setFlipped] = useState(false);
|
|
69
|
+
const [folds, setFolds] = useState(startFolds);
|
|
70
|
+
const [draft, setDraft] = useState("");
|
|
71
|
+
const [unsent, setUnsent] = useState(undefined);
|
|
72
|
+
// The profile in use brings its settings, as it comes and each time another comes in its place; one without any takes this browser's.
|
|
73
|
+
const profileId = served?.profile.id;
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (served === undefined || profileId === undefined) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (served.settings === undefined) {
|
|
79
|
+
keepSettings(profileId, settings);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
apply(settingsFrom(served.settings));
|
|
83
|
+
}
|
|
84
|
+
}, [profileId]);
|
|
85
|
+
// The AI as the host names it (Claude, Codex), also where the page sends the player to write; the last known while the page is offline.
|
|
86
|
+
const name = served?.opponent ?? text.ai;
|
|
87
|
+
// Under it, the model and effort the AI played its last move with (D-82).
|
|
88
|
+
const signed = served?.aiModel === undefined ? undefined : signature(served.aiModel.model, served.aiModel.effort);
|
|
89
|
+
// The game is replayed from its PGN once for each one the server sends, not at every click. Once no game goes on, the game in analysis shows in its place
|
|
90
|
+
// (D-69), with the AI's badges and notes: replayed again only when its review changed.
|
|
91
|
+
const game = served?.game;
|
|
92
|
+
const studied = served?.analysis;
|
|
93
|
+
const replayed = useMemo(() => studied !== undefined ? new Game(readPgn(studied.pgn)[0]) : game === undefined || game === null ? new Game() : new Game(readPgn(game.pgn)[0]), [studied === undefined ? game : studied.pgn]);
|
|
94
|
+
// Between games the board shows the table set for the next (D-77): before the first, and on a game over the page did not see end; one it saw end keeps
|
|
95
|
+
// its final position until the next starts. The talk keeps the game's words all the same.
|
|
96
|
+
const over = isOver(resultOf(replayed));
|
|
97
|
+
const sawEnd = useSawEnd(studied !== undefined || game === undefined || game === null ? undefined : over ? "over" : "going");
|
|
98
|
+
const phase = phaseOf(studied !== undefined, game, over, sawEnd);
|
|
99
|
+
const going = phase === "going";
|
|
100
|
+
const between = phase === "over" || phase === "idle";
|
|
101
|
+
const idle = phase === "idle";
|
|
102
|
+
const tableSet = useMemo(() => new Game(), []);
|
|
103
|
+
const live = idle ? tableSet : replayed;
|
|
104
|
+
const material = useMemo(() => materialOf(live.position.fen), [live]);
|
|
105
|
+
const pieces = useMemo(() => piecesOf(live.position.fen), [live]);
|
|
106
|
+
// While the game goes on, the book's first moves carry its badge, when the player wants it, as the AI's review gives it after (D-69).
|
|
107
|
+
const inBook = studied === undefined && settings.book ? game?.book ?? 0 : 0;
|
|
108
|
+
// The judgments of the player's moves, and the AI's drawings, show during a game only with the helps on, always once it is over (D-78); during it the
|
|
109
|
+
// coach's come from its own file, never in the PGN the opponent may read (D-88).
|
|
110
|
+
const judged = !going || settings.helps;
|
|
111
|
+
const coached = going ? game?.coach : undefined;
|
|
112
|
+
const plies = useMemo(() => {
|
|
113
|
+
const read = pliesOf(replayed.record);
|
|
114
|
+
return withBook(!judged ? withoutReviews(read) : coached === undefined ? read : withJudgments(read, coached), inBook);
|
|
115
|
+
}, [replayed, inBook, judged, coached]);
|
|
116
|
+
const book = useOpeningBook(live.record.moves.length > 0);
|
|
117
|
+
const opening = useMemo(() => book === undefined ? undefined : openingOf(live.record, book), [live, book]);
|
|
118
|
+
// The player's move shows at once, before the server has it: the game it sends next takes its place.
|
|
119
|
+
const [pending, setPending] = useState(undefined);
|
|
120
|
+
// The coach's hint for the position on the board, none once the player has moved, folded until they open it; a new hint comes folded (Zoe).
|
|
121
|
+
const hint = pending === undefined && coached?.hint?.position === replayed.record.moves.length ? coached.hint : undefined;
|
|
122
|
+
const hintKey = hint === undefined ? undefined : `${hint.position} ${hint.text}`;
|
|
123
|
+
const [openHint, setOpenHint] = useState(undefined);
|
|
124
|
+
const hintOpen = hintKey !== undefined && openHint === hintKey;
|
|
125
|
+
const openTheHint = (open) => setOpenHint(open ? hintKey : undefined);
|
|
126
|
+
// The player may look back through the moves played: `viewing` how many are shown, undefined for the game as it is; a new move brings it back.
|
|
127
|
+
const [viewing, setViewing] = useState(undefined);
|
|
128
|
+
// The squares the player points at in the AI's words or the helps' card, lit on the board; the control map while it is held (D-78).
|
|
129
|
+
const [pointed, setPointed] = useState(noSquares);
|
|
130
|
+
const pointSquare = useMemo(() => (square) => setPointed(square === undefined ? noSquares : [square]), []);
|
|
131
|
+
const [mapping, setMapping] = useState(false);
|
|
132
|
+
// The board without its drawings while their button is held (Stefano).
|
|
133
|
+
const [bare, setBare] = useState(false);
|
|
134
|
+
const past = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing) }), [live, viewing]);
|
|
135
|
+
const next = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing + 1) }).lastMove, [live, viewing]);
|
|
136
|
+
const shown = useMemo(() => past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past]);
|
|
137
|
+
// A square lit from the AI's words goes off with the position: a touch keeps it lit until then, a mouse until it leaves the word.
|
|
138
|
+
useEffect(() => setPointed(noSquares), [shown]);
|
|
139
|
+
// With the helps on, the player sees all of Chesspal's facts, whatever the AI's level (D-79), in a game and in the analysis of one (Stefano).
|
|
140
|
+
const analysing = studied !== undefined;
|
|
141
|
+
// Seen from the player's side (D-91): theirs in a game, in analysis the side the game names theirs, none when it names neither.
|
|
142
|
+
const you = analysing ? playerIn(studiedSides(live.record.tags, served?.profile.name ?? "", name, text.sides, served?.profile.accounts)) : game?.ai === undefined ? undefined : game.ai === "white" ? "black" : "white";
|
|
143
|
+
const helps = useMemo(() => (going || analysing) && settings.helps ? helpsOf(shown, you) : undefined, [shown, going, analysing, settings.helps, you]);
|
|
144
|
+
// Who the AI saw ahead, the coach's with the helps on in a game and the analyst's in analysis, for the bar and the chart (D-90).
|
|
145
|
+
const drawnOnBoard = live.outcome.over && live.outcome.result === "1/2-1/2";
|
|
146
|
+
const standings = useMemo(() => (going && settings.helps) || analysing ? standingsOf(plies, drawnOnBoard) : undefined, [plies, drawnOnBoard, going, analysing, settings.helps]);
|
|
147
|
+
useMapKey(helps !== undefined, setMapping);
|
|
148
|
+
// A new move or a new game ends what the player was doing on the one before: the move sent, the look back, the choice of a promotion. The same game
|
|
149
|
+
// sent again, for the talk, the coach's words or the settings, leaves them where they are (Zoe); so does a review written on the game in analysis.
|
|
150
|
+
const gameKey = studied !== undefined ? studied.name : game === undefined || game === null ? game : `${game.started} ${game.pgn}`;
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
setPending(undefined);
|
|
153
|
+
setViewing(undefined);
|
|
154
|
+
setPromoting(undefined);
|
|
155
|
+
}, [gameKey]);
|
|
156
|
+
const played = studied === undefined ? game : null;
|
|
157
|
+
useGameSounds(played, replayed, sound);
|
|
158
|
+
// Before the first game the AI speaks in the talk alone (D-77).
|
|
159
|
+
const spoke = played === undefined ? undefined : played === null ? served?.talk?.filter((line) => line.who === "ai").length ?? 0 : aiSpoke(plies, played.ai, played.talk);
|
|
160
|
+
const unread = useUnread(spoke, folds.talk !== "open");
|
|
161
|
+
const fold = (column) => {
|
|
162
|
+
const next = nextFold(column, folds[column]);
|
|
163
|
+
// The focus in what folds away goes to the column's tab.
|
|
164
|
+
if (next !== "open" && document.getElementById(column)?.contains(document.activeElement) === true) {
|
|
165
|
+
document.getElementById(`fold-${column}`)?.focus();
|
|
166
|
+
}
|
|
167
|
+
// The hint folds away with the game's column, so that its drawings never stay on the board without its words (Zoe).
|
|
168
|
+
if (column === "game" && next !== "open") {
|
|
169
|
+
setOpenHint(undefined);
|
|
170
|
+
}
|
|
171
|
+
setFolds({ ...folds, [column]: next });
|
|
172
|
+
rememberFold(column, next);
|
|
173
|
+
};
|
|
174
|
+
useFoldKeys(fold);
|
|
175
|
+
const total = live.record.moves.length;
|
|
176
|
+
const at = viewing ?? total;
|
|
177
|
+
// In analysis, the opening the move shown has reached, while the book knows the moves (D-69).
|
|
178
|
+
const bookAt = studied === undefined || book === undefined || at === 0 || at > studied.book ? undefined : { tags: live.record.tags, moves: live.record.moves.slice(0, at) };
|
|
179
|
+
const shownOpening = useMemo(() => bookAt === undefined || book === undefined ? undefined : openingOf(bookAt, book), [live, book, bookAt === undefined ? 0 : at]);
|
|
180
|
+
// A jump of more than one move shows the position at once: only a step slides its pieces.
|
|
181
|
+
const shownBefore = useRef(at);
|
|
182
|
+
const jumped = Math.abs(shownBefore.current - at) > 1;
|
|
183
|
+
shownBefore.current = at;
|
|
184
|
+
const goTo = (count) => {
|
|
185
|
+
setSelected(undefined);
|
|
186
|
+
setViewing(count >= total ? undefined : Math.max(0, count));
|
|
187
|
+
};
|
|
188
|
+
const step = (to) => goTo(stepped(to, at, total));
|
|
189
|
+
useBrowseKeys(step, () => setFlipped(!flipped));
|
|
190
|
+
const wheel = useWheelSteps(step);
|
|
191
|
+
// The move shown stays in the page's address, for a reload and a link to it; after the reset on a new game, which would undo the move it goes to. A
|
|
192
|
+
// game a link opens is there to look at: the analyst starts on the player's yes alone (D-89; Zoe and Federico).
|
|
193
|
+
const playedName = game === undefined || game === null ? undefined : anchorName(game.name);
|
|
194
|
+
useAnchor({ ready: served !== undefined, shown: studied !== undefined ? anchorName(studied.name) : idle ? undefined : playedName, played: playedName, going }, viewing, (plies) => goTo(plies ?? total), async (wanted) => await post("/api/study", { game: wanted, look: true }, text, name) === undefined);
|
|
195
|
+
// The header's theme switch picks the other of the two the page shows, leaving the system's.
|
|
196
|
+
const switchTheme = () => change({ ...settings, theme: document.documentElement.dataset.theme === "dark" ? "light" : "dark" });
|
|
197
|
+
// An action of the player's on the game: the game the server sends next shows it; a refusal shows on the board.
|
|
198
|
+
const act = (path, body) => {
|
|
199
|
+
setSelected(undefined);
|
|
200
|
+
void post(path, body, text, name).then((refused) => setRefusal(refused === undefined ? undefined : { text: refused.text }));
|
|
201
|
+
};
|
|
202
|
+
const result = resultOf(live);
|
|
203
|
+
const ended = isOver(result);
|
|
204
|
+
const how = ended ? howEnded(live.outcome, live.record.tags.Termination) : undefined;
|
|
205
|
+
const winner = result === "1-0" ? "white" : result === "0-1" ? "black" : undefined;
|
|
206
|
+
const justEnded = useJustEnded(played === undefined || played === null ? undefined : over ? "over" : "going");
|
|
207
|
+
const offer = played?.drawOffer;
|
|
208
|
+
const declined = useDeclined(offer, total, ended);
|
|
209
|
+
// In a game going on the player offers a draw on their turn once each side has moved, answers the AI's, or resigns, saying so first (D-65).
|
|
210
|
+
const actions = !going || played === undefined || played === null ? undefined : {
|
|
211
|
+
...(offer === undefined ? {} : { offer }),
|
|
212
|
+
canOffer: offer === undefined && pending === undefined && live.position.turn !== played.ai && total >= 2,
|
|
213
|
+
onOffer: () => act("/api/draw", { action: "offer" }),
|
|
214
|
+
onAnswer: (accept) => act("/api/draw", { action: accept ? "accept" : "decline" }),
|
|
215
|
+
onResign: () => {
|
|
216
|
+
if (confirm(text.resignGame)) {
|
|
217
|
+
act("/api/resign", {});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
const { kind, verdict } = verdictOf(phase, how, winner, offer, declined, text);
|
|
222
|
+
const header = (_jsx(Header, { text: text, kind: kind, verdict: verdict, ...(!ended && (offer === "ai" || declined) ? { who: name } : {}), actions: actions, onNewGame: served === undefined ? undefined : () => setDialog("new-game"),
|
|
223
|
+
// Between games the new one stands out in the player's card (D-77): in the header only when the talk, and the card with it, is folded away.
|
|
224
|
+
standOut: studied !== undefined || (between && folds.talk !== "open"), onStudy: served === undefined || going ? undefined : () => setDialog("study"), flipped: flipped, onFlip: () => setFlipped(!flipped), sound: sound, onSound: () => change({ ...settings, sound: !sound }), onTheme: switchTheme, onSettings: () => setSetting(true), ...(served === undefined ? {} : { profile: served.profile, onProfile: () => setDialog("profiles") }) }));
|
|
225
|
+
const panel = _jsx(SettingsPanel, { open: setting, settings: settings, onChange: change, onClose: () => setSetting(false), text: text });
|
|
226
|
+
if (problem !== undefined) {
|
|
227
|
+
return _jsx("p", { class: "problem", children: problem });
|
|
228
|
+
}
|
|
229
|
+
if (served === undefined || game === undefined) {
|
|
230
|
+
return (_jsxs(_Fragment, { children: [header, panel] }));
|
|
231
|
+
}
|
|
232
|
+
// At the first start the page asks who plays (D-23), to complete the profile Chesspal made (D-71): the game comes after.
|
|
233
|
+
if (served.profile.new === true) {
|
|
234
|
+
return _jsx(Wizard, { text: text, hasGame: served.game !== null, account: served.profile.name, dataFolder: served.dataFolder, onMade: reload });
|
|
235
|
+
}
|
|
236
|
+
const profile = served.profile;
|
|
237
|
+
const picture = _jsx(PlayerPicture, { profile: profile });
|
|
238
|
+
// In analysis the cards name the players of the game, the player's side at the bottom when they played it (D-69).
|
|
239
|
+
const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name, name, text.sides, profile.accounts);
|
|
240
|
+
const player = sides?.bottom ?? (game === null || game.ai === "black" ? "white" : "black");
|
|
241
|
+
const ai = player === "white" ? "black" : "white";
|
|
242
|
+
// The Torretta's face on the move shown: the mood the AI gave, while it lasts (D-70); in analysis, of the side the AI played.
|
|
243
|
+
const mood = moodAt(live.record, sides?.white.who === "ai" ? "white" : sides?.black.who === "ai" ? "black" : ai, at).mood;
|
|
244
|
+
const playerToMove = going && live.position.turn === player;
|
|
245
|
+
const ahead = (side) => side === "white" ? material.balance : -material.balance;
|
|
246
|
+
const reviewed = judged || at <= inBook ? plies[at - 1] : undefined;
|
|
247
|
+
// With the helps on, the coach's judgments of the player's last move, once the AI has answered it, and of one a held note let go only now (D-78, D-88).
|
|
248
|
+
const judgments = going && settings.helps ? judgmentsOf(plies, player, coached ?? noCoach) : [];
|
|
249
|
+
const review = reviewed?.review;
|
|
250
|
+
// An open hint draws on the board as it is, in place of the last move's drawings.
|
|
251
|
+
const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
|
|
252
|
+
const summary = studied === undefined ? undefined : gameReviewOf(live.record);
|
|
253
|
+
const closing = studied === undefined ? undefined : closingOf(live.record);
|
|
254
|
+
const annotator = studied === undefined ? undefined : annotatorOf(live.record);
|
|
255
|
+
// In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
|
|
256
|
+
const playedBy = studied === undefined ? undefined : aiModelIn(live.record);
|
|
257
|
+
const talk = game?.talk ?? served.talk ?? [];
|
|
258
|
+
const said = playerWords(talk);
|
|
259
|
+
// The clocks only in a game going on: between games the player's card holds the next one in their place (D-77).
|
|
260
|
+
const clock = going ? game?.clock : undefined;
|
|
261
|
+
// No chat hears the player: the page says where to wake it, and the AI's clock waits, as the server holds it.
|
|
262
|
+
const asleep = served.listening === false && !offline;
|
|
263
|
+
// What the AI thinks on, a cloud at the Torretta's corner (Zoe): only in a game going on, and never while no chat hears the player, which the note says.
|
|
264
|
+
const thinking = going && !asleep && !offline && studied === undefined ? served.thinking : undefined;
|
|
265
|
+
const thinker = (size) => (_jsx(Thinker, { thinking: thinking, words: text.thinking, children: _jsx(Torretta, { size: size, mood: mood }) }));
|
|
266
|
+
// The clocks wait for the player from the game's start: when the AI moves first, a button gives it the start; else the player's first move does (Stefano).
|
|
267
|
+
const clockWaits = clock?.waiting === true && !ended && studied === undefined && viewing === undefined && live.position.turn === ai;
|
|
268
|
+
const clockOf = (side) => clock === undefined ? undefined : (_jsx(ClockFace, { clock: clock, side: side, toMove: !ended && live.position.turn === side, held: pending !== undefined || (asleep && side === ai), onFlag: reload, text: text }));
|
|
269
|
+
// Where to wake the chat, under the AI's card (over the board when the talk is folded); the button that starts the clocks, under the player's (Zoe).
|
|
270
|
+
const asleepNote = asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null;
|
|
271
|
+
const startClocks = clockWaits
|
|
272
|
+
? (_jsxs("button", { type: "button", class: "command primary start-clocks", onClick: () => act("/api/start", {}), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M8 5.5v13l10.5-6.5z" }) }), text.startClocks] }))
|
|
273
|
+
: null;
|
|
274
|
+
// Between games the next starts from the player's card, where the clock and the button that starts it stand in a game (D-77).
|
|
275
|
+
const nextGame = between
|
|
276
|
+
? (_jsxs("button", { type: "button", class: "command primary next-game", onClick: () => setDialog("new-game"), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M12 5V19M5 12H19" }) }), text.newGame] }))
|
|
277
|
+
: null;
|
|
278
|
+
// Words sent leave the box; what the player wrote on while they went stays.
|
|
279
|
+
const clearSent = (refused, words) => {
|
|
280
|
+
if (refused === undefined) {
|
|
281
|
+
setDraft((now) => now.startsWith(words) ? now.slice(words.length).trimStart() : now);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const select = (square) => {
|
|
285
|
+
setRefusal(undefined);
|
|
286
|
+
setSelected(square);
|
|
287
|
+
};
|
|
288
|
+
const play = (from, to, piece) => {
|
|
289
|
+
setSelected(undefined);
|
|
290
|
+
setRefusal(undefined);
|
|
291
|
+
if (piece === undefined && promotes(pieces.get(from), to)) {
|
|
292
|
+
setPromoting({ from, to });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
// The words written and not sent go with the move (D-53): the box holds no more than the move has left (D-54). A command for Claude's app stays here.
|
|
296
|
+
const command = isCommand(draft);
|
|
297
|
+
const words = command ? "" : draft;
|
|
298
|
+
const move = `${from}${to}${piece ?? ""}`;
|
|
299
|
+
setPromoting(undefined);
|
|
300
|
+
setPending(move);
|
|
301
|
+
void sendMove(move, words, text, name).then((refused) => {
|
|
302
|
+
setRefusal(refused === undefined || refused.words ? undefined : { text: refused.text, square: from });
|
|
303
|
+
setUnsent(refused?.words === true ? refused.text : command ? text.commandHere(name) : undefined);
|
|
304
|
+
clearSent(refused?.text, words);
|
|
305
|
+
if (refused !== undefined) {
|
|
306
|
+
setPending(undefined);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
// A move the board refuses says why, without the AI (D-18), unless the player turned it off: the piece goes back, the reason over it.
|
|
311
|
+
const refuse = (from, to) => {
|
|
312
|
+
const illegal = settings.explain ? live.whyNot(from, to) : undefined;
|
|
313
|
+
if (illegal !== undefined) {
|
|
314
|
+
const letterOn = (square) => pieces.get(square).toUpperCase();
|
|
315
|
+
const sideOn = (square) => pieces.get(square) === letterOn(square) ? "white" : "black";
|
|
316
|
+
const by = illegal.by === undefined ? undefined : { letter: letterOn(illegal.by), side: sideOn(illegal.by), square: illegal.by };
|
|
317
|
+
const words = text.illegal({ reason: illegal.reason, piece: letterOn(from), to, takes: pieces.has(to), by, ...(illegal.at === undefined ? {} : { at: illegal.at }) });
|
|
318
|
+
const king = [...pieces].find(([, letter]) => letter === (sideOn(from) === "white" ? "K" : "k"))?.[0];
|
|
319
|
+
setRefusal({ text: words, square: from, piece: pieces.get(from), explanation: explain(illegal, to, king) });
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const sendWords = () => {
|
|
323
|
+
const words = draft;
|
|
324
|
+
// /clear and the other commands are for Claude's app: written here they would reach the AI as words (Sebastiano's playtest).
|
|
325
|
+
if (isCommand(words)) {
|
|
326
|
+
setUnsent(text.commandHere(name));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
setUnsent(undefined);
|
|
330
|
+
void sendMessage(words, text, name).then((refused) => {
|
|
331
|
+
setUnsent(refused?.text);
|
|
332
|
+
clearSent(refused?.text, words);
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
// The new game starts from the choices of the game before; the first, from the level that suits the player.
|
|
336
|
+
const level = suggestedLevel(played === null ? undefined : replayed.record, profile.level);
|
|
337
|
+
const lastControl = game?.clock?.control;
|
|
338
|
+
const lastClock = lastControl === undefined ? undefined : timeControlText(lastControl);
|
|
339
|
+
const choice = { colour: player, level, ...(lastClock === undefined ? {} : { clock: lastClock }) };
|
|
340
|
+
const startNewGame = ({ colour, level: chosen, clock: control }) => {
|
|
341
|
+
setDialog(undefined);
|
|
342
|
+
act("/api/new", { ai_colour: colour === "random" ? "random" : colour === "white" ? "black" : "white", level: chosen, ...(control === undefined ? {} : { clock: control }) });
|
|
343
|
+
};
|
|
344
|
+
// In analysis the AI's note stays under the card on top and its word on the game over the one below, however the board is turned (D-69).
|
|
345
|
+
const top = flipped ? player : ai;
|
|
346
|
+
const bottom = flipped ? ai : player;
|
|
347
|
+
const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place,
|
|
348
|
+
// The player changes their picture in analysis too, from their own side's card.
|
|
349
|
+
picture: sides[side].who === "player" ? _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("picture"), text: text }) : pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
|
|
350
|
+
// A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog.
|
|
351
|
+
const studyGame = async (studied) => {
|
|
352
|
+
const refused = await post("/api/study", studied, text, name);
|
|
353
|
+
if (refused === undefined) {
|
|
354
|
+
setDialog(undefined);
|
|
355
|
+
}
|
|
356
|
+
return refused?.text;
|
|
357
|
+
};
|
|
358
|
+
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(PictureDialog, { open: dialog === "picture", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onClose: () => setDialog(undefined), text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true ? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) }) : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
|
|
359
|
+
? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
|
|
360
|
+
: (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined
|
|
361
|
+
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[top].name, children: [studiedCard(top, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(summary === undefined ? { reviewing: true } : {}), name: name, by: annotator, mood: mood, ...(settings.voice ? { voice: text.language } : {}), text: text })] }), _jsx("section", { class: "you-block", "aria-label": sides[bottom].name, children: studiedCard(bottom, "you") })] }))
|
|
362
|
+
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: name, signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(settings.voice ? { voice: text.language } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
|
|
363
|
+
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
364
|
+
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
|
365
|
+
setDraft(words);
|
|
366
|
+
setUnsent(undefined);
|
|
367
|
+
}, onSend: sendWords, canMove: playerToMove, name: name, problem: unsent, text: text }), _jsx(PlayerCard, { place: "you", picture: _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("picture"), text: text }), name: _jsx(NameButton, { profile: profile, onOpen: () => setDialog("profiles"), text: text }), side: idle ? undefined : player, taken: material.taken[player], ahead: ahead(player), turn: playerToMove, clock: _jsxs(_Fragment, { children: [clockOf(player), startClocks, nextGame] }), text: text })] })] })) }), _jsxs("div", {
|
|
368
|
+
// Between games the board goes off (D-77): a game the page saw end once its effects are over, its final position less off than the table set.
|
|
369
|
+
class: between && !justEnded ? "board-wrap off" : "board-wrap", onWheel: wheel,
|
|
370
|
+
// A press on the board while the player looks back brings the game as it is: the next one plays.
|
|
371
|
+
onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: ended && viewing === undefined && pending === undefined ? { how, winner, player, fresh: justEnded } : undefined, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleep || clockWaits)
|
|
372
|
+
? (_jsxs("div", { class: "over-board", children: [asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null, startClocks] }))
|
|
373
|
+
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [_jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, text: text })), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: name } : { by: name, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
|
|
374
|
+
}
|
|
375
|
+
/** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
|
|
376
|
+
function pictureOf({ who }, profile, size, mood) {
|
|
377
|
+
return who === "ai" ? _jsx(Torretta, { size: size, mood: mood }) : _jsx(PlayerPicture, { profile: who === "player" ? profile : undefined, size: size });
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Plays the sound of each move the game gains while the page watches it, the AI's and the player's, of its end without a move and of a draw offered:
|
|
381
|
+
* none for a game loaded or begun again.
|
|
382
|
+
*/
|
|
383
|
+
function useGameSounds(game, live, on) {
|
|
384
|
+
const seen = useRef(undefined);
|
|
385
|
+
useEffect(wakeSounds, []);
|
|
386
|
+
useEffect(() => {
|
|
387
|
+
if (game === undefined || game === null) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const moves = live.record.moves.length;
|
|
391
|
+
const over = isOver(resultOf(live));
|
|
392
|
+
const offered = game.drawOffer !== undefined;
|
|
393
|
+
const before = seen.current;
|
|
394
|
+
seen.current = { started: game.started, moves, over, offered };
|
|
395
|
+
if (!on || before?.started !== game.started) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
// A move sounds as the board stands after it, and two read together as the last, so a mate that comes with the player's move sounds too; a game
|
|
399
|
+
// ended without a move, by how it ended; a draw offered, with its move or on its own.
|
|
400
|
+
const sound = moves > before.moves ? soundOf(live) : over && !before.over ? endSoundOf(howEnded(live.outcome, live.record.tags.Termination)) : undefined;
|
|
401
|
+
if (sound !== undefined) {
|
|
402
|
+
playSound(sound);
|
|
403
|
+
}
|
|
404
|
+
if (offered && !before.offered) {
|
|
405
|
+
playSound("offer");
|
|
406
|
+
}
|
|
407
|
+
}, [live]);
|
|
408
|
+
}
|
|
409
|
+
/** The game's result: the board's, else the record's, which a resignation, a draw agreed or a flag write. */
|
|
410
|
+
function resultOf(live) {
|
|
411
|
+
return live.outcome.over ? live.outcome.result : live.record.tags.Result;
|
|
412
|
+
}
|
|
413
|
+
function isOver(result) {
|
|
414
|
+
return result !== undefined && result !== "*";
|
|
415
|
+
}
|
|
416
|
+
/** Whether the AI spoke while the talk was folded, by how many times it has spoken (`spoke`, undefined before a game): until the player opens the talk. */
|
|
417
|
+
function useUnread(spoke, folded) {
|
|
418
|
+
const [unread, setUnread] = useState(false);
|
|
419
|
+
const heard = useRef(spoke);
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
if (!folded) {
|
|
422
|
+
setUnread(false);
|
|
423
|
+
}
|
|
424
|
+
else if (spoke !== undefined && heard.current !== undefined && spoke > heard.current) {
|
|
425
|
+
setUnread(true);
|
|
426
|
+
}
|
|
427
|
+
heard.current = spoke;
|
|
428
|
+
}, [spoke, folded]);
|
|
429
|
+
return unread;
|
|
430
|
+
}
|
|
431
|
+
const noSquares = [];
|
|
432
|
+
const noCoach = { judgments: [] };
|
|
433
|
+
/** Whether the words are a command for Claude's app, such as /clear, rather than words for the AI. */
|
|
434
|
+
function isCommand(words) {
|
|
435
|
+
return /^\s*\/[a-z]/i.test(words);
|
|
436
|
+
}
|
|
437
|
+
/** Saves a PGN file in the player's downloads: a game in analysis with its review, which the board page loads back ready, or the moves alone. */
|
|
438
|
+
function download(pgn, name) {
|
|
439
|
+
const link = document.createElement("a");
|
|
440
|
+
link.href = URL.createObjectURL(new Blob([pgn], { type: "application/x-chess-pgn" }));
|
|
441
|
+
link.download = name;
|
|
442
|
+
link.click();
|
|
443
|
+
URL.revokeObjectURL(link.href);
|
|
444
|
+
}
|
|
445
|
+
/** The file's name from the game's tags and what it holds: Ann-Bob-2026.11.02-analysis.pgn, without characters a file's name cannot hold. */
|
|
446
|
+
function pgnName(tags, kind) {
|
|
447
|
+
const parts = [tags.White, tags.Black, tags.Date?.includes("?") === true ? undefined : tags.Date, kind].filter((part) => part !== undefined && part !== "");
|
|
448
|
+
return `${(parts.length === 0 ? ["game"] : parts).join("-").replace(/[\\/:*?"<>|\s]+/g, "_")}.pgn`;
|
|
449
|
+
}
|
|
450
|
+
/** The game with the player's move played on a copy, for the board to show before the server answers; the game itself when the move is not legal. */
|
|
451
|
+
function withMove(game, move) {
|
|
452
|
+
const copy = new Game(game.record);
|
|
453
|
+
return copy.play(move).played ? copy : game;
|
|
454
|
+
}
|
|
455
|
+
/** Whether a pawn's move takes it to the last rank, where the player chooses what it becomes. */
|
|
456
|
+
function promotes(piece, to) {
|
|
457
|
+
return (piece === "P" && to.endsWith("8")) || (piece === "p" && to.endsWith("1"));
|
|
458
|
+
}
|