chesspal 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) 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 +9 -3
  81. package/dist/core/review.js +29 -9
  82. package/dist/games/analysis.d.ts +1 -1
  83. package/dist/games/archive.d.ts +24 -3
  84. package/dist/games/archive.js +96 -22
  85. package/dist/games/coach.d.ts +57 -0
  86. package/dist/games/coach.js +144 -0
  87. package/dist/games/current-game.d.ts +2 -0
  88. package/dist/games/current-game.js +7 -1
  89. package/dist/games/files.js +1 -0
  90. package/dist/games/index.d.ts +4 -3
  91. package/dist/games/index.js +4 -3
  92. package/dist/games/talk.d.ts +1 -5
  93. package/dist/games/talk.js +14 -14
  94. package/dist/home/open-folder.js +1 -1
  95. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  96. package/dist/host-files/agents.js +53 -0
  97. package/dist/host-files/host-files.d.ts +7 -2
  98. package/dist/host-files/host-files.js +27 -12
  99. package/dist/host-files/index.d.ts +2 -2
  100. package/dist/host-files/index.js +3 -3
  101. package/dist/index.d.ts +1 -3
  102. package/dist/index.js +1 -0
  103. package/dist/page/app/anchor.js +88 -0
  104. package/dist/page/{app.js → app/app.js} +112 -230
  105. package/dist/page/app/keys.js +94 -0
  106. package/dist/page/app/phase.js +30 -0
  107. package/dist/page/app/served.js +73 -0
  108. package/dist/page/avatars/avatars.js +19 -0
  109. package/dist/page/avatars/thought.js +10 -0
  110. package/dist/page/base.css +160 -0
  111. package/dist/page/board/board.css +959 -0
  112. package/dist/page/board/board.js +125 -0
  113. package/dist/page/{explain.js → board/explain.js} +1 -1
  114. package/dist/page/board/marks.js +160 -0
  115. package/dist/page/board/motion.js +71 -0
  116. package/dist/page/board/pointer.js +239 -0
  117. package/dist/page/board/squares.js +26 -0
  118. package/dist/page/dialogs/analyses.js +195 -0
  119. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  120. package/dist/page/dialogs/dialogs.css +1228 -0
  121. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  122. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  123. package/dist/page/dialogs/search.js +31 -0
  124. package/dist/page/dialogs/settings.js +42 -0
  125. package/dist/page/dialogs/studied.js +39 -0
  126. package/dist/page/dialogs/study.js +51 -0
  127. package/dist/page/{badges.js → game/badges.js} +1 -1
  128. package/dist/page/{browse.js → game/browse.js} +1 -1
  129. package/dist/page/game/card-fold.js +40 -0
  130. package/dist/page/game/column.js +51 -0
  131. package/dist/page/{ending.js → game/ending.js} +2 -2
  132. package/dist/page/game/game.css +577 -0
  133. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  134. package/dist/page/{plies.js → game/plies.js} +30 -10
  135. package/dist/page/game/result-tile.js +14 -0
  136. package/dist/page/helps/helps.css +689 -0
  137. package/dist/page/helps/helps.js +220 -0
  138. package/dist/page/helps/view.js +127 -0
  139. package/dist/page/layout/fold-tab.js +11 -0
  140. package/dist/page/{folds.js → layout/folds.js} +1 -1
  141. package/dist/page/{header.js → layout/header.js} +5 -4
  142. package/dist/page/layout/layout.css +834 -0
  143. package/dist/page/lead/lead.js +82 -0
  144. package/dist/page/lead/view.js +38 -0
  145. package/dist/page/main.js +2 -2
  146. package/dist/page/pieces/CREDITS.md +12 -29
  147. package/dist/page/pieces/capablanca-small.svg +20 -0
  148. package/dist/page/pieces/capablanca.svg +22 -0
  149. package/dist/page/pieces/nitida-small.svg +3 -2
  150. package/dist/page/pieces/nitida.svg +3 -2
  151. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  152. package/dist/page/pieces/torretta-small.svg +15 -14
  153. package/dist/page/pieces/torretta.svg +17 -15
  154. package/dist/page/{player-card.js → players/card.js} +2 -6
  155. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  156. package/dist/page/{profile.js → profile/profile.js} +9 -5
  157. package/dist/page/{settings.js → profile/settings.js} +5 -4
  158. package/dist/page/style.css +9 -4398
  159. package/dist/page/talk/talk.css +958 -0
  160. package/dist/page/{talk.js → talk/talk.js} +39 -46
  161. package/dist/page/text.js +137 -19
  162. package/dist/page/theme.css +292 -0
  163. package/dist/profiles/index.d.ts +1 -1
  164. package/dist/profiles/profiles.d.ts +15 -4
  165. package/dist/profiles/profiles.js +24 -7
  166. package/dist/server/bell.d.ts +10 -2
  167. package/dist/server/bell.js +28 -12
  168. package/dist/server/caller.d.ts +14 -3
  169. package/dist/server/caller.js +44 -11
  170. package/dist/server/mcp.d.ts +1 -2
  171. package/dist/server/mcp.js +42 -14
  172. package/dist/server/page/analyses.d.ts +23 -0
  173. package/dist/server/page/analyses.js +65 -0
  174. package/dist/server/page/files.d.ts +5 -0
  175. package/dist/server/page/files.js +92 -0
  176. package/dist/server/page/game.d.ts +6 -0
  177. package/dist/server/page/game.js +381 -0
  178. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  179. package/dist/server/{http.js → page/http.js} +11 -0
  180. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  181. package/dist/server/page/page.js +229 -0
  182. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  183. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  184. package/instructions/analysis.md +4 -0
  185. package/instructions/analyst-agent.md +8 -0
  186. package/instructions/analyst.md +3 -7
  187. package/instructions/coach.md +11 -0
  188. package/instructions/opponent.md +4 -4
  189. package/package.json +1 -1
  190. package/dist/host-files/ponder-agents.js +0 -40
  191. package/dist/page/avatars.js +0 -16
  192. package/dist/page/board.js +0 -566
  193. package/dist/page/fold-tab.js +0 -9
  194. package/dist/page/game-column.js +0 -44
  195. package/dist/page/helps-view.js +0 -51
  196. package/dist/page/helps.js +0 -90
  197. package/dist/page/pieces/cburnett-small.svg +0 -20
  198. package/dist/page/pieces/cburnett.svg +0 -20
  199. package/dist/page/settings-panel.js +0 -32
  200. package/dist/page/studied.js +0 -14
  201. package/dist/page/study-dialog.js +0 -58
  202. package/dist/server/analyses.d.ts +0 -17
  203. package/dist/server/analyses.js +0 -46
  204. package/dist/server/page.js +0 -657
  205. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  206. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  207. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  208. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  209. /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 { aiModelIn, annotatorOf, closingOf, Game, gameReviewOf, materialOf, moodAt, movesOnly, openingOf, readPgn, suggestedLevel, timeControlText, writePgn, } from "../../core/index.js";
4
+ import { PlayerPicture } from "../avatars/avatars.js";
5
+ import { Thinker, Thought } from "../avatars/thought.js";
6
+ import { Torretta } from "../avatars/torretta.js";
7
+ import { Board } from "../board/board.js";
8
+ import { explain } from "../board/explain.js";
9
+ import { piecesOf } from "../board/squares.js";
10
+ import { NewGameDialog } from "../dialogs/new-game.js";
11
+ import { NameButton, PictureButton, PictureDialog, ProfilesDialog, Wizard } from "../dialogs/profiles.js";
12
+ import { SettingsPanel } from "../dialogs/settings.js";
13
+ import { playerIn, studiedSides } from "../dialogs/studied.js";
14
+ import { StudyDialog } from "../dialogs/study.js";
15
+ import { BadgeCounts } from "../game/badges.js";
16
+ import { stepped } from "../game/browse.js";
17
+ import { GameColumn } from "../game/column.js";
18
+ import { howEnded, useDeclined, useJustEnded, useSawEnd } from "../game/ending.js";
19
+ import { Steps } from "../game/move-list.js";
20
+ import { useOpeningBook } from "../game/opening-book.js";
21
+ import { aiSpoke, aiWords, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
22
+ import { BareButton, HelpsCard, HintLine, MapButton } from "../helps/helps.js";
23
+ import { helpsOf } from "../helps/view.js";
24
+ import { FoldTab } from "../layout/fold-tab.js";
25
+ import { nextFold, rememberFold, startFolds } from "../layout/folds.js";
26
+ import { Header } from "../layout/header.js";
27
+ import { standingsOf } from "../lead/view.js";
28
+ import { ChosenSet } from "../pieces/pieces.js";
29
+ import { ClockFace, PlayerCard } from "../players/card.js";
30
+ import { signature } from "../players/model-name.js";
31
+ import { keepSettings } from "../profile/profile.js";
32
+ import { languagesOf, saveSettings, settingsFrom, showSettings } from "../profile/settings.js";
33
+ import { endSoundOf, playSound, soundOf, wakeSounds } from "../sounds/sounds.js";
34
+ import { AiNote, AiWords, Compose, PointSquare, TalkFaces } from "../talk/talk.js";
35
+ import { textFor } from "../text.js";
36
+ import { anchorName, useAnchor } from "./anchor.js";
37
+ import { useBrowseKeys, useFoldKeys, useMapKey, useWheelSteps } from "./keys.js";
38
+ import { phaseOf, verdictOf } from "./phase.js";
39
+ import { post, sendMessage, sendMove, useServed } from "./served.js";
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,65 @@ 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;
131
104
  const material = useMemo(() => materialOf(live.position.fen), [live]);
132
105
  const pieces = useMemo(() => piecesOf(live.position.fen), [live]);
133
106
  // While the game goes on, the book's first moves carry its badge, when the player wants it, as the AI's review gives it after (D-69).
134
107
  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).
108
+ // The judgments of the player's moves, and the AI's drawings, show during a game only with the helps on, always once it is over (D-78); during it the
109
+ // coach's come from its own file, never in the PGN the opponent may read (D-88).
136
110
  const judged = !going || settings.helps;
137
- const plies = useMemo(() => withBook(judged ? pliesOf(replayed.record) : withoutReviews(pliesOf(replayed.record)), inBook), [replayed, inBook, judged]);
111
+ const coached = going ? game?.coach : undefined;
112
+ const plies = useMemo(() => {
113
+ const read = pliesOf(replayed.record);
114
+ return withBook(!judged ? withoutReviews(read) : coached === undefined ? read : withJudgments(read, coached), inBook);
115
+ }, [replayed, inBook, judged, coached]);
138
116
  const book = useOpeningBook(live.record.moves.length > 0);
139
117
  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).
118
+ // The player's move shows at once, before the server has it: the game it sends next takes its place.
141
119
  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.
120
+ // The coach's hint for the position on the board, none once the player has moved, folded until they open it; a new hint comes folded (Zoe).
121
+ const hint = pending === undefined && coached?.hint?.position === replayed.record.moves.length ? coached.hint : undefined;
122
+ const hintKey = hint === undefined ? undefined : `${hint.position} ${hint.text}`;
123
+ const [openHint, setOpenHint] = useState(undefined);
124
+ const hintOpen = hintKey !== undefined && openHint === hintKey;
125
+ const openTheHint = (open) => setOpenHint(open ? hintKey : undefined);
126
+ // The player may look back through the moves played: `viewing` how many are shown, undefined for the game as it is; a new move brings it back.
143
127
  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).
128
+ // The squares the player points at in the AI's words or the helps' card, lit on the board; the control map while it is held (D-78).
145
129
  const [pointed, setPointed] = useState(noSquares);
146
130
  const pointSquare = useMemo(() => (square) => setPointed(square === undefined ? noSquares : [square]), []);
147
131
  const [mapping, setMapping] = useState(false);
132
+ // The board without its drawings while their button is held (Stefano).
133
+ const [bare, setBare] = useState(false);
148
134
  const past = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing) }), [live, viewing]);
149
135
  const next = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing + 1) }).lastMove, [live, viewing]);
150
136
  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).
137
+ // A square lit from the AI's words goes off with the position: a touch keeps it lit until then, a mouse until it leaves the word.
152
138
  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]);
139
+ // With the helps on, the player sees all of Chesspal's facts, whatever the AI's level (D-79), in a game and in the analysis of one (Stefano).
140
+ const analysing = studied !== undefined;
141
+ // Seen from the player's side (D-91): theirs in a game, in analysis the side the game names theirs, none when it names neither.
142
+ const you = analysing ? playerIn(studiedSides(live.record.tags, served?.profile.name ?? "", name, text.sides, served?.profile.accounts)) : game?.ai === undefined ? undefined : game.ai === "white" ? "black" : "white";
143
+ const helps = useMemo(() => (going || analysing) && settings.helps ? helpsOf(shown, you) : undefined, [shown, going, analysing, settings.helps, you]);
144
+ // Who the AI saw ahead, the coach's with the helps on in a game and the analyst's in analysis, for the bar and the chart (D-90).
145
+ const drawnOnBoard = live.outcome.over && live.outcome.result === "1/2-1/2";
146
+ const standings = useMemo(() => (going && settings.helps) || analysing ? standingsOf(plies, drawnOnBoard) : undefined, [plies, drawnOnBoard, going, analysing, settings.helps]);
155
147
  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.
148
+ // A new move or a new game ends what the player was doing on the one before: the move sent, the look back, the choice of a promotion. The same game
149
+ // sent again, for the talk, the coach's words or the settings, leaves them where they are (Zoe); so does a review written on the game in analysis.
150
+ const gameKey = studied !== undefined ? studied.name : game === undefined || game === null ? game : `${game.started} ${game.pgn}`;
158
151
  useEffect(() => {
159
152
  setPending(undefined);
160
153
  setViewing(undefined);
161
154
  setPromoting(undefined);
162
- }, [studied === undefined ? game : studied.name]);
155
+ }, [gameKey]);
163
156
  const played = studied === undefined ? game : null;
164
157
  useGameSounds(played, replayed, sound);
165
158
  // Before the first game the AI speaks in the talk alone (D-77).
@@ -171,6 +164,10 @@ export function App({ start }) {
171
164
  if (next !== "open" && document.getElementById(column)?.contains(document.activeElement) === true) {
172
165
  document.getElementById(`fold-${column}`)?.focus();
173
166
  }
167
+ // The hint folds away with the game's column, so that its drawings never stay on the board without its words (Zoe).
168
+ if (column === "game" && next !== "open") {
169
+ setOpenHint(undefined);
170
+ }
174
171
  setFolds({ ...folds, [column]: next });
175
172
  rememberFold(column, next);
176
173
  };
@@ -191,6 +188,10 @@ export function App({ start }) {
191
188
  const step = (to) => goTo(stepped(to, at, total));
192
189
  useBrowseKeys(step, () => setFlipped(!flipped));
193
190
  const wheel = useWheelSteps(step);
191
+ // The move shown stays in the page's address, for a reload and a link to it; after the reset on a new game, which would undo the move it goes to. A
192
+ // game a link opens is there to look at: the analyst starts on the player's yes alone (D-89; Zoe and Federico).
193
+ const playedName = game === undefined || game === null ? undefined : anchorName(game.name);
194
+ useAnchor({ ready: served !== undefined, shown: studied !== undefined ? anchorName(studied.name) : idle ? undefined : playedName, played: playedName, going }, viewing, (plies) => goTo(plies ?? total), async (wanted) => await post("/api/study", { game: wanted, look: true }, text, name) === undefined);
194
195
  // The header's theme switch picks the other of the two the page shows, leaving the system's.
195
196
  const switchTheme = () => change({ ...settings, theme: document.documentElement.dataset.theme === "dark" ? "light" : "dark" });
196
197
  // 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 +218,10 @@ export function App({ start }) {
217
218
  }
218
219
  },
219
220
  };
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"),
221
+ const { kind, verdict } = verdictOf(phase, how, winner, offer, declined, text);
222
+ const header = (_jsx(Header, { text: text, kind: kind, verdict: verdict, ...(!ended && (offer === "ai" || declined) ? { who: name } : {}), actions: actions, onNewGame: served === undefined ? undefined : () => setDialog("new-game"),
233
223
  // Between games the new one stands out in the player's card (D-77): in the header only when the talk, and the card with it, is folded away.
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) }));
224
+ standOut: studied !== undefined || (between && folds.talk !== "open"), onStudy: served === undefined || going ? undefined : () => setDialog("study"), flipped: flipped, onFlip: () => setFlipped(!flipped), sound: sound, onSound: () => change({ ...settings, sound: !sound }), onTheme: switchTheme, onSettings: () => setSetting(true), ...(served === undefined ? {} : { profile: served.profile, onProfile: () => setDialog("profiles") }) }));
235
225
  const panel = _jsx(SettingsPanel, { open: setting, settings: settings, onChange: change, onClose: () => setSetting(false), text: text });
236
226
  if (problem !== undefined) {
237
227
  return _jsx("p", { class: "problem", children: problem });
@@ -241,12 +231,12 @@ export function App({ start }) {
241
231
  }
242
232
  // At the first start the page asks who plays (D-23), to complete the profile Chesspal made (D-71): the game comes after.
243
233
  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() });
234
+ return _jsx(Wizard, { text: text, hasGame: served.game !== null, account: served.profile.name, dataFolder: served.dataFolder, onMade: reload });
245
235
  }
246
236
  const profile = served.profile;
247
237
  const picture = _jsx(PlayerPicture, { profile: profile });
248
238
  // 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);
239
+ const sides = studied === undefined ? undefined : studiedSides(live.record.tags, profile.name, name, text.sides, profile.accounts);
250
240
  const player = sides?.bottom ?? (game === null || game.ai === "black" ? "white" : "black");
251
241
  const ai = player === "white" ? "black" : "white";
252
242
  // The Torretta's face on the move shown: the mood the AI gave, while it lasts (D-70); in analysis, of the side the AI played.
@@ -254,9 +244,11 @@ export function App({ start }) {
254
244
  const playerToMove = going && live.position.turn === player;
255
245
  const ahead = (side) => side === "white" ? material.balance : -material.balance;
256
246
  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;
247
+ // With the helps on, the coach's judgments of the player's last move, once the AI has answered it, and of one a held note let go only now (D-78, D-88).
248
+ const judgments = going && settings.helps ? judgmentsOf(plies, player, coached ?? noCoach) : [];
259
249
  const review = reviewed?.review;
250
+ // An open hint draws on the board as it is, in place of the last move's drawings.
251
+ const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
260
252
  const summary = studied === undefined ? undefined : gameReviewOf(live.record);
261
253
  const closing = studied === undefined ? undefined : closingOf(live.record);
262
254
  const annotator = studied === undefined ? undefined : annotatorOf(live.record);
@@ -268,9 +260,12 @@ export function App({ start }) {
268
260
  const clock = going ? game?.clock : undefined;
269
261
  // No chat hears the player: the page says where to wake it, and the AI's clock waits, as the server holds it.
270
262
  const asleep = served.listening === false && !offline;
263
+ // What the AI thinks on, a cloud at the Torretta's corner (Zoe): only in a game going on, and never while no chat hears the player, which the note says.
264
+ const thinking = going && !asleep && !offline && studied === undefined ? served.thinking : undefined;
265
+ const thinker = (size) => (_jsx(Thinker, { thinking: thinking, words: text.thinking, children: _jsx(Torretta, { size: size, mood: mood }) }));
271
266
  // The clocks wait for the player from the game's start: when the AI moves first, a button gives it the start; else the player's first move does (Stefano).
272
267
  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 }));
268
+ const clockOf = (side) => clock === undefined ? undefined : (_jsx(ClockFace, { clock: clock, side: side, toMove: !ended && live.position.turn === side, held: pending !== undefined || (asleep && side === ai), onFlag: reload, text: text }));
274
269
  // Where to wake the chat, under the AI's card (over the board when the talk is folded); the button that starts the clocks, under the player's (Zoe).
275
270
  const asleepNote = asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null;
276
271
  const startClocks = clockWaits
@@ -337,7 +332,7 @@ export function App({ start }) {
337
332
  clearSent(refused?.text, words);
338
333
  });
339
334
  };
340
- // The new game starts from the choices of the game before; the first, from the level that suits the player (T-58).
335
+ // The new game starts from the choices of the game before; the first, from the level that suits the player.
341
336
  const level = suggestedLevel(played === null ? undefined : replayed.record, profile.level);
342
337
  const lastControl = game?.clock?.control;
343
338
  const lastClock = lastControl === undefined ? undefined : timeControlText(lastControl);
@@ -349,8 +344,10 @@ export function App({ start }) {
349
344
  // In analysis the AI's note stays under the card on top and its word on the game over the one below, however the board is turned (D-69).
350
345
  const top = flipped ? player : ai;
351
346
  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.
347
+ const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place,
348
+ // The player changes their picture in analysis too, from their own side's card.
349
+ picture: sides[side].who === "player" ? _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("picture"), text: text }) : pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
350
+ // A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog.
354
351
  const studyGame = async (studied) => {
355
352
  const refused = await post("/api/study", studied, text, name);
356
353
  if (refused === undefined) {
@@ -358,11 +355,11 @@ export function App({ start }) {
358
355
  }
359
356
  return refused?.text;
360
357
  };
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,
358
+ return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(PictureDialog, { open: dialog === "picture", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onClose: () => setDialog(undefined), text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true ? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) }) : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
359
+ ? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
360
+ : (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined
361
+ ? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[top].name, children: [studiedCard(top, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(summary === undefined ? { reviewing: true } : {}), name: name, by: annotator, mood: mood, ...(settings.voice ? { voice: text.language } : {}), text: text })] }), _jsx("section", { class: "you-block", "aria-label": sides[bottom].name, children: studiedCard(bottom, "you") })] }))
362
+ : (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: name, signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(settings.voice ? { voice: text.language } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
366
363
  // Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
367
364
  spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
368
365
  setDraft(words);
@@ -371,9 +368,9 @@ export function App({ start }) {
371
368
  // Between games the board goes off (D-77): a game the page saw end once its effects are over, its final position less off than the table set.
372
369
  class: between && !justEnded ? "board-wrap off" : "board-wrap", onWheel: wheel,
373
370
  // 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)
371
+ onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: ended && viewing === undefined && pending === undefined ? { how, winner, player, fresh: justEnded } : undefined, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleep || clockWaits)
375
372
  ? (_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 })] })] }) }));
373
+ : null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [_jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, text: text })), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: name } : { by: name, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
377
374
  }
378
375
  /** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
379
376
  function pictureOf({ who }, profile, size, mood) {
@@ -381,7 +378,7 @@ function pictureOf({ who }, profile, size, mood) {
381
378
  }
382
379
  /**
383
380
  * Plays the sound of each move the game gains while the page watches it, the AI's and the player's, of its end without a move and of a draw offered:
384
- * none for a game loaded or begun again (T-71, T-100).
381
+ * none for a game loaded or begun again.
385
382
  */
386
383
  function useGameSounds(game, live, on) {
387
384
  const seen = useRef(undefined);
@@ -431,128 +428,13 @@ function useUnread(spoke, folded) {
431
428
  }, [spoke, folded]);
432
429
  return unread;
433
430
  }
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
431
  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
- }
432
+ const noCoach = { judgments: [] };
533
433
  /** Whether the words are a command for Claude's app, such as /clear, rather than words for the AI. */
534
434
  function isCommand(words) {
535
435
  return /^\s*\/[a-z]/i.test(words);
536
436
  }
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). */
437
+ /** Saves a PGN file in the player's downloads: a game in analysis with its review, which the board page loads back ready, or the moves alone. */
556
438
  function download(pgn, name) {
557
439
  const link = document.createElement("a");
558
440
  link.href = URL.createObjectURL(new Blob([pgn], { type: "application/x-chess-pgn" }));
@@ -570,7 +452,7 @@ function withMove(game, move) {
570
452
  const copy = new Game(game.record);
571
453
  return copy.play(move).played ? copy : game;
572
454
  }
573
- /** Whether a pawn's move takes it to the last rank, where the player chooses what it becomes (T-11). */
455
+ /** Whether a pawn's move takes it to the last rank, where the player chooses what it becomes. */
574
456
  function promotes(piece, to) {
575
457
  return (piece === "P" && to.endsWith("8")) || (piece === "p" && to.endsWith("1"));
576
458
  }
@@ -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
+ }