chesspal 0.0.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +99 -2
  2. package/dist/cli/chat-hook.d.ts +2 -0
  3. package/dist/cli/chat-hook.js +33 -0
  4. package/dist/cli/cli.d.ts +2 -0
  5. package/dist/cli/cli.js +28 -0
  6. package/dist/cli/command-line.d.ts +7 -0
  7. package/dist/cli/command-line.js +177 -0
  8. package/dist/commands/agents.d.ts +26 -0
  9. package/dist/commands/agents.js +72 -0
  10. package/dist/commands/ai-move.d.ts +28 -0
  11. package/dist/commands/ai-move.js +85 -0
  12. package/dist/commands/ai-say.d.ts +22 -0
  13. package/dist/commands/ai-say.js +58 -0
  14. package/dist/commands/analyse-game.d.ts +65 -0
  15. package/dist/commands/analyse-game.js +265 -0
  16. package/dist/commands/annotate-game.d.ts +39 -0
  17. package/dist/commands/annotate-game.js +131 -0
  18. package/dist/commands/answer-lines.d.ts +28 -0
  19. package/dist/commands/answer-lines.js +144 -0
  20. package/dist/commands/clock.d.ts +15 -0
  21. package/dist/commands/clock.js +45 -0
  22. package/dist/commands/coach.d.ts +45 -0
  23. package/dist/commands/coach.js +344 -0
  24. package/dist/commands/context.d.ts +67 -0
  25. package/dist/commands/context.js +37 -0
  26. package/dist/commands/delete-game.d.ts +7 -0
  27. package/dist/commands/delete-game.js +30 -0
  28. package/dist/commands/delete-review.d.ts +8 -0
  29. package/dist/commands/delete-review.js +34 -0
  30. package/dist/commands/draw-offer.d.ts +10 -0
  31. package/dist/commands/draw-offer.js +43 -0
  32. package/dist/commands/drawings.d.ts +13 -0
  33. package/dist/commands/drawings.js +25 -0
  34. package/dist/commands/end-game.d.ts +23 -0
  35. package/dist/commands/end-game.js +67 -0
  36. package/dist/commands/export-game.d.ts +14 -0
  37. package/dist/commands/export-game.js +27 -0
  38. package/dist/commands/game-summary.d.ts +78 -0
  39. package/dist/commands/game-summary.js +102 -0
  40. package/dist/commands/index.d.ts +18 -0
  41. package/dist/commands/index.js +64 -0
  42. package/dist/commands/init.d.ts +15 -0
  43. package/dist/commands/init.js +73 -0
  44. package/dist/commands/lead-input.d.ts +8 -0
  45. package/dist/commands/lead-input.js +21 -0
  46. package/dist/commands/levels.d.ts +9 -0
  47. package/dist/commands/levels.js +25 -0
  48. package/dist/commands/look.d.ts +31 -0
  49. package/dist/commands/look.js +64 -0
  50. package/dist/commands/new-game.d.ts +68 -0
  51. package/dist/commands/new-game.js +218 -0
  52. package/dist/commands/opening-book.d.ts +13 -0
  53. package/dist/commands/opening-book.js +84 -0
  54. package/dist/commands/page-link.d.ts +11 -0
  55. package/dist/commands/page-link.js +28 -0
  56. package/dist/commands/past-games.d.ts +11 -0
  57. package/dist/commands/past-games.js +47 -0
  58. package/dist/commands/play-move.d.ts +37 -0
  59. package/dist/commands/play-move.js +94 -0
  60. package/dist/commands/player-move.d.ts +13 -0
  61. package/dist/commands/player-move.js +51 -0
  62. package/dist/commands/player-say.d.ts +5 -0
  63. package/dist/commands/player-say.js +16 -0
  64. package/dist/commands/ponder.d.ts +52 -0
  65. package/dist/commands/ponder.js +179 -0
  66. package/dist/commands/position.d.ts +14 -0
  67. package/dist/commands/position.js +130 -0
  68. package/dist/commands/profiles.d.ts +69 -0
  69. package/dist/commands/profiles.js +163 -0
  70. package/dist/commands/registry.d.ts +59 -0
  71. package/dist/commands/registry.js +62 -0
  72. package/dist/commands/say.d.ts +16 -0
  73. package/dist/commands/say.js +84 -0
  74. package/dist/commands/speech.d.ts +8 -0
  75. package/dist/commands/speech.js +20 -0
  76. package/dist/commands/status.d.ts +35 -0
  77. package/dist/commands/status.js +128 -0
  78. package/dist/commands/study-games.d.ts +5 -0
  79. package/dist/commands/study-games.js +40 -0
  80. package/dist/commands/study.d.ts +38 -0
  81. package/dist/commands/study.js +106 -0
  82. package/dist/commands/view.d.ts +14 -0
  83. package/dist/commands/view.js +125 -0
  84. package/dist/core/board-text.d.ts +7 -0
  85. package/dist/core/board-text.js +18 -0
  86. package/dist/core/book.json +1 -0
  87. package/dist/core/clock.d.ts +45 -0
  88. package/dist/core/clock.js +109 -0
  89. package/dist/core/facts.d.ts +55 -0
  90. package/dist/core/facts.js +175 -0
  91. package/dist/core/game.d.ts +95 -0
  92. package/dist/core/game.js +352 -0
  93. package/dist/core/illegal.d.ts +19 -0
  94. package/dist/core/illegal.js +141 -0
  95. package/dist/core/index.d.ts +20 -0
  96. package/dist/core/index.js +12 -0
  97. package/dist/core/lead.d.ts +15 -0
  98. package/dist/core/lead.js +40 -0
  99. package/dist/core/levels.d.ts +20 -0
  100. package/dist/core/levels.js +33 -0
  101. package/dist/core/material.d.ts +9 -0
  102. package/dist/core/material.js +12 -0
  103. package/dist/core/moods.d.ts +21 -0
  104. package/dist/core/moods.js +45 -0
  105. package/dist/core/openings.d.ts +35 -0
  106. package/dist/core/openings.js +98 -0
  107. package/dist/core/openings.json +1 -0
  108. package/dist/core/pgn.d.ts +10 -0
  109. package/dist/core/pgn.js +213 -0
  110. package/dist/core/profile.d.ts +117 -0
  111. package/dist/core/profile.js +232 -0
  112. package/dist/core/record.d.ts +21 -0
  113. package/dist/core/record.js +25 -0
  114. package/dist/core/review.d.ts +80 -0
  115. package/dist/core/review.js +146 -0
  116. package/dist/games/ai-model.d.ts +5 -0
  117. package/dist/games/ai-model.js +15 -0
  118. package/dist/games/analysis.d.ts +12 -0
  119. package/dist/games/analysis.js +26 -0
  120. package/dist/games/archive.d.ts +63 -0
  121. package/dist/games/archive.js +242 -0
  122. package/dist/games/chat-start.d.ts +16 -0
  123. package/dist/games/chat-start.js +26 -0
  124. package/dist/games/coach.d.ts +57 -0
  125. package/dist/games/coach.js +144 -0
  126. package/dist/games/current-game.d.ts +56 -0
  127. package/dist/games/current-game.js +134 -0
  128. package/dist/games/files.d.ts +9 -0
  129. package/dist/games/files.js +53 -0
  130. package/dist/games/index.d.ts +11 -0
  131. package/dist/games/index.js +12 -0
  132. package/dist/games/plans.d.ts +9 -0
  133. package/dist/games/plans.js +23 -0
  134. package/dist/games/table-talk.d.ts +12 -0
  135. package/dist/games/table-talk.js +25 -0
  136. package/dist/games/talk.d.ts +36 -0
  137. package/dist/games/talk.js +131 -0
  138. package/dist/games/thoughts.d.ts +15 -0
  139. package/dist/games/thoughts.js +60 -0
  140. package/dist/home/data-folder.d.ts +12 -0
  141. package/dist/home/data-folder.js +55 -0
  142. package/dist/home/index.d.ts +2 -0
  143. package/dist/home/index.js +3 -0
  144. package/dist/home/open-folder.d.ts +2 -0
  145. package/dist/home/open-folder.js +14 -0
  146. package/dist/host-files/agents.d.ts +20 -0
  147. package/dist/host-files/agents.js +53 -0
  148. package/dist/host-files/host-files.d.ts +15 -0
  149. package/dist/host-files/host-files.js +182 -0
  150. package/dist/host-files/index.d.ts +2 -0
  151. package/dist/host-files/index.js +3 -0
  152. package/dist/index.d.ts +2 -0
  153. package/dist/index.js +3 -0
  154. package/dist/page/app/anchor.js +88 -0
  155. package/dist/page/app/app.js +458 -0
  156. package/dist/page/app/keys.js +94 -0
  157. package/dist/page/app/phase.js +30 -0
  158. package/dist/page/app/served.js +73 -0
  159. package/dist/page/avatars/avatars.js +19 -0
  160. package/dist/page/avatars/beanie.webp +0 -0
  161. package/dist/page/avatars/beard.webp +0 -0
  162. package/dist/page/avatars/bun.webp +0 -0
  163. package/dist/page/avatars/cat.webp +0 -0
  164. package/dist/page/avatars/curls.webp +0 -0
  165. package/dist/page/avatars/glasses.webp +0 -0
  166. package/dist/page/avatars/moustache.webp +0 -0
  167. package/dist/page/avatars/owl.webp +0 -0
  168. package/dist/page/avatars/short-hair.webp +0 -0
  169. package/dist/page/avatars/silver-bob.webp +0 -0
  170. package/dist/page/avatars/thought.js +10 -0
  171. package/dist/page/avatars/torretta.js +38 -0
  172. package/dist/page/base.css +160 -0
  173. package/dist/page/board/board.css +959 -0
  174. package/dist/page/board/board.js +125 -0
  175. package/dist/page/board/explain.js +58 -0
  176. package/dist/page/board/marks.js +160 -0
  177. package/dist/page/board/motion.js +71 -0
  178. package/dist/page/board/pointer.js +239 -0
  179. package/dist/page/board/squares.js +26 -0
  180. package/dist/page/dialogs/analyses.js +195 -0
  181. package/dist/page/dialogs/dialog.js +26 -0
  182. package/dist/page/dialogs/dialogs.css +1228 -0
  183. package/dist/page/dialogs/new-game.js +42 -0
  184. package/dist/page/dialogs/profiles.js +152 -0
  185. package/dist/page/dialogs/search.js +31 -0
  186. package/dist/page/dialogs/settings.js +42 -0
  187. package/dist/page/dialogs/studied.js +39 -0
  188. package/dist/page/dialogs/study.js +51 -0
  189. package/dist/page/game/badges.js +50 -0
  190. package/dist/page/game/browse.js +17 -0
  191. package/dist/page/game/card-fold.js +40 -0
  192. package/dist/page/game/column.js +51 -0
  193. package/dist/page/game/ending.js +73 -0
  194. package/dist/page/game/game.css +577 -0
  195. package/dist/page/game/move-list.js +72 -0
  196. package/dist/page/game/opening-book.js +14 -0
  197. package/dist/page/game/plies.js +82 -0
  198. package/dist/page/game/result-tile.js +14 -0
  199. package/dist/page/helps/helps.css +689 -0
  200. package/dist/page/helps/helps.js +220 -0
  201. package/dist/page/helps/view.js +127 -0
  202. package/dist/page/layout/fold-tab.js +11 -0
  203. package/dist/page/layout/folds.js +43 -0
  204. package/dist/page/layout/header.js +28 -0
  205. package/dist/page/layout/layout.css +834 -0
  206. package/dist/page/lead/lead.js +82 -0
  207. package/dist/page/lead/view.js +38 -0
  208. package/dist/page/main.js +8 -0
  209. package/dist/page/pieces/CREDITS.md +19 -0
  210. package/dist/page/pieces/capablanca-small.svg +20 -0
  211. package/dist/page/pieces/capablanca.svg +22 -0
  212. package/dist/page/pieces/nitida-small.svg +20 -0
  213. package/dist/page/pieces/nitida.svg +21 -0
  214. package/dist/page/pieces/pieces.js +28 -0
  215. package/dist/page/pieces/torretta-small.svg +20 -0
  216. package/dist/page/pieces/torretta.svg +21 -0
  217. package/dist/page/players/card.js +56 -0
  218. package/dist/page/players/clock-time.js +12 -0
  219. package/dist/page/players/model-name.js +20 -0
  220. package/dist/page/profile/profile.js +99 -0
  221. package/dist/page/profile/settings.js +77 -0
  222. package/dist/page/sounds/capture.ogg +0 -0
  223. package/dist/page/sounds/check.ogg +0 -0
  224. package/dist/page/sounds/draw.ogg +0 -0
  225. package/dist/page/sounds/flag.ogg +0 -0
  226. package/dist/page/sounds/mate.ogg +0 -0
  227. package/dist/page/sounds/move.ogg +0 -0
  228. package/dist/page/sounds/offer.ogg +0 -0
  229. package/dist/page/sounds/resign.ogg +0 -0
  230. package/dist/page/sounds/sounds.js +105 -0
  231. package/dist/page/style.css +9 -0
  232. package/dist/page/talk/talk.css +958 -0
  233. package/dist/page/talk/talk.js +165 -0
  234. package/dist/page/talk/voice.js +38 -0
  235. package/dist/page/text.js +711 -0
  236. package/dist/page/theme.css +292 -0
  237. package/dist/profiles/avatar.d.ts +23 -0
  238. package/dist/profiles/avatar.js +84 -0
  239. package/dist/profiles/front-matter.d.ts +9 -0
  240. package/dist/profiles/front-matter.js +33 -0
  241. package/dist/profiles/index.d.ts +3 -0
  242. package/dist/profiles/index.js +5 -0
  243. package/dist/profiles/profiles.d.ts +69 -0
  244. package/dist/profiles/profiles.js +254 -0
  245. package/dist/profiles/settings.d.ts +12 -0
  246. package/dist/profiles/settings.js +46 -0
  247. package/dist/server/bell.d.ts +68 -0
  248. package/dist/server/bell.js +225 -0
  249. package/dist/server/caller.d.ts +21 -0
  250. package/dist/server/caller.js +132 -0
  251. package/dist/server/mcp.d.ts +13 -0
  252. package/dist/server/mcp.js +207 -0
  253. package/dist/server/page/analyses.d.ts +23 -0
  254. package/dist/server/page/analyses.js +65 -0
  255. package/dist/server/page/files.d.ts +5 -0
  256. package/dist/server/page/files.js +92 -0
  257. package/dist/server/page/game.d.ts +6 -0
  258. package/dist/server/page/game.js +381 -0
  259. package/dist/server/page/http.d.ts +29 -0
  260. package/dist/server/page/http.js +68 -0
  261. package/dist/server/page/page.d.ts +41 -0
  262. package/dist/server/page/page.js +229 -0
  263. package/dist/server/page/profiles.d.ts +19 -0
  264. package/dist/server/page/profiles.js +215 -0
  265. package/dist/version.d.ts +2 -0
  266. package/dist/version.js +3 -0
  267. package/instructions/analysis.md +4 -0
  268. package/instructions/analyst-agent.md +8 -0
  269. package/instructions/analyst.md +4 -0
  270. package/instructions/coach.md +11 -0
  271. package/instructions/opponent.md +15 -0
  272. package/instructions/ponder.md +9 -0
  273. package/package.json +66 -5
@@ -0,0 +1,239 @@
1
+ import { useEffect, useRef, useState } from "preact/hooks";
2
+ /**
3
+ * The board's pointer and keyboard, for the board turned as `columns` and `ranks` say from its top left corner: the player's pieces answer while they may
4
+ * move, the right button marks at any time.
5
+ */
6
+ export function useBoardPointer({ columns, ranks, pieces, player, selected, targets, promotion, onSelect, onMove, onRefuse, onPromote }) {
7
+ const board = useRef(null);
8
+ const press = useRef(undefined);
9
+ const [drag, setDrag] = useState(undefined);
10
+ const marking = useRef(undefined);
11
+ const [marks, setMarks] = useState(noMarks);
12
+ const [drawing, setDrawing] = useState(undefined);
13
+ const dragged = useRef(undefined);
14
+ const own = (square) => {
15
+ const piece = pieces.get(square);
16
+ return player !== undefined && piece !== undefined && (piece === piece.toUpperCase()) === (player === "white");
17
+ };
18
+ // Where a move to a square lands: there, or castling's square for a king taken onto its own rook, as on Lichess; none for a square it cannot go to.
19
+ const landing = (square) => targets.find((move) => move.to === square || rookOf(move) === square)?.to;
20
+ const isTarget = (square) => landing(square) !== undefined;
21
+ // A press on a square that is not the player's, and none of the piece picked's moves, puts the piece down, saying why it cannot go there.
22
+ const putDown = (square) => {
23
+ onSelect(undefined);
24
+ if (selected !== undefined) {
25
+ onRefuse(selected, square);
26
+ }
27
+ };
28
+ // The keyboard: the arrows move between the squares as the board is turned, Enter and Space press one as a click does, Esc puts the piece down.
29
+ const [cursor, setCursor] = useState(undefined);
30
+ const focused = cursor ?? selected ?? `${columns[0]}${ranks[7]}`;
31
+ const key = (event, square) => {
32
+ const step = arrowSteps[event.key];
33
+ if (step !== undefined) {
34
+ const column = Math.min(7, Math.max(0, columns.indexOf(square[0]) + step[0]));
35
+ const row = Math.min(7, Math.max(0, ranks.indexOf(Number(square[1])) + step[1]));
36
+ const next = `${columns[column]}${ranks[row]}`;
37
+ event.preventDefault();
38
+ setCursor(next);
39
+ board.current.querySelector(`[data-square="${next}"]`).focus();
40
+ }
41
+ else if (event.key === "Enter" || event.key === " ") {
42
+ event.preventDefault();
43
+ pressKey(square);
44
+ }
45
+ else if (event.key === "Escape" && selected !== undefined) {
46
+ onSelect(undefined);
47
+ }
48
+ };
49
+ // The focus goes back to the board after the choice of a promotion: to the pawn's square when it was cancelled, else to where it went.
50
+ const promote = (piece) => {
51
+ const back = piece === undefined ? promotion.from : promotion.to;
52
+ onPromote(piece);
53
+ setCursor(back);
54
+ requestAnimationFrame(() => board.current?.querySelector(`[data-square="${back}"]`)?.focus());
55
+ };
56
+ const pressKey = (square) => {
57
+ setMarks(noMarks);
58
+ if (player === undefined) {
59
+ return;
60
+ }
61
+ if (selected !== undefined && isTarget(square)) {
62
+ onMove(selected, landing(square));
63
+ }
64
+ else if (own(square)) {
65
+ onSelect(selected === square ? undefined : square);
66
+ }
67
+ else {
68
+ putDown(square);
69
+ }
70
+ };
71
+ // The square under a point, as the board is turned; none off the board.
72
+ const squareAt = (x, y) => {
73
+ const box = board.current.getBoundingClientRect();
74
+ const column = Math.floor((x - box.left) / (box.width / 8));
75
+ const row = Math.floor((y - box.top) / (box.height / 8));
76
+ return column >= 0 && column < 8 && row >= 0 && row < 8 ? `${columns[column]}${ranks[row]}` : undefined;
77
+ };
78
+ // Where a square's corner is in the window.
79
+ const cornerOf = (square) => {
80
+ const box = board.current.getBoundingClientRect();
81
+ const size = box.width / 8;
82
+ return [box.left + columns.indexOf(square[0]) * size, box.top + ranks.indexOf(Number(square[1])) * size];
83
+ };
84
+ const goBack = () => {
85
+ setDrag((now) => now === undefined ? undefined : { ...now, at: cornerOf(now.from), over: undefined, back: true });
86
+ };
87
+ useEffect(() => {
88
+ if (drag === undefined) {
89
+ return;
90
+ }
91
+ // Esc puts the piece back; a piece going back lands within 260 ms, whether its transition ends or not.
92
+ const escape = (event) => {
93
+ if (event.key === "Escape") {
94
+ press.current = undefined;
95
+ goBack();
96
+ }
97
+ };
98
+ const landed = drag.back ? setTimeout(() => setDrag(undefined), reducedMotion() ? 0 : 260) : undefined;
99
+ addEventListener("keydown", escape);
100
+ document.documentElement.classList.add("dragging");
101
+ return () => {
102
+ removeEventListener("keydown", escape);
103
+ document.documentElement.classList.remove("dragging");
104
+ clearTimeout(landed);
105
+ };
106
+ }, [drag?.back, drag === undefined]);
107
+ const down = (event) => {
108
+ const square = squareAt(event.clientX, event.clientY);
109
+ if (square === undefined) {
110
+ return;
111
+ }
112
+ // The right button marks a square, or draws an arrow when dragged; the left one clears them all, and moves.
113
+ if (event.button === 2) {
114
+ marking.current = { from: square, id: event.pointerId };
115
+ board.current.setPointerCapture(event.pointerId);
116
+ return;
117
+ }
118
+ if (event.button !== 0) {
119
+ return;
120
+ }
121
+ setMarks(noMarks);
122
+ if (player === undefined || drag !== undefined) {
123
+ return;
124
+ }
125
+ if (selected !== undefined && isTarget(square)) {
126
+ // A finger plays when it lifts, so a page scrolled from a dot plays nothing.
127
+ if (event.pointerType === "touch") {
128
+ press.current = { from: selected, to: landing(square), x: event.clientX, y: event.clientY, was: false, touch: true, id: event.pointerId, moved: false };
129
+ }
130
+ else {
131
+ onMove(selected, landing(square));
132
+ }
133
+ return;
134
+ }
135
+ if (!own(square)) {
136
+ putDown(square);
137
+ return;
138
+ }
139
+ press.current = { from: square, x: event.clientX, y: event.clientY, was: selected === square, touch: event.pointerType === "touch", id: event.pointerId, moved: false };
140
+ board.current.setPointerCapture(event.pointerId);
141
+ onSelect(square);
142
+ };
143
+ const moveTo = (event) => {
144
+ const mark = marking.current;
145
+ if (mark !== undefined && event.pointerId === mark.id) {
146
+ const to = squareAt(event.clientX, event.clientY);
147
+ setDrawing(to === undefined || to === mark.from ? undefined : { from: mark.from, to });
148
+ return;
149
+ }
150
+ const now = press.current;
151
+ if (now === undefined || event.pointerId !== now.id) {
152
+ return;
153
+ }
154
+ const box = board.current.getBoundingClientRect();
155
+ const size = box.width / 8;
156
+ const distance = Math.hypot(event.clientX - now.x, event.clientY - now.y);
157
+ if (now.to !== undefined) {
158
+ if (distance >= 8) {
159
+ press.current = undefined;
160
+ }
161
+ return;
162
+ }
163
+ // A press becomes a drag past 4 px of the mouse, 8 of a finger; before, it is a click.
164
+ if (drag === undefined && distance < (now.touch ? 8 : 4)) {
165
+ return;
166
+ }
167
+ now.moved ||= distance >= size / 2;
168
+ // A finger holds the piece three quarters of a square above it, and drops it where the piece is seen; until it has gone half a square, the
169
+ // piece stays on its own, so a touch never pushes it a square on by mistake.
170
+ const over = now.touch && !now.moved ? now.from : squareAt(event.clientX, now.touch ? event.clientY - 0.75 * size : event.clientY);
171
+ setDrag({ from: now.from, at: [event.clientX - size / 2, event.clientY - size / 2], over, touch: now.touch, back: false });
172
+ };
173
+ const release = (event, cancelled) => {
174
+ const mark = marking.current;
175
+ if (mark !== undefined && event.pointerId === mark.id) {
176
+ const to = squareAt(event.clientX, event.clientY);
177
+ marking.current = undefined;
178
+ setDrawing(undefined);
179
+ if (!cancelled && to !== undefined) {
180
+ setMarks((now) => toggled(now, mark.from, to));
181
+ }
182
+ return;
183
+ }
184
+ const now = press.current;
185
+ if (now === undefined || event.pointerId !== now.id) {
186
+ return;
187
+ }
188
+ press.current = undefined;
189
+ if (now.to !== undefined) {
190
+ if (!cancelled) {
191
+ onMove(now.from, now.to);
192
+ }
193
+ return;
194
+ }
195
+ if (drag === undefined) {
196
+ // A click on the piece already picked puts it down.
197
+ if (now.was) {
198
+ onSelect(undefined);
199
+ }
200
+ return;
201
+ }
202
+ const to = drag.over;
203
+ const landed = to === undefined || to === drag.from ? undefined : landing(to);
204
+ if (!cancelled && landed !== undefined) {
205
+ setDrag(undefined);
206
+ dragged.current = `${drag.from}${landed}`;
207
+ onMove(drag.from, landed);
208
+ }
209
+ else if (!cancelled && to === drag.from) {
210
+ setDrag(undefined);
211
+ }
212
+ else {
213
+ goBack();
214
+ // A game that changed under the drag may have given the turn away: no reason for a piece that may not move.
215
+ if (!cancelled && to !== undefined && to !== drag.from && player !== undefined) {
216
+ onRefuse(drag.from, to);
217
+ }
218
+ }
219
+ };
220
+ return { board, drag, marks, drawing, focused, dragged, own, isTarget, down, moveTo, release, key, pressKey, promote, setCursor };
221
+ }
222
+ const noMarks = { squares: [], arrows: [] };
223
+ /** The marks with a square, or an arrow when the squares differ, added; taken away when they were there. */
224
+ function toggled(marks, from, to) {
225
+ if (from === to) {
226
+ return { ...marks, squares: marks.squares.includes(to) ? marks.squares.filter((square) => square !== to) : [...marks.squares, to] };
227
+ }
228
+ const there = marks.arrows.some((arrow) => arrow.from === from && arrow.to === to);
229
+ return { ...marks, arrows: there ? marks.arrows.filter((arrow) => arrow.from !== from || arrow.to !== to) : [...marks.arrows, { from, to }] };
230
+ }
231
+ /** The corner of the rook a castling moves; none for another move. */
232
+ function rookOf(move) {
233
+ return move.san.startsWith("O-O-O") ? `a${move.from[1]}` : move.san.startsWith("O-O") ? `h${move.from[1]}` : undefined;
234
+ }
235
+ // How each arrow moves the keyboard's square, in columns and rows as the board is seen.
236
+ const arrowSteps = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
237
+ function reducedMotion() {
238
+ return matchMedia("(prefers-reduced-motion: reduce)").matches;
239
+ }
@@ -0,0 +1,26 @@
1
+ // The squares of a board and the pieces on them, read from a FEN.
2
+ /** The files' letters, from White's left. */
3
+ export const files = "abcdefgh";
4
+ /** The pieces of a FEN by square, as its letters: uppercase for White. */
5
+ export function piecesOf(fen) {
6
+ const pieces = new Map();
7
+ const rows = (fen.split(" ")[0] ?? "").split("/");
8
+ rows.forEach((row, index) => {
9
+ let file = 0;
10
+ for (const letter of row) {
11
+ if (/\d/.test(letter)) {
12
+ file += Number(letter);
13
+ }
14
+ else {
15
+ pieces.set(`${files[file]}${8 - index}`, letter);
16
+ file += 1;
17
+ }
18
+ }
19
+ });
20
+ return pieces;
21
+ }
22
+ /** The square of the king whose side is to move in a FEN. */
23
+ export function kingToMove(fen, pieces) {
24
+ const king = fen.split(" ")[1] === "b" ? "k" : "K";
25
+ return [...pieces].find(([, letter]) => letter === king)?.[0];
26
+ }
@@ -0,0 +1,195 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
+ // The list of the analyses (D-92): the games analysed or brought to study, found by a search, to open again, analyse again or delete, each row with its
3
+ // menu over the dialog and the question it asks in its place.
4
+ import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
5
+ import { ResultTile } from "../game/result-tile.js";
6
+ import { signature } from "../players/model-name.js";
7
+ import { searched } from "./search.js";
8
+ import { accountSide, playerIn, studiedSides } from "./studied.js";
9
+ /** The list, read anew at each opening: the search, the row that loads a game, then a row for each game found, with its menu or its question. */
10
+ export function Analyses({ onStudy, onLoad, you, accounts, ai, text }) {
11
+ const words = text.study;
12
+ const [analyses, reload] = useAnalyses();
13
+ const [query, setQuery] = useState("");
14
+ const [menu, setMenu] = useState(undefined);
15
+ const [asking, setAsking] = useState(undefined);
16
+ const [busy, setBusy] = useState(false);
17
+ const [problem, setProblem] = useState(undefined);
18
+ useClosing(menu !== undefined, () => setMenu(undefined));
19
+ useClosing(asking !== undefined, () => setAsking(undefined));
20
+ const act = async (name, asked) => {
21
+ setBusy(true);
22
+ const refused = asked === "remove" ? await deletion("/api/delete-game", name) : await deletion("/api/delete-review", name);
23
+ // Analysing again opens the game without its review: the chat's analyst goes over it (D-89).
24
+ const after = refused === undefined && asked === "redo" ? await onStudy({ game: name }) : refused;
25
+ setBusy(false);
26
+ setAsking(undefined);
27
+ setProblem(after);
28
+ reload();
29
+ };
30
+ const open = (name) => {
31
+ setBusy(true);
32
+ void onStudy({ game: name }).then((refused) => {
33
+ setBusy(false);
34
+ setProblem(refused);
35
+ });
36
+ };
37
+ const rows = analyses.map((item) => {
38
+ const named = namedSides(item, you, accounts, ai, text);
39
+ // How it went only for the player's own games: played here, or loaded and named by their name on the site (D-93).
40
+ const played = item.name.startsWith("games/") ? named.you : accountSide(tagsOf(item), accounts);
41
+ const outcome = played === undefined ? undefined : outcomeOf(item.result, played);
42
+ return { item, named, outcome };
43
+ });
44
+ const found = searched(rows, query, ({ item, named, outcome }) => searchText(item, named.sides, outcome, text));
45
+ return (_jsxs("div", { class: "analyses", children: [_jsxs("label", { class: "search", children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M10.5 17A6.5 6.5 0 1 0 10.5 4A6.5 6.5 0 0 0 10.5 17ZM15.5 15.5L20 20" }) }), _jsx("input", { type: "search", value: query, placeholder: words.search, "aria-label": words.search, spellcheck: false, onInput: (event) => setQuery(event.currentTarget.value) })] }), _jsxs("button", { class: "load-row", type: "button", onClick: onLoad, children: [_jsx("span", { class: "plus", "aria-hidden": "true", children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", children: _jsx("path", { d: "M12 5V19M5 12H19" }) }) }), _jsxs("span", { children: [_jsx("b", { children: words.bring }), _jsx("small", { children: words.bringNote })] })] }), problem === undefined ? null : _jsx("p", { class: "problem", role: "alert", children: problem }), analyses.length > 0 && found.length === 0 ? _jsx("p", { class: "none", children: words.noMatch }) : null, found.length === 0 ? null : (_jsx("ul", { class: "games", "aria-label": words.yours, children: found.map(({ item, named, outcome }) => asking?.name === item.name
46
+ ? (_jsx(Question, { asked: asking.asked, game: named.sides, when: dayOf(item.date, text.language), by: by(item), busy: busy, onYes: () => void act(item.name, asking.asked), onNo: () => setAsking(undefined), words: words }, item.name))
47
+ : (_jsxs("li", { class: "game", children: [_jsxs("button", { class: "reopen", type: "button", disabled: busy, onClick: () => open(item.name), children: [_jsx(ResultTile, { result: item.result }), _jsxs("span", { class: "what", children: [_jsx("b", { class: "who", children: named.sides }), _jsx("span", { class: "meta", children: [dayOf(item.date, text.language), words.moves(item.moves), item.opening].filter((part) => part !== undefined).join(" · ") }), item.reviewed ? _jsx(Reviewed, { by: by(item), words: words }) : null] }), _jsxs("span", { class: "score", children: [_jsx("b", { children: scoreOf(item.result) }), outcome === undefined ? _jsx("span", {}) : _jsx(OutcomeBadge, { outcome: outcome, words: words })] })] }), _jsx("span", { class: "row-actions", children: _jsx("button", { class: `icon-button${menu?.name === item.name ? " on" : ""}`, type: "button", title: words.more, "aria-label": words.more, "aria-expanded": menu?.name === item.name, disabled: busy, onClick: (event) => setMenu(menu?.name === item.name ? undefined : { name: item.name, anchor: event.currentTarget }), children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12H5.01M12 12H12.01M19 12H19.01" }) }) }) }), menu?.name === item.name
48
+ ? (_jsx(RowMenu, { anchor: menu.anchor, reviewed: item.reviewed, onPick: (asked) => {
49
+ setMenu(undefined);
50
+ setAsking({ name: item.name, asked });
51
+ }, onClose: () => setMenu(undefined), words: words }))
52
+ : null] }, item.name))) }))] }));
53
+ }
54
+ /**
55
+ * A row's menu, over the dialog and the page in the browser's top layer, under its button or above it when there is no room: analyse again and delete the
56
+ * analysis, for a game reviewed; delete the game, apart and in red. Scrolling closes it, which would leave its button.
57
+ */
58
+ function RowMenu({ anchor, reviewed, onPick, onClose, words }) {
59
+ const menu = useRef(null);
60
+ useLayoutEffect(() => {
61
+ const element = menu.current;
62
+ element.showPopover();
63
+ placeUnder(element, anchor);
64
+ element.querySelector("button")?.focus();
65
+ window.addEventListener("scroll", onClose, true);
66
+ window.addEventListener("resize", onClose);
67
+ return () => {
68
+ window.removeEventListener("scroll", onClose, true);
69
+ window.removeEventListener("resize", onClose);
70
+ };
71
+ }, []);
72
+ return (_jsxs("div", { ref: menu, class: "row-menu", popover: "manual", children: [reviewed
73
+ ? (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => onPick("redo"), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M19 12A7 7 0 1 1 16.5 6.6M17 3V7H13" }) }), _jsxs("span", { children: [words.redo, _jsx("small", { children: words.redoNote })] })] }), _jsxs("button", { type: "button", onClick: () => onPick("unreview"), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 19H19M8 15L4.5 11.5L12 4L18 10L11 17H8ZM8.5 7.5L14.5 13.5" }) }), _jsxs("span", { children: [words.unreview, _jsx("small", { children: words.unreviewNote })] })] }), _jsx("hr", {})] }))
74
+ : null, _jsxs("button", { class: "danger", type: "button", onClick: () => onPick("remove"), children: [_jsx(TrashIcon, {}), _jsx("span", { children: words.remove })] })] }));
75
+ }
76
+ /** The question a row asks in place of the row: deleting in red, analysing again in the accent. */
77
+ function Question({ asked, game, when, by, busy, onYes, onNo, words }) {
78
+ const { question, note, yes } = asked === "redo" ? words.askRedo(game, by) : asked === "unreview" ? words.askUnreview(game) : words.askRemove(game, when);
79
+ // The safe answer takes the focus, as the menu's item had it.
80
+ const cancel = useRef(null);
81
+ useEffect(() => cancel.current?.focus(), []);
82
+ return (_jsxs("li", { class: `ask ${asked === "redo" ? "redo" : "danger"}`, role: "alertdialog", "aria-label": question, children: [_jsxs("p", { children: [_jsx("b", { children: question }), _jsx("small", { children: note })] }), _jsxs("span", { class: "buttons", children: [_jsx("button", { ref: cancel, type: "button", disabled: busy, onClick: onNo, children: words.cancel }), _jsxs("button", { class: "yes", type: "button", disabled: busy, onClick: onYes, children: [asked === "remove" ? _jsx(TrashIcon, {}) : null, yes] })] })] }));
83
+ }
84
+ function TrashIcon() {
85
+ return (_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M4 7H20M9 7V4.5H15V7M6.5 7L7.5 20H16.5L17.5 7M10.5 11V16M13.5 11V16" }) }));
86
+ }
87
+ // The gap between the menu and its button, and the least margin it keeps from the window's edges.
88
+ const menuGap = 4;
89
+ const edge = 8;
90
+ /** Puts a menu under its button, its right edges aligned, or above the button when it would go past the window's bottom. */
91
+ function placeUnder(menu, anchor) {
92
+ const button = anchor.getBoundingClientRect();
93
+ const box = menu.getBoundingClientRect();
94
+ const below = button.bottom + menuGap + box.height <= window.innerHeight - edge;
95
+ menu.style.top = `${below ? button.bottom + menuGap : Math.max(edge, button.top - menuGap - box.height)}px`;
96
+ menu.style.left = `${Math.max(edge, button.right - box.width)}px`;
97
+ }
98
+ /** Who played a game listed, as its analysis names them, «Stefano – Opus 5.5 · xhigh», and the player's side when they played one. */
99
+ function namedSides(item, you, accounts, ai, text) {
100
+ const model = item.aiModel === undefined ? ai : signature(item.aiModel.model, item.aiModel.effort);
101
+ const sides = studiedSides(tagsOf(item), you, model, text.sides, accounts);
102
+ const side = playerIn(sides);
103
+ return { sides: `${sides.white.name} – ${sides.black.name}`, ...(side === undefined ? {} : { you: side }) };
104
+ }
105
+ /** What the search looks through in a row: who played, the models, the date, the opening and its variation, how it went and whether it is reviewed. */
106
+ function searchText(item, sides, outcome, text) {
107
+ const words = text.study;
108
+ const state = item.reviewed ? words.reviewed : words.unreviewed;
109
+ const went = outcome === undefined ? undefined : words.outcome[outcome];
110
+ return [sides, item.aiModel?.model, by(item), item.annotator?.model, dayOf(item.date, text.language), item.opening, scoreOf(item.result), went, state].join(" ");
111
+ }
112
+ /** How a game went for the player, as chess.com marks it: a green square with a plus, a red one with a minus, a grey one with an equals sign. */
113
+ function OutcomeBadge({ outcome, words }) {
114
+ return (_jsxs("svg", { class: `outcome ${outcome}`, viewBox: "0 0 16 16", role: "img", "aria-label": words.outcome[outcome], children: [_jsx("title", { children: words.outcome[outcome] }), _jsx("rect", { width: "16", height: "16", rx: "4" }), _jsx("path", { d: outcomeSigns[outcome] })] }));
115
+ }
116
+ /** A game the AI reviewed: its speech bubble, and the model that wrote the review when the tags say it. */
117
+ function Reviewed({ by, words }) {
118
+ return (_jsxs("span", { class: "state reviewed", title: by === undefined ? words.reviewed : `${words.reviewed}: ${by}`, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", role: "img", "aria-label": words.reviewed, children: _jsx("path", { d: "M5 5H19V15.5H11L7 19.5V15.5H5Z" }) }), by === undefined ? null : _jsx("span", { children: by })] }));
119
+ }
120
+ const outcomeSigns = { won: "M5 8h6M8 5v6", lost: "M5 8h6", drawn: "M5 6.5h6M5 9.5h6" };
121
+ /** The tags of a game listed that name its sides. */
122
+ function tagsOf(item) {
123
+ return { White: item.white, Black: item.black, ...(item.site === undefined ? {} : { Site: item.site }) };
124
+ }
125
+ /** Who reviewed a game, «Opus 5.5 · high», when its tags say it (D-81). */
126
+ function by(item) {
127
+ return item.annotator === undefined ? undefined : signature(item.annotator.model, item.annotator.effort);
128
+ }
129
+ /** A result as the row shows it: 1–0, 0–1, ½–½ or *. */
130
+ function scoreOf(result) {
131
+ return result === "1/2-1/2" ? "½–½" : result.replace("-", "–");
132
+ }
133
+ /** How a game went for the player: won, lost or drawn; nothing for a game without a result. */
134
+ function outcomeOf(result, you) {
135
+ if (result === "1/2-1/2") {
136
+ return "drawn";
137
+ }
138
+ return result === "1-0" || result === "0-1" ? (result === "1-0") === (you === "white") ? "won" : "lost" : undefined;
139
+ }
140
+ /** A PGN date, 2026.11.04, as the player reads it: «4 nov», with the year when it is not this one. */
141
+ function dayOf(date, language) {
142
+ const [, year, month, day] = /^(\d{4})\.(\d{2})\.(\d{2})$/.exec(date ?? "") ?? [];
143
+ if (year === undefined || month === undefined || day === undefined) {
144
+ return date;
145
+ }
146
+ const when = new Date(Number(year), Number(month) - 1, Number(day));
147
+ return when.toLocaleDateString(language, { day: "numeric", month: "short", ...(when.getFullYear() === new Date().getFullYear() ? {} : { year: "numeric" }) });
148
+ }
149
+ /** A deletion asked of the server: undefined once done, else why it refused. */
150
+ async function deletion(route, game) {
151
+ try {
152
+ const response = await fetch(route, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ game }) });
153
+ return response.ok ? undefined : (await response.json()).error ?? response.statusText;
154
+ }
155
+ catch (error) {
156
+ return error.message;
157
+ }
158
+ }
159
+ /** Closes a menu, or a row's question, at a click outside it or at Escape, which would otherwise close the whole dialog. */
160
+ function useClosing(open, close) {
161
+ useEffect(() => {
162
+ if (!open) {
163
+ return undefined;
164
+ }
165
+ const outside = (event) => {
166
+ if (!event.target.closest(".row-menu, .row-actions, .ask")) {
167
+ close();
168
+ }
169
+ };
170
+ const escape = (event) => {
171
+ if (event.key === "Escape") {
172
+ event.preventDefault();
173
+ close();
174
+ }
175
+ };
176
+ document.addEventListener("pointerdown", outside);
177
+ document.addEventListener("keydown", escape, true);
178
+ return () => {
179
+ document.removeEventListener("pointerdown", outside);
180
+ document.removeEventListener("keydown", escape, true);
181
+ };
182
+ }, [open]);
183
+ }
184
+ /** The analyses of the player's folder, read once the dialog opens and again on demand; none while they come, or when the server cannot give them. */
185
+ function useAnalyses() {
186
+ const [analyses, setAnalyses] = useState([]);
187
+ const [reads, setReads] = useState(0);
188
+ useEffect(() => {
189
+ void fetch("/api/analyses")
190
+ .then(async (response) => response.ok ? await response.json() : [])
191
+ .catch(() => [])
192
+ .then(setAnalyses);
193
+ }, [reads]);
194
+ return [analyses, () => setReads((count) => count + 1)];
195
+ }
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "preact/jsx-runtime";
2
+ import { useEffect, useRef } from "preact/hooks";
3
+ /** A dialog over the page, as the settings' one: Esc and a click outside close it; a step inside it goes back with the arrow beside its title. */
4
+ export function Dialog({ open, title, onBack, onClose, text, children }) {
5
+ const dialog = useRef(null);
6
+ useEffect(() => {
7
+ const element = dialog.current;
8
+ if (open && !element.open) {
9
+ element.showModal();
10
+ // The field marked autofocus takes the focus, which the dialog itself would keep.
11
+ element.querySelector("[autofocus]")?.focus();
12
+ }
13
+ else if (!open && element.open) {
14
+ element.close();
15
+ }
16
+ }, [open]);
17
+ // A click on the backdrop reaches the dialog itself, as one on its padding does: only the first is outside its box.
18
+ const closeOutside = (event) => {
19
+ const box = dialog.current.getBoundingClientRect();
20
+ const inside = event.clientX >= box.left && event.clientX <= box.right && event.clientY >= box.top && event.clientY <= box.bottom;
21
+ if (event.target === dialog.current && !inside) {
22
+ onClose();
23
+ }
24
+ };
25
+ return (_jsxs("dialog", { ref: dialog, class: "dialog", "aria-label": title, onClose: onClose, onClick: closeOutside, children: [_jsxs("header", { children: [onBack === undefined ? null : (_jsx("button", { class: "icon-button", type: "button", title: text.profile.back, "aria-label": text.profile.back, onClick: onBack, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M15 6L9 12L15 18" }) }) })), _jsx("h2", { children: title }), _jsx("button", { class: "icon-button", type: "button", title: text.close, "aria-label": text.close, onClick: onClose, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 6L18 18M18 6L6 18" }) }) })] }), open ? children : null] }));
26
+ }