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.
Files changed (211) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +18 -3
  81. package/dist/core/review.js +52 -9
  82. package/dist/games/ai-model.d.ts +4 -0
  83. package/dist/games/ai-model.js +9 -0
  84. package/dist/games/analysis.d.ts +1 -1
  85. package/dist/games/archive.d.ts +25 -4
  86. package/dist/games/archive.js +99 -25
  87. package/dist/games/coach.d.ts +57 -0
  88. package/dist/games/coach.js +144 -0
  89. package/dist/games/current-game.d.ts +2 -0
  90. package/dist/games/current-game.js +7 -1
  91. package/dist/games/files.js +1 -0
  92. package/dist/games/index.d.ts +5 -4
  93. package/dist/games/index.js +5 -4
  94. package/dist/games/talk.d.ts +1 -5
  95. package/dist/games/talk.js +14 -14
  96. package/dist/home/open-folder.js +1 -1
  97. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  98. package/dist/host-files/agents.js +53 -0
  99. package/dist/host-files/host-files.d.ts +7 -2
  100. package/dist/host-files/host-files.js +27 -12
  101. package/dist/host-files/index.d.ts +2 -2
  102. package/dist/host-files/index.js +3 -3
  103. package/dist/index.d.ts +1 -3
  104. package/dist/index.js +1 -0
  105. package/dist/page/app/anchor.js +88 -0
  106. package/dist/page/{app.js → app/app.js} +116 -231
  107. package/dist/page/app/keys.js +94 -0
  108. package/dist/page/app/phase.js +30 -0
  109. package/dist/page/app/served.js +73 -0
  110. package/dist/page/avatars/avatars.js +19 -0
  111. package/dist/page/avatars/thought.js +10 -0
  112. package/dist/page/base.css +160 -0
  113. package/dist/page/board/board.css +959 -0
  114. package/dist/page/board/board.js +125 -0
  115. package/dist/page/{explain.js → board/explain.js} +1 -1
  116. package/dist/page/board/marks.js +160 -0
  117. package/dist/page/board/motion.js +71 -0
  118. package/dist/page/board/pointer.js +239 -0
  119. package/dist/page/board/squares.js +26 -0
  120. package/dist/page/dialogs/analyses.js +195 -0
  121. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  122. package/dist/page/dialogs/dialogs.css +1228 -0
  123. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  124. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  125. package/dist/page/dialogs/search.js +31 -0
  126. package/dist/page/dialogs/settings.js +42 -0
  127. package/dist/page/dialogs/studied.js +39 -0
  128. package/dist/page/dialogs/study.js +51 -0
  129. package/dist/page/{badges.js → game/badges.js} +1 -1
  130. package/dist/page/{browse.js → game/browse.js} +1 -1
  131. package/dist/page/game/card-fold.js +40 -0
  132. package/dist/page/game/column.js +55 -0
  133. package/dist/page/{ending.js → game/ending.js} +2 -2
  134. package/dist/page/game/game.css +577 -0
  135. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  136. package/dist/page/{plies.js → game/plies.js} +30 -10
  137. package/dist/page/game/result-tile.js +14 -0
  138. package/dist/page/helps/helps.css +689 -0
  139. package/dist/page/helps/helps.js +220 -0
  140. package/dist/page/helps/view.js +127 -0
  141. package/dist/page/layout/fold-tab.js +11 -0
  142. package/dist/page/{folds.js → layout/folds.js} +1 -1
  143. package/dist/page/{header.js → layout/header.js} +5 -4
  144. package/dist/page/layout/layout.css +834 -0
  145. package/dist/page/lead/lead.js +82 -0
  146. package/dist/page/lead/view.js +38 -0
  147. package/dist/page/main.js +2 -2
  148. package/dist/page/pieces/CREDITS.md +12 -29
  149. package/dist/page/pieces/capablanca-small.svg +20 -0
  150. package/dist/page/pieces/capablanca.svg +22 -0
  151. package/dist/page/pieces/nitida-small.svg +3 -2
  152. package/dist/page/pieces/nitida.svg +3 -2
  153. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  154. package/dist/page/pieces/torretta-small.svg +15 -14
  155. package/dist/page/pieces/torretta.svg +17 -15
  156. package/dist/page/{player-card.js → players/card.js} +2 -6
  157. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  158. package/dist/page/{profile.js → profile/profile.js} +9 -5
  159. package/dist/page/{settings.js → profile/settings.js} +5 -4
  160. package/dist/page/style.css +9 -4398
  161. package/dist/page/talk/talk.css +958 -0
  162. package/dist/page/{talk.js → talk/talk.js} +39 -46
  163. package/dist/page/text.js +137 -19
  164. package/dist/page/theme.css +292 -0
  165. package/dist/profiles/index.d.ts +1 -1
  166. package/dist/profiles/profiles.d.ts +15 -4
  167. package/dist/profiles/profiles.js +24 -7
  168. package/dist/server/bell.d.ts +10 -2
  169. package/dist/server/bell.js +28 -12
  170. package/dist/server/caller.d.ts +14 -3
  171. package/dist/server/caller.js +44 -11
  172. package/dist/server/mcp.d.ts +2 -3
  173. package/dist/server/mcp.js +53 -19
  174. package/dist/server/page/analyses.d.ts +23 -0
  175. package/dist/server/page/analyses.js +65 -0
  176. package/dist/server/page/files.d.ts +5 -0
  177. package/dist/server/page/files.js +92 -0
  178. package/dist/server/page/game.d.ts +6 -0
  179. package/dist/server/page/game.js +381 -0
  180. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  181. package/dist/server/{http.js → page/http.js} +11 -0
  182. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  183. package/dist/server/page/page.js +229 -0
  184. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  185. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  186. package/instructions/analysis.md +4 -0
  187. package/instructions/analyst-agent.md +8 -0
  188. package/instructions/analyst.md +3 -7
  189. package/instructions/coach.md +11 -0
  190. package/instructions/opponent.md +4 -4
  191. package/package.json +1 -1
  192. package/dist/host-files/ponder-agents.js +0 -40
  193. package/dist/page/avatars.js +0 -16
  194. package/dist/page/board.js +0 -566
  195. package/dist/page/fold-tab.js +0 -9
  196. package/dist/page/game-column.js +0 -44
  197. package/dist/page/helps-view.js +0 -51
  198. package/dist/page/helps.js +0 -90
  199. package/dist/page/pieces/cburnett-small.svg +0 -20
  200. package/dist/page/pieces/cburnett.svg +0 -20
  201. package/dist/page/settings-panel.js +0 -32
  202. package/dist/page/studied.js +0 -14
  203. package/dist/page/study-dialog.js +0 -58
  204. package/dist/server/analyses.d.ts +0 -17
  205. package/dist/server/analyses.js +0 -46
  206. package/dist/server/page.js +0 -657
  207. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  208. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  209. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  210. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  211. /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 "../core/index.js";
4
- import { PlayerPicture } from "./avatars.js";
5
- import { BadgeCounts } from "./badges.js";
6
- import { Board, piecesOf } from "./board.js";
7
- import { flipKeys, stepKeys, stepped } from "./browse.js";
8
- import { howEnded, useDeclined, useJustEnded, useSawEnd } from "./ending.js";
9
- import { explain } from "./explain.js";
10
- import { FoldTab } from "./fold-tab.js";
11
- import { foldKeys, gameUnderBoard, nextFold, oneColumn, rememberFold, startFolds } from "./folds.js";
12
- import { GameColumn } from "./game-column.js";
13
- import { Header } from "./header.js";
14
- import { helpsOf } from "./helps-view.js";
15
- import { HelpsCard, MapButton } from "./helps.js";
16
- import { signature } from "./model-name.js";
17
- import { Steps } from "./move-list.js";
18
- import { NewGameDialog } from "./new-game-dialog.js";
19
- import { useOpeningBook } from "./opening-book.js";
20
- import { ChosenSet } from "./pieces.js";
21
- import { ClockFace, PlayerCard } from "./player-card.js";
22
- import { aiSpoke, aiWords, judgmentOf, playerSpent, playerWords, pliesOf, withBook, withoutReviews } from "./plies.js";
23
- import { NameButton, PictureButton, PictureDialog, ProfilesDialog, Wizard } from "./profile-dialogs.js";
24
- import { keepSettings } from "./profile.js";
25
- import { SettingsPanel } from "./settings-panel.js";
26
- import { languagesOf, saveSettings, settingsFrom, showSettings } from "./settings.js";
27
- import { endSoundOf, playSound, soundOf, wakeSounds } from "./sounds.js";
28
- import { studiedSides } from "./studied.js";
29
- import { StudyDialog } from "./study-dialog.js";
30
- import { AiNote, AiWords, Compose, PointSquare, TalkFaces } from "./talk.js";
31
- import { textFor } from "./text.js";
32
- import { Torretta } from "./torretta.js";
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 [served, setServed] = useState(undefined);
56
- const [problem, setProblem] = useState(undefined);
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 (T-11).
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 idle = between && !sawEnd;
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 AI's judgments of the player's moves, and its drawings, show during a game only with the helps on, always once it is over (D-78).
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 plies = useMemo(() => withBook(judged ? pliesOf(replayed.record) : withoutReviews(pliesOf(replayed.record)), inBook), [replayed, inBook, judged]);
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 (T-70).
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 player may look back through the moves played (T-88): `viewing` how many are shown, undefined for the game as it is; a new move brings it back.
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 (T-111); the control map while it is held (D-78).
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 (T-111).
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 helps = useMemo(() => going && settings.helps ? helpsOf(shown) : undefined, [shown, going, settings.helps]);
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 game read again ends what the player was doing on the one before: the move sent, the look back, the choice of a promotion. A review written on the
157
- // game in analysis leaves the player where they are in it.
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
- }, [studied === undefined ? game : studied.name]);
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 header = (_jsx(Header, { text: text, kind: studied !== undefined ? "analysis" : idle ? "idle" : ended ? "over" : offer !== undefined || declined ? "offer" : undefined, verdict: studied !== undefined
221
- ? text.analysing
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: () => reload.current() });
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, name, text.sides);
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 AI's judgment of the player's last move, once it has answered it (D-78).
258
- const judgment = going && settings.helps ? judgmentOf(plies, player) : undefined;
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: () => reload.current(), text: text }));
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 (T-58).
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, picture: 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 (T-92): the page shows it once the server has it in analysis; a refusal stays in the dialog.
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, playing: going, 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, ai: name, text: text }), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(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: _jsx(Torretta, { size: 40, mood: mood }), 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: text.talk, unread: (unread || asleep) && folds.talk === "folded", 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 } : {}), 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") })] }))
365
- : (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: _jsx(Torretta, { size: 48, mood: mood }), 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 } : {}), ...(judgment === undefined ? {} : { judged: judgment }), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
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] } }), ...(review === undefined ? {} : { drawn: review }), 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, 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)
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 : (_jsx("div", { class: "board-map", children: _jsx(MapButton, { mapping: mapping, onMap: setMapping, held: false, 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, onPoint: setPointed, onMap: setMapping, mapping: mapping, text: text })), ...(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 })] })] }) }));
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 (T-71, T-100).
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
- /** 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. */
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 (T-11). */
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
+ }