chesspal 0.1.0 → 0.2.1
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 +45 -82
- package/dist/cli/chat-hook.js +1 -1
- package/dist/cli/command-line.d.ts +1 -2
- package/dist/cli/command-line.js +2 -3
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +5 -14
- package/dist/commands/ai-move.js +28 -35
- package/dist/commands/ai-say.d.ts +3 -1
- package/dist/commands/ai-say.js +24 -6
- package/dist/commands/analyse-game.d.ts +4 -0
- package/dist/commands/analyse-game.js +43 -16
- package/dist/commands/annotate-game.d.ts +4 -0
- package/dist/commands/annotate-game.js +65 -12
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +16 -5
- package/dist/commands/context.js +1 -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/drawings.d.ts +0 -2
- package/dist/commands/drawings.js +5 -3
- package/dist/commands/end-game.js +5 -2
- package/dist/commands/export-game.d.ts +1 -1
- package/dist/commands/export-game.js +4 -3
- package/dist/commands/game-summary.d.ts +21 -38
- package/dist/commands/game-summary.js +30 -167
- package/dist/commands/index.d.ts +15 -0
- package/dist/commands/index.js +47 -2
- package/dist/commands/init.d.ts +3 -3
- package/dist/commands/init.js +4 -4
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -0
- package/dist/commands/look.js +4 -1
- package/dist/commands/new-game.d.ts +2 -3
- package/dist/commands/new-game.js +7 -4
- package/dist/commands/play-move.d.ts +8 -15
- package/dist/commands/play-move.js +5 -13
- package/dist/commands/player-move.d.ts +2 -6
- package/dist/commands/player-move.js +13 -6
- package/dist/commands/player-say.d.ts +1 -1
- package/dist/commands/player-say.js +1 -1
- package/dist/commands/ponder.d.ts +10 -0
- package/dist/commands/ponder.js +33 -21
- package/dist/commands/position.d.ts +1 -1
- package/dist/commands/position.js +3 -1
- package/dist/commands/profiles.d.ts +13 -4
- package/dist/commands/profiles.js +49 -15
- package/dist/commands/registry.d.ts +8 -3
- package/dist/commands/registry.js +12 -11
- package/dist/commands/say.d.ts +1 -1
- package/dist/commands/say.js +16 -16
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +9 -3
- package/dist/commands/status.js +40 -15
- package/dist/commands/study-games.js +1 -1
- package/dist/commands/study.js +3 -19
- package/dist/core/facts.js +15 -15
- package/dist/core/game.d.ts +3 -1
- package/dist/core/game.js +8 -4
- package/dist/core/illegal.js +1 -1
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +5 -4
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -0
- package/dist/core/levels.d.ts +1 -1
- package/dist/core/levels.js +1 -1
- package/dist/core/moods.d.ts +0 -2
- package/dist/core/moods.js +2 -2
- package/dist/core/openings.d.ts +0 -2
- package/dist/core/openings.js +1 -1
- package/dist/core/profile.js +1 -1
- package/dist/core/record.d.ts +1 -1
- package/dist/core/record.js +1 -1
- package/dist/core/review.d.ts +18 -3
- package/dist/core/review.js +52 -9
- package/dist/games/ai-model.d.ts +4 -0
- package/dist/games/ai-model.js +9 -0
- package/dist/games/analysis.d.ts +1 -1
- package/dist/games/archive.d.ts +25 -4
- package/dist/games/archive.js +99 -25
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +2 -0
- package/dist/games/current-game.js +7 -1
- package/dist/games/files.js +1 -0
- package/dist/games/index.d.ts +5 -4
- package/dist/games/index.js +5 -4
- package/dist/games/talk.d.ts +1 -5
- package/dist/games/talk.js +14 -14
- package/dist/home/open-folder.js +1 -1
- package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +7 -2
- package/dist/host-files/host-files.js +27 -12
- package/dist/host-files/index.d.ts +2 -2
- package/dist/host-files/index.js +3 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/{app.js → app/app.js} +116 -231
- 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/thought.js +10 -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/{explain.js → board/explain.js} +1 -1
- 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/{dialog.js → dialogs/dialog.js} +1 -1
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
- package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
- 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/{badges.js → game/badges.js} +1 -1
- package/dist/page/{browse.js → game/browse.js} +1 -1
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +55 -0
- package/dist/page/{ending.js → game/ending.js} +2 -2
- package/dist/page/game/game.css +577 -0
- package/dist/page/{move-list.js → game/move-list.js} +10 -6
- package/dist/page/{plies.js → game/plies.js} +30 -10
- 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/{folds.js → layout/folds.js} +1 -1
- package/dist/page/{header.js → layout/header.js} +5 -4
- 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 +2 -2
- package/dist/page/pieces/CREDITS.md +12 -29
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +3 -2
- package/dist/page/pieces/nitida.svg +3 -2
- package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
- package/dist/page/pieces/torretta-small.svg +15 -14
- package/dist/page/pieces/torretta.svg +17 -15
- package/dist/page/{player-card.js → players/card.js} +2 -6
- package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
- package/dist/page/{profile.js → profile/profile.js} +9 -5
- package/dist/page/{settings.js → profile/settings.js} +5 -4
- package/dist/page/style.css +9 -4398
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/{talk.js → talk/talk.js} +39 -46
- package/dist/page/text.js +137 -19
- package/dist/page/theme.css +292 -0
- package/dist/profiles/index.d.ts +1 -1
- package/dist/profiles/profiles.d.ts +15 -4
- package/dist/profiles/profiles.js +24 -7
- package/dist/server/bell.d.ts +10 -2
- package/dist/server/bell.js +28 -12
- package/dist/server/caller.d.ts +14 -3
- package/dist/server/caller.js +44 -11
- package/dist/server/mcp.d.ts +2 -3
- package/dist/server/mcp.js +53 -19
- 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/{http.d.ts → page/http.d.ts} +13 -0
- package/dist/server/{http.js → page/http.js} +11 -0
- package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
- package/dist/server/page/page.js +229 -0
- package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
- package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +3 -7
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +4 -4
- package/package.json +1 -1
- package/dist/host-files/ponder-agents.js +0 -40
- package/dist/page/avatars.js +0 -16
- package/dist/page/board.js +0 -566
- package/dist/page/fold-tab.js +0 -9
- package/dist/page/game-column.js +0 -44
- package/dist/page/helps-view.js +0 -51
- package/dist/page/helps.js +0 -90
- package/dist/page/pieces/cburnett-small.svg +0 -20
- package/dist/page/pieces/cburnett.svg +0 -20
- package/dist/page/settings-panel.js +0 -32
- package/dist/page/studied.js +0 -14
- package/dist/page/study-dialog.js +0 -58
- package/dist/server/analyses.d.ts +0 -17
- package/dist/server/analyses.js +0 -46
- package/dist/server/page.js +0 -657
- /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
- /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
- /package/dist/page/{model-name.js → players/model-name.js} +0 -0
- /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
- /package/dist/page/{voice.js → talk/voice.js} +0 -0
|
@@ -1,35 +1,42 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "preact/jsx-runtime";
|
|
2
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 "
|
|
4
|
-
import { PlayerPicture } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
3
|
+
import { aiHostIn, aiModelIn, aiNameOf, 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";
|
|
33
40
|
/**
|
|
34
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
|
|
35
42
|
* settings the player chose, `start` as this browser remembered them (D-56).
|
|
@@ -52,51 +59,16 @@ export function App({ start }) {
|
|
|
52
59
|
keepSettings(served.profile.id, next);
|
|
53
60
|
}
|
|
54
61
|
};
|
|
55
|
-
const [
|
|
56
|
-
const
|
|
57
|
-
const [offline, setOffline] = useState(false);
|
|
62
|
+
const [dialog, setDialog] = useState(undefined);
|
|
63
|
+
const { data: served, problem, offline, reload } = useServed(() => setDialog("new-game"));
|
|
58
64
|
const [selected, setSelected] = useState(undefined);
|
|
59
65
|
const [refusal, setRefusal] = useState(undefined);
|
|
60
|
-
// A pawn taken to the last rank waits for the player to choose what it becomes
|
|
66
|
+
// A pawn taken to the last rank waits for the player to choose what it becomes.
|
|
61
67
|
const [promoting, setPromoting] = useState(undefined);
|
|
62
68
|
const [flipped, setFlipped] = useState(false);
|
|
63
69
|
const [folds, setFolds] = useState(startFolds);
|
|
64
70
|
const [draft, setDraft] = useState("");
|
|
65
71
|
const [unsent, setUnsent] = useState(undefined);
|
|
66
|
-
const [dialog, setDialog] = useState(undefined);
|
|
67
|
-
// Reads the game again: when the server says it changed, and when a clock runs out on the page (T-37).
|
|
68
|
-
const reload = useRef(() => { });
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
let latest = 0;
|
|
71
|
-
const load = () => {
|
|
72
|
-
// Two reads in flight may answer out of order: only the last one asked counts.
|
|
73
|
-
const asked = ++latest;
|
|
74
|
-
fetch("/api/game")
|
|
75
|
-
.then((response) => response.json())
|
|
76
|
-
.then((body) => {
|
|
77
|
-
if (asked !== latest) {
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
setProblem(body.error);
|
|
81
|
-
if (body.error === undefined) {
|
|
82
|
-
setServed(body);
|
|
83
|
-
}
|
|
84
|
-
})
|
|
85
|
-
.catch((error) => setProblem(String(error)));
|
|
86
|
-
};
|
|
87
|
-
// The server says when the game changes, whoever changed it: the page reads it again, and as it connects, the first time or to a new chat's server.
|
|
88
|
-
// The server lives in the chat's MCP server: when the chat ends, after /clear in Claude's app too, the page waits for the next one and says so.
|
|
89
|
-
reload.current = load;
|
|
90
|
-
const events = new EventSource("/api/events");
|
|
91
|
-
events.onopen = () => {
|
|
92
|
-
setOffline(false);
|
|
93
|
-
load();
|
|
94
|
-
};
|
|
95
|
-
// The chat asks for a new game: the player chooses it in the New game window, which opens here.
|
|
96
|
-
events.onmessage = (event) => event.data === "new-game" ? setDialog("new-game") : load();
|
|
97
|
-
events.onerror = () => setOffline(events.readyState !== EventSource.OPEN);
|
|
98
|
-
return () => events.close();
|
|
99
|
-
}, []);
|
|
100
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.
|
|
101
73
|
const profileId = served?.profile.id;
|
|
102
74
|
useEffect(() => {
|
|
@@ -122,44 +94,69 @@ export function App({ start }) {
|
|
|
122
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
|
|
123
95
|
// its final position until the next starts. The talk keeps the game's words all the same.
|
|
124
96
|
const over = isOver(resultOf(replayed));
|
|
125
|
-
const going = studied === undefined && game !== undefined && game !== null && !over;
|
|
126
|
-
const between = studied === undefined && game !== undefined && !going;
|
|
127
97
|
const sawEnd = useSawEnd(studied !== undefined || game === undefined || game === null ? undefined : over ? "over" : "going");
|
|
128
|
-
const
|
|
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";
|
|
129
102
|
const tableSet = useMemo(() => new Game(), []);
|
|
130
103
|
const live = idle ? tableSet : replayed;
|
|
104
|
+
// In analysis the AI is who played that game, as its PGN says, and the review's words are of who wrote them: never the chat open now (D-97).
|
|
105
|
+
const annotator = studied === undefined ? undefined : annotatorOf(live.record);
|
|
106
|
+
const playedAs = studied === undefined ? name : aiNameOf(aiHostIn(live.record), aiModelIn(live.record)?.model) ?? text.ai;
|
|
107
|
+
const analyst = annotator === undefined ? name : aiNameOf(annotator.annotator, annotator.annotator) ?? text.ai;
|
|
131
108
|
const material = useMemo(() => materialOf(live.position.fen), [live]);
|
|
132
109
|
const pieces = useMemo(() => piecesOf(live.position.fen), [live]);
|
|
133
110
|
// 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).
|
|
134
111
|
const inBook = studied === undefined && settings.book ? game?.book ?? 0 : 0;
|
|
135
|
-
// The
|
|
112
|
+
// 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
|
|
113
|
+
// coach's come from its own file, never in the PGN the opponent may read (D-88).
|
|
136
114
|
const judged = !going || settings.helps;
|
|
137
|
-
const
|
|
115
|
+
const coached = going ? game?.coach : undefined;
|
|
116
|
+
const plies = useMemo(() => {
|
|
117
|
+
const read = pliesOf(replayed.record);
|
|
118
|
+
return withBook(!judged ? withoutReviews(read) : coached === undefined ? read : withJudgments(read, coached), inBook);
|
|
119
|
+
}, [replayed, inBook, judged, coached]);
|
|
138
120
|
const book = useOpeningBook(live.record.moves.length > 0);
|
|
139
121
|
const opening = useMemo(() => book === undefined ? undefined : openingOf(live.record, book), [live, book]);
|
|
140
|
-
// The player's move shows at once, before the server has it: the game it sends next takes its place
|
|
122
|
+
// The player's move shows at once, before the server has it: the game it sends next takes its place.
|
|
141
123
|
const [pending, setPending] = useState(undefined);
|
|
142
|
-
// The
|
|
124
|
+
// 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).
|
|
125
|
+
const hint = pending === undefined && coached?.hint?.position === replayed.record.moves.length ? coached.hint : undefined;
|
|
126
|
+
const hintKey = hint === undefined ? undefined : `${hint.position} ${hint.text}`;
|
|
127
|
+
const [openHint, setOpenHint] = useState(undefined);
|
|
128
|
+
const hintOpen = hintKey !== undefined && openHint === hintKey;
|
|
129
|
+
const openTheHint = (open) => setOpenHint(open ? hintKey : undefined);
|
|
130
|
+
// 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.
|
|
143
131
|
const [viewing, setViewing] = useState(undefined);
|
|
144
|
-
// The squares the player points at in the AI's words or the helps' card, lit on the board
|
|
132
|
+
// 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).
|
|
145
133
|
const [pointed, setPointed] = useState(noSquares);
|
|
146
134
|
const pointSquare = useMemo(() => (square) => setPointed(square === undefined ? noSquares : [square]), []);
|
|
147
135
|
const [mapping, setMapping] = useState(false);
|
|
136
|
+
// The board without its drawings while their button is held (Stefano).
|
|
137
|
+
const [bare, setBare] = useState(false);
|
|
148
138
|
const past = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing) }), [live, viewing]);
|
|
149
139
|
const next = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing + 1) }).lastMove, [live, viewing]);
|
|
150
140
|
const shown = useMemo(() => past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past]);
|
|
151
|
-
// 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
|
|
141
|
+
// 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.
|
|
152
142
|
useEffect(() => setPointed(noSquares), [shown]);
|
|
153
|
-
// With the helps on, the player sees all of Chesspal's facts, whatever the AI's level (D-79).
|
|
154
|
-
const
|
|
143
|
+
// 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).
|
|
144
|
+
const analysing = studied !== undefined;
|
|
145
|
+
// 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.
|
|
146
|
+
const you = analysing ? playerIn(studiedSides(live.record.tags, served?.profile.name ?? "", playedAs, text.sides, served?.profile.accounts)) : game?.ai === undefined ? undefined : game.ai === "white" ? "black" : "white";
|
|
147
|
+
const helps = useMemo(() => (going || analysing) && settings.helps ? helpsOf(shown, you) : undefined, [shown, going, analysing, settings.helps, you]);
|
|
148
|
+
// 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).
|
|
149
|
+
const drawnOnBoard = live.outcome.over && live.outcome.result === "1/2-1/2";
|
|
150
|
+
const standings = useMemo(() => (going && settings.helps) || analysing ? standingsOf(plies, drawnOnBoard) : undefined, [plies, drawnOnBoard, going, analysing, settings.helps]);
|
|
155
151
|
useMapKey(helps !== undefined, setMapping);
|
|
156
|
-
// A
|
|
157
|
-
//
|
|
152
|
+
// 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
|
|
153
|
+
// 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.
|
|
154
|
+
const gameKey = studied !== undefined ? studied.name : game === undefined || game === null ? game : `${game.started} ${game.pgn}`;
|
|
158
155
|
useEffect(() => {
|
|
159
156
|
setPending(undefined);
|
|
160
157
|
setViewing(undefined);
|
|
161
158
|
setPromoting(undefined);
|
|
162
|
-
}, [
|
|
159
|
+
}, [gameKey]);
|
|
163
160
|
const played = studied === undefined ? game : null;
|
|
164
161
|
useGameSounds(played, replayed, sound);
|
|
165
162
|
// Before the first game the AI speaks in the talk alone (D-77).
|
|
@@ -171,6 +168,10 @@ export function App({ start }) {
|
|
|
171
168
|
if (next !== "open" && document.getElementById(column)?.contains(document.activeElement) === true) {
|
|
172
169
|
document.getElementById(`fold-${column}`)?.focus();
|
|
173
170
|
}
|
|
171
|
+
// The hint folds away with the game's column, so that its drawings never stay on the board without its words (Zoe).
|
|
172
|
+
if (column === "game" && next !== "open") {
|
|
173
|
+
setOpenHint(undefined);
|
|
174
|
+
}
|
|
174
175
|
setFolds({ ...folds, [column]: next });
|
|
175
176
|
rememberFold(column, next);
|
|
176
177
|
};
|
|
@@ -191,6 +192,10 @@ export function App({ start }) {
|
|
|
191
192
|
const step = (to) => goTo(stepped(to, at, total));
|
|
192
193
|
useBrowseKeys(step, () => setFlipped(!flipped));
|
|
193
194
|
const wheel = useWheelSteps(step);
|
|
195
|
+
// 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
|
|
196
|
+
// game a link opens is there to look at: the analyst starts on the player's yes alone (D-89; Zoe and Federico).
|
|
197
|
+
const playedName = game === undefined || game === null ? undefined : anchorName(game.name);
|
|
198
|
+
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);
|
|
194
199
|
// The header's theme switch picks the other of the two the page shows, leaving the system's.
|
|
195
200
|
const switchTheme = () => change({ ...settings, theme: document.documentElement.dataset.theme === "dark" ? "light" : "dark" });
|
|
196
201
|
// An action of the player's on the game: the game the server sends next shows it; a refusal shows on the board.
|
|
@@ -217,21 +222,10 @@ export function App({ start }) {
|
|
|
217
222
|
}
|
|
218
223
|
},
|
|
219
224
|
};
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
: idle
|
|
223
|
-
? text.noGameNow
|
|
224
|
-
: ended
|
|
225
|
-
? text.verdict(how, winner)
|
|
226
|
-
: offer === "ai"
|
|
227
|
-
? text.offers
|
|
228
|
-
: offer === "player"
|
|
229
|
-
? text.drawOffered
|
|
230
|
-
: declined
|
|
231
|
-
? text.declines
|
|
232
|
-
: undefined, ...(!ended && (offer === "ai" || declined) ? { who: name } : {}), actions: actions, onNewGame: served === undefined ? undefined : () => setDialog("new-game"),
|
|
225
|
+
const { kind, verdict } = verdictOf(phase, how, winner, offer, declined, text);
|
|
226
|
+
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"),
|
|
233
227
|
// 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.
|
|
234
|
-
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) }));
|
|
228
|
+
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") }) }));
|
|
235
229
|
const panel = _jsx(SettingsPanel, { open: setting, settings: settings, onChange: change, onClose: () => setSetting(false), text: text });
|
|
236
230
|
if (problem !== undefined) {
|
|
237
231
|
return _jsx("p", { class: "problem", children: problem });
|
|
@@ -241,12 +235,12 @@ export function App({ start }) {
|
|
|
241
235
|
}
|
|
242
236
|
// At the first start the page asks who plays (D-23), to complete the profile Chesspal made (D-71): the game comes after.
|
|
243
237
|
if (served.profile.new === true) {
|
|
244
|
-
return _jsx(Wizard, { text: text, hasGame: served.game !== null, account: served.profile.name, dataFolder: served.dataFolder, onMade:
|
|
238
|
+
return _jsx(Wizard, { text: text, hasGame: served.game !== null, account: served.profile.name, dataFolder: served.dataFolder, onMade: reload });
|
|
245
239
|
}
|
|
246
240
|
const profile = served.profile;
|
|
247
241
|
const picture = _jsx(PlayerPicture, { profile: profile });
|
|
248
242
|
// In analysis the cards name the players of the game, the player's side at the bottom when they played it (D-69).
|
|
249
|
-
const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name,
|
|
243
|
+
const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name, playedAs, text.sides, profile.accounts);
|
|
250
244
|
const player = sides?.bottom ?? (game === null || game.ai === "black" ? "white" : "black");
|
|
251
245
|
const ai = player === "white" ? "black" : "white";
|
|
252
246
|
// 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.
|
|
@@ -254,12 +248,13 @@ export function App({ start }) {
|
|
|
254
248
|
const playerToMove = going && live.position.turn === player;
|
|
255
249
|
const ahead = (side) => side === "white" ? material.balance : -material.balance;
|
|
256
250
|
const reviewed = judged || at <= inBook ? plies[at - 1] : undefined;
|
|
257
|
-
// With the helps on, the
|
|
258
|
-
const
|
|
251
|
+
// 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).
|
|
252
|
+
const judgments = going && settings.helps ? judgmentsOf(plies, player, coached ?? noCoach) : [];
|
|
259
253
|
const review = reviewed?.review;
|
|
254
|
+
// An open hint draws on the board as it is, in place of the last move's drawings.
|
|
255
|
+
const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
|
|
260
256
|
const summary = studied === undefined ? undefined : gameReviewOf(live.record);
|
|
261
257
|
const closing = studied === undefined ? undefined : closingOf(live.record);
|
|
262
|
-
const annotator = studied === undefined ? undefined : annotatorOf(live.record);
|
|
263
258
|
// In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
|
|
264
259
|
const playedBy = studied === undefined ? undefined : aiModelIn(live.record);
|
|
265
260
|
const talk = game?.talk ?? served.talk ?? [];
|
|
@@ -268,9 +263,12 @@ export function App({ start }) {
|
|
|
268
263
|
const clock = going ? game?.clock : undefined;
|
|
269
264
|
// No chat hears the player: the page says where to wake it, and the AI's clock waits, as the server holds it.
|
|
270
265
|
const asleep = served.listening === false && !offline;
|
|
266
|
+
// 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.
|
|
267
|
+
const thinking = going && !asleep && !offline && studied === undefined ? served.thinking : undefined;
|
|
268
|
+
const thinker = (size) => (_jsx(Thinker, { thinking: thinking, words: text.thinking, children: _jsx(Torretta, { size: size, mood: mood }) }));
|
|
271
269
|
// 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).
|
|
272
270
|
const clockWaits = clock?.waiting === true && !ended && studied === undefined && viewing === undefined && live.position.turn === ai;
|
|
273
|
-
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:
|
|
271
|
+
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 }));
|
|
274
272
|
// 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).
|
|
275
273
|
const asleepNote = asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null;
|
|
276
274
|
const startClocks = clockWaits
|
|
@@ -337,7 +335,7 @@ export function App({ start }) {
|
|
|
337
335
|
clearSent(refused?.text, words);
|
|
338
336
|
});
|
|
339
337
|
};
|
|
340
|
-
// The new game starts from the choices of the game before; the first, from the level that suits the player
|
|
338
|
+
// The new game starts from the choices of the game before; the first, from the level that suits the player.
|
|
341
339
|
const level = suggestedLevel(played === null ? undefined : replayed.record, profile.level);
|
|
342
340
|
const lastControl = game?.clock?.control;
|
|
343
341
|
const lastClock = lastControl === undefined ? undefined : timeControlText(lastControl);
|
|
@@ -349,8 +347,10 @@ export function App({ start }) {
|
|
|
349
347
|
// 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).
|
|
350
348
|
const top = flipped ? player : ai;
|
|
351
349
|
const bottom = flipped ? ai : player;
|
|
352
|
-
const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place,
|
|
353
|
-
|
|
350
|
+
const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place,
|
|
351
|
+
// The player changes their picture in analysis too, from their own side's card.
|
|
352
|
+
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 }));
|
|
353
|
+
// A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog.
|
|
354
354
|
const studyGame = async (studied) => {
|
|
355
355
|
const refused = await post("/api/study", studied, text, name);
|
|
356
356
|
if (refused === undefined) {
|
|
@@ -358,11 +358,11 @@ export function App({ start }) {
|
|
|
358
358
|
}
|
|
359
359
|
return refused?.text;
|
|
360
360
|
};
|
|
361
|
-
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,
|
|
362
|
-
? (_jsx(TalkFaces, { name: name, aiPicture:
|
|
363
|
-
: (_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: text.talk
|
|
364
|
-
? (_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 } : {}), name:
|
|
365
|
-
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture:
|
|
361
|
+
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
|
|
362
|
+
? (_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 }))
|
|
363
|
+
: (_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
|
|
364
|
+
? (_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: analyst, 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") })] }))
|
|
365
|
+
: (_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,
|
|
366
366
|
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
367
367
|
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
|
368
368
|
setDraft(words);
|
|
@@ -371,9 +371,9 @@ export function App({ start }) {
|
|
|
371
371
|
// 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.
|
|
372
372
|
class: between && !justEnded ? "board-wrap off" : "board-wrap", onWheel: wheel,
|
|
373
373
|
// A press on the board while the player looks back brings the game as it is: the next one plays.
|
|
374
|
-
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 } : {}), ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(
|
|
374
|
+
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)
|
|
375
375
|
? (_jsxs("div", { class: "over-board", children: [asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null, startClocks] }))
|
|
376
|
-
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (
|
|
376
|
+
: 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: analyst } : { by: analyst, 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 })] })] }) }));
|
|
377
377
|
}
|
|
378
378
|
/** 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. */
|
|
379
379
|
function pictureOf({ who }, profile, size, mood) {
|
|
@@ -381,7 +381,7 @@ function pictureOf({ who }, profile, size, mood) {
|
|
|
381
381
|
}
|
|
382
382
|
/**
|
|
383
383
|
* 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:
|
|
384
|
-
* none for a game loaded or begun again
|
|
384
|
+
* none for a game loaded or begun again.
|
|
385
385
|
*/
|
|
386
386
|
function useGameSounds(game, live, on) {
|
|
387
387
|
const seen = useRef(undefined);
|
|
@@ -431,128 +431,13 @@ function useUnread(spoke, folded) {
|
|
|
431
431
|
}, [spoke, folded]);
|
|
432
432
|
return unread;
|
|
433
433
|
}
|
|
434
|
-
/**
|
|
435
|
-
* `[` and `]` fold the talk and the game, as in editors with two side bars; never while the player types or drags a piece, nor in one column, nor the game
|
|
436
|
-
* under the board.
|
|
437
|
-
*/
|
|
438
|
-
function useFoldKeys(fold) {
|
|
439
|
-
useEffect(() => {
|
|
440
|
-
const press = (event) => {
|
|
441
|
-
if (!forThePage(event) || oneColumn()) {
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
const column = Object.keys(foldKeys).find((key) => foldKeys[key] === event.key);
|
|
445
|
-
if (column !== undefined && !(column === "game" && gameUnderBoard())) {
|
|
446
|
-
event.preventDefault();
|
|
447
|
-
fold(column);
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
window.addEventListener("keydown", press);
|
|
451
|
-
return () => window.removeEventListener("keydown", press);
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
/** M held shows the control map while the helps are on (D-78); it goes off when M is let go or the page loses the focus. */
|
|
455
|
-
function useMapKey(helping, map) {
|
|
456
|
-
useEffect(() => {
|
|
457
|
-
if (!helping) {
|
|
458
|
-
map(false);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
const down = (event) => {
|
|
462
|
-
if (forThePage(event) && event.key.toLowerCase() === "m") {
|
|
463
|
-
event.preventDefault();
|
|
464
|
-
map(true);
|
|
465
|
-
}
|
|
466
|
-
};
|
|
467
|
-
const up = (event) => {
|
|
468
|
-
if (event.key.toLowerCase() === "m") {
|
|
469
|
-
map(false);
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
const away = () => map(false);
|
|
473
|
-
window.addEventListener("keydown", down);
|
|
474
|
-
window.addEventListener("keyup", up);
|
|
475
|
-
window.addEventListener("blur", away);
|
|
476
|
-
return () => {
|
|
477
|
-
window.removeEventListener("keydown", down);
|
|
478
|
-
window.removeEventListener("keyup", up);
|
|
479
|
-
window.removeEventListener("blur", away);
|
|
480
|
-
};
|
|
481
|
-
}, [helping]);
|
|
482
|
-
}
|
|
483
|
-
/** The keys of Lichess and chess.com: the arrows, Home and End step through the moves (T-88), f and x turn the board round. */
|
|
484
|
-
function useBrowseKeys(step, flip) {
|
|
485
|
-
useEffect(() => {
|
|
486
|
-
const press = (event) => {
|
|
487
|
-
const to = stepKeys[event.key];
|
|
488
|
-
const flips = flipKeys.includes(event.key.toLowerCase());
|
|
489
|
-
if (!forThePage(event) || (to === undefined && !flips)) {
|
|
490
|
-
return;
|
|
491
|
-
}
|
|
492
|
-
event.preventDefault();
|
|
493
|
-
if (to === undefined) {
|
|
494
|
-
flip();
|
|
495
|
-
}
|
|
496
|
-
else {
|
|
497
|
-
step(to);
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
window.addEventListener("keydown", press);
|
|
501
|
-
return () => window.removeEventListener("keydown", press);
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
/** The mouse wheel over the board steps through the moves, as on Lichess: down forward, a step a notch, however finely a touchpad turns it. */
|
|
505
|
-
function useWheelSteps(step) {
|
|
506
|
-
const turned = useRef(0);
|
|
507
|
-
return (event) => {
|
|
508
|
-
event.preventDefault();
|
|
509
|
-
turned.current += event.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? event.deltaY : event.deltaY * notch;
|
|
510
|
-
while (Math.abs(turned.current) >= notch) {
|
|
511
|
-
step(turned.current > 0 ? "forward" : "back");
|
|
512
|
-
turned.current -= Math.sign(turned.current) * notch;
|
|
513
|
-
}
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
// How far a wheel turns in one notch, in pixels.
|
|
517
|
-
const notch = 100;
|
|
518
434
|
const noSquares = [];
|
|
519
|
-
|
|
520
|
-
function forThePage(event) {
|
|
521
|
-
const typing = event.target.closest("input, textarea, select, [contenteditable], dialog") !== null;
|
|
522
|
-
return !(event.defaultPrevented || event.ctrlKey || event.altKey || event.metaKey || typing || document.documentElement.classList.contains("dragging"));
|
|
523
|
-
}
|
|
524
|
-
/** Sends the player's move to the server, which plays it as the chat would, with the words written for the AI: the reason it was refused, or undefined. */
|
|
525
|
-
function sendMove(move, words, text, name) {
|
|
526
|
-
const message = words.trim();
|
|
527
|
-
return post("/api/move", message === "" ? { move } : { move, message }, text, name);
|
|
528
|
-
}
|
|
529
|
-
/** Sends the player's words alone: the reason they were refused, or undefined. */
|
|
530
|
-
function sendMessage(words, text, name) {
|
|
531
|
-
return post("/api/message", { text: words.trim() }, text, name);
|
|
532
|
-
}
|
|
435
|
+
const noCoach = { judgments: [] };
|
|
533
436
|
/** Whether the words are a command for Claude's app, such as /clear, rather than words for the AI. */
|
|
534
437
|
function isCommand(words) {
|
|
535
438
|
return /^\s*\/[a-z]/i.test(words);
|
|
536
439
|
}
|
|
537
|
-
/**
|
|
538
|
-
* Sends a request to the server: the reason it was refused, in the player's language when the page knows the refusal (T-11, D-57), with the host's name where
|
|
539
|
-
* it sends the player, or undefined.
|
|
540
|
-
*/
|
|
541
|
-
async function post(path, body, text, name) {
|
|
542
|
-
try {
|
|
543
|
-
const response = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
544
|
-
if (response.ok) {
|
|
545
|
-
return undefined;
|
|
546
|
-
}
|
|
547
|
-
const refused = (await response.json());
|
|
548
|
-
const known = refused.refusal === undefined ? undefined : text.refused[refused.refusal];
|
|
549
|
-
return { text: known?.(refused.most ?? 0, name) ?? refused.error ?? response.statusText, words: refused.refusal?.startsWith("words-") === true };
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
return { text: String(error), words: false };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
/** Saves a PGN file in the player's downloads: a game in analysis with its review, which the board page loads back ready (T-106), or the moves alone (T-59). */
|
|
440
|
+
/** 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. */
|
|
556
441
|
function download(pgn, name) {
|
|
557
442
|
const link = document.createElement("a");
|
|
558
443
|
link.href = URL.createObjectURL(new Blob([pgn], { type: "application/x-chess-pgn" }));
|
|
@@ -570,7 +455,7 @@ function withMove(game, move) {
|
|
|
570
455
|
const copy = new Game(game.record);
|
|
571
456
|
return copy.play(move).played ? copy : game;
|
|
572
457
|
}
|
|
573
|
-
/** Whether a pawn's move takes it to the last rank, where the player chooses what it becomes
|
|
458
|
+
/** Whether a pawn's move takes it to the last rank, where the player chooses what it becomes. */
|
|
574
459
|
function promotes(piece, to) {
|
|
575
460
|
return (piece === "P" && to.endsWith("8")) || (piece === "p" && to.endsWith("1"));
|
|
576
461
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// The page's keys and wheel: [ and ] fold the side columns, M holds the helps' map (D-78), the arrows, Home and End step through the moves and f
|
|
2
|
+
// and x turn the board, and the wheel over the board steps too.
|
|
3
|
+
import { useEffect, useRef } from "preact/hooks";
|
|
4
|
+
import { flipKeys, stepKeys } from "../game/browse.js";
|
|
5
|
+
import { foldKeys, gameUnderBoard, oneColumn } from "../layout/folds.js";
|
|
6
|
+
/**
|
|
7
|
+
* `[` and `]` fold the talk and the game, as in editors with two side bars; never while the player types or drags a piece, nor in one column, nor the game
|
|
8
|
+
* under the board.
|
|
9
|
+
*/
|
|
10
|
+
export function useFoldKeys(fold) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const press = (event) => {
|
|
13
|
+
if (!forThePage(event) || oneColumn()) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const column = Object.keys(foldKeys).find((key) => foldKeys[key] === event.key);
|
|
17
|
+
if (column !== undefined && !(column === "game" && gameUnderBoard())) {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
fold(column);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
window.addEventListener("keydown", press);
|
|
23
|
+
return () => window.removeEventListener("keydown", press);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** M held shows the control map while the helps are on (D-78); it goes off when M is let go or the page loses the focus. */
|
|
27
|
+
export function useMapKey(helping, map) {
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!helping) {
|
|
30
|
+
map(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const down = (event) => {
|
|
34
|
+
if (forThePage(event) && event.key.toLowerCase() === "m") {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
map(true);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const up = (event) => {
|
|
40
|
+
if (event.key.toLowerCase() === "m") {
|
|
41
|
+
map(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const away = () => map(false);
|
|
45
|
+
window.addEventListener("keydown", down);
|
|
46
|
+
window.addEventListener("keyup", up);
|
|
47
|
+
window.addEventListener("blur", away);
|
|
48
|
+
return () => {
|
|
49
|
+
window.removeEventListener("keydown", down);
|
|
50
|
+
window.removeEventListener("keyup", up);
|
|
51
|
+
window.removeEventListener("blur", away);
|
|
52
|
+
};
|
|
53
|
+
}, [helping]);
|
|
54
|
+
}
|
|
55
|
+
/** The keys of Lichess and chess.com: the arrows, Home and End step through the moves, f and x turn the board round. */
|
|
56
|
+
export function useBrowseKeys(step, flip) {
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const press = (event) => {
|
|
59
|
+
const to = stepKeys[event.key];
|
|
60
|
+
const flips = flipKeys.includes(event.key.toLowerCase());
|
|
61
|
+
if (!forThePage(event) || (to === undefined && !flips)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
if (to === undefined) {
|
|
66
|
+
flip();
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
step(to);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
window.addEventListener("keydown", press);
|
|
73
|
+
return () => window.removeEventListener("keydown", press);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** The mouse wheel over the board steps through the moves, as on Lichess: down forward, a step a notch, however finely a touchpad turns it. */
|
|
77
|
+
export function useWheelSteps(step) {
|
|
78
|
+
const turned = useRef(0);
|
|
79
|
+
return (event) => {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
turned.current += event.deltaMode === WheelEvent.DOM_DELTA_PIXEL ? event.deltaY : event.deltaY * notch;
|
|
82
|
+
while (Math.abs(turned.current) >= notch) {
|
|
83
|
+
step(turned.current > 0 ? "forward" : "back");
|
|
84
|
+
turned.current -= Math.sign(turned.current) * notch;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// How far a wheel turns in one notch, in pixels.
|
|
89
|
+
const notch = 100;
|
|
90
|
+
/** Whether a key is the page's: not one the player types into a box or the settings, nor with a modifier, nor while a piece is in the air. */
|
|
91
|
+
function forThePage(event) {
|
|
92
|
+
const typing = event.target.closest("input, textarea, select, [contenteditable], dialog") !== null;
|
|
93
|
+
return !(event.defaultPrevented || event.ctrlKey || event.altKey || event.metaKey || typing || document.documentElement.classList.contains("dragging"));
|
|
94
|
+
}
|