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,352 @@
1
+ // A live game on chess.js: the rules, the record, and the legal moves computed once per position.
2
+ import { Chess, validateFen } from "chess.js";
3
+ import { factsOf } from "./facts.js";
4
+ import { whyIllegal } from "./illegal.js";
5
+ import { profileOf } from "./profile.js";
6
+ import { cleanComment, recordedMove } from "./record.js";
7
+ /**
8
+ * A live game: it plays moves written in SAN or as squares, and keeps the record. Checkmate, stalemate and the draws by insufficient material,
9
+ * threefold repetition and fifty moves end it (D-37); a game recorded elsewhere replays past the draws it played on from.
10
+ */
11
+ export class Game {
12
+ chess;
13
+ tags;
14
+ comment;
15
+ moves = [];
16
+ last;
17
+ // The key of every position the game has stood in, the first included, and how each side castled: for the profile's repetitions and castling.
18
+ keys;
19
+ castled = {};
20
+ keptPosition;
21
+ keptOutcome;
22
+ keptRecord;
23
+ /** The game of a record, replayed move by move; without one, a new game from the standard position. */
24
+ constructor(record = { tags: {}, moves: [] }) {
25
+ this.chess = startingBoard(record.tags.FEN);
26
+ // The first key without an en passant square no pawn can use, which the FEN may give: the position then repeats as the rules count it.
27
+ this.keys = [new Chess(this.chess.fen()).hash()];
28
+ this.tags = { ...record.tags };
29
+ this.comment = cleanComment(record.comment ?? "") || undefined;
30
+ for (const { san, nags, comment } of record.moves) {
31
+ const move = this.apply(normalized(san));
32
+ // A refused move changes nothing: the label still names its place.
33
+ if ("refusal" in move) {
34
+ throw new Error(`The record's move ${moveLabel(this.chess, san)} cannot be played: ${move.message} Fix it, or the moves before it.`);
35
+ }
36
+ this.moves.push(recordedMove(move.san, nags, comment));
37
+ }
38
+ }
39
+ /** Where the game stands now. */
40
+ get position() {
41
+ this.keptPosition ??= Object.freeze({
42
+ fen: this.chess.fen(),
43
+ key: this.chess.hash(),
44
+ turn: sideToMove(this.chess),
45
+ moveNumber: this.chess.moveNumber(),
46
+ check: this.chess.inCheck(),
47
+ });
48
+ return this.keptPosition;
49
+ }
50
+ /** Whether the board ends the game, and how. */
51
+ get outcome() {
52
+ this.keptOutcome ??= outcomeOf(this.chess);
53
+ return this.keptOutcome;
54
+ }
55
+ /** The game as plain data, frozen; when the board ends the game, its Result tag is the board's, unless the record gave one. */
56
+ get record() {
57
+ this.keptRecord ??= this.snapshot();
58
+ return this.keptRecord;
59
+ }
60
+ /** The last move played, with its squares; none before the first. */
61
+ get lastMove() {
62
+ return this.last;
63
+ }
64
+ /** What the position shows (D-26): attackers and defenders, pieces at risk, pins, checks, the moves of each piece; computed once for each position. */
65
+ get facts() {
66
+ return factsOf(this.chess, legalMovesOf(this.chess));
67
+ }
68
+ /** The positional profile (D-19): pawns, files, outposts, space, mobility, development, king safety, control, phase, draws; computed once for each position. */
69
+ get profile() {
70
+ const key = this.chess.hash();
71
+ return profileOf(this.chess, { repetitions: this.keys.filter((seen) => seen === key).length, castled: this.castled, fromFen: this.tags.FEN !== undefined }, legalMovesOf(this.chess));
72
+ }
73
+ /** The legal moves, or those of the piece on a square. */
74
+ legalMoves(square) {
75
+ const moves = legalMovesOf(this.chess);
76
+ return square === undefined ? moves : moves.filter((move) => move.from === square);
77
+ }
78
+ /** Why the piece of the side to move on a square cannot go to another (D-18); undefined when it can, or when no rule says why. */
79
+ whyNot(from, to) {
80
+ return this.legalMoves(from).some((move) => move.to === to) ? undefined : whyIllegal(this.chess, from, to);
81
+ }
82
+ /** Plays a move written in SAN (Nf3, exd5, O-O, e8=Q), with the pieces' figurines too (♘f3), or as squares (g1f3, e7e8q); a refused move changes nothing. */
83
+ play(text) {
84
+ const { outcome } = this;
85
+ if (outcome.over) {
86
+ return { played: false, ...gameOver(outcome), legal: [] };
87
+ }
88
+ return this.playRecorded(text);
89
+ }
90
+ /** Plays a move of a game recorded elsewhere, as play does, but also past a draw that game played on from: insufficient material, repetition or fifty moves. */
91
+ playRecorded(text) {
92
+ const input = normalized(text);
93
+ const move = this.apply(input);
94
+ if ("refusal" in move) {
95
+ return { played: false, ...move, legal: legalMovesOf(this.chess).map((legal) => legal.san) };
96
+ }
97
+ this.moves.push(recordedMove(move.san));
98
+ const note = checkNote(input, move.san);
99
+ return note === undefined ? { played: true, move } : { played: true, move, note };
100
+ }
101
+ /** Sets the judgment of a move by its ply, 1 for the first move recorded: the NAGs and the comment given replace its own, an empty one removes them. */
102
+ annotate(ply, annotation) {
103
+ const move = this.moves[ply - 1];
104
+ if (!Number.isInteger(ply) || move === undefined) {
105
+ const plies = this.moves.length === 0 ? "the game has no moves yet" : `give a ply from 1 to ${this.moves.length}`;
106
+ throw new RangeError(`There is no move ${ply} in this game: ${plies}.`);
107
+ }
108
+ this.moves[ply - 1] = recordedMove(move.san, annotation.nags ?? move.nags, annotation.comment ?? move.comment);
109
+ this.keptRecord = undefined;
110
+ }
111
+ apply(input) {
112
+ // chess.js plays "--" as a null move, which skips a turn.
113
+ if (input.startsWith("--")) {
114
+ return unreadable(input);
115
+ }
116
+ const mover = sideToMove(this.chess);
117
+ // Proper SAN is the fast way; anything else is matched by hand, because chess.js guesses where we must ask.
118
+ const exact = attempt(() => this.chess.move(input, { strict: true }));
119
+ const move = exact === undefined ? this.resolve(input) : toMove(exact);
120
+ if ("refusal" in move) {
121
+ return move;
122
+ }
123
+ if (exact === undefined) {
124
+ this.chess.move({ from: move.from, to: move.to, promotion: move.promotion });
125
+ }
126
+ this.last = move;
127
+ this.keys.push(this.chess.hash());
128
+ if (move.san.startsWith("O-O")) {
129
+ this.castled[mover] = move.san.startsWith("O-O-O") ? "O-O-O" : "O-O";
130
+ }
131
+ this.keptPosition = undefined;
132
+ this.keptOutcome = undefined;
133
+ this.keptRecord = undefined;
134
+ return move;
135
+ }
136
+ /** A move written loosely (as squares, with a letter too many or too few) matched against the legal moves. */
137
+ resolve(input) {
138
+ const legal = legalMovesOf(this.chess);
139
+ // No legal move: checkmate or stalemate.
140
+ if (legal.length === 0 && this.outcome.over) {
141
+ return gameOver(this.outcome);
142
+ }
143
+ const bare = input.replace(/[+#]?[!?]*$/, "");
144
+ if (/^O-O(?:-O)?$/.test(bare)) {
145
+ return this.illegal(input);
146
+ }
147
+ const parts = loosePattern.exec(bare);
148
+ if (parts === null) {
149
+ return foreignLetter(input, bare) ?? unreadable(input);
150
+ }
151
+ const [, piece, file, rank, separator, target = "", promotion] = parts;
152
+ const capture = separator === "x" || separator === ":";
153
+ // "bc4" is a bishop without its capital, or a pawn's capture without its x, as "bb4" or "cc6" is a piece or a pawn: never guessed (Anatolij, round of D-87).
154
+ if (piece === undefined && file !== undefined && rank === undefined && !capture) {
155
+ return smallLetter(input, file, target);
156
+ }
157
+ // Without a letter, a move is a pawn's, unless it gives its whole starting square.
158
+ const kind = piece?.toLowerCase() ?? (file !== undefined && rank !== undefined ? undefined : "p");
159
+ const candidates = legal.filter((move) => move.to === target
160
+ && (file === undefined || move.from[0] === file)
161
+ && (rank === undefined || move.from[1] === rank)
162
+ && (kind === undefined || this.chess.get(move.from)?.type === kind)
163
+ && (!capture || move.san.includes("x"))
164
+ && (promotion === undefined || move.promotion === promotion.toLowerCase()));
165
+ if (promotion === undefined && candidates.some((move) => move.promotion !== undefined)) {
166
+ return { refusal: "unreadable", message: `${input} takes a pawn to the last rank: say the piece it becomes, as e8=Q or e7e8q.` };
167
+ }
168
+ if (candidates.length > 1) {
169
+ return { refusal: "ambiguous", message: `${input} could be ${candidates.map((move) => move.san).sort().join(" or ")}: say which.` };
170
+ }
171
+ if (candidates.length === 0 && capture && this.chess.get(target) === undefined && !legal.some((move) => move.to === target && move.san.includes("x"))) {
172
+ return { refusal: "illegal", message: `${input} takes, but there is nothing to take on ${target}.` };
173
+ }
174
+ // An Italian, French or Spanish R is the king: said when the king could go there and no rook can.
175
+ if (candidates.length === 0 && piece === "R" && legal.some((move) => move.to === target && this.chess.get(move.from)?.type === "k")) {
176
+ return { refusal: "illegal", message: `${input} is not legal here: R is the rook in SAN, and the king is K: K${target}.` };
177
+ }
178
+ return candidates[0] ?? this.illegal(input);
179
+ }
180
+ illegal(input) {
181
+ return { refusal: "illegal", message: `${input} is not legal here, with ${this.chess.turn() === "w" ? "White" : "Black"} to move.` };
182
+ }
183
+ snapshot() {
184
+ const { outcome } = this;
185
+ // A game recorded elsewhere keeps its Result: it may have played on from a draw and ended otherwise.
186
+ const open = (this.tags.Result ?? "*") === "*";
187
+ const tags = Object.freeze(outcome.over && open ? { ...this.tags, Result: outcome.result } : { ...this.tags });
188
+ const moves = Object.freeze([...this.moves]);
189
+ return Object.freeze(this.comment === undefined ? { tags, moves } : { tags, comment: this.comment, moves });
190
+ }
191
+ }
192
+ // A piece, all or part of its square, a separator, the square it goes to, the promotion: e4, Nbd2, g1f3, e7-e8q, exd6 e.p.
193
+ const loosePattern = /^([KQRBNP])?([a-h])?([1-8])?([-x:])?([a-h][1-8])=?([QRBNqrbn])?(?:\s*e\.?p\.?)?$/;
194
+ // What word processors, phones and books write for SAN's letters and signs; the pawn's figurine says nothing.
195
+ const asciiOf = {
196
+ "–": "-",
197
+ "—": "-",
198
+ "−": "-",
199
+ "×": "x",
200
+ "♔": "K",
201
+ "♚": "K",
202
+ "♕": "Q",
203
+ "♛": "Q",
204
+ "♖": "R",
205
+ "♜": "R",
206
+ "♗": "B",
207
+ "♝": "B",
208
+ "♘": "N",
209
+ "♞": "N",
210
+ "♙": "",
211
+ "♟": "",
212
+ };
213
+ /** A text with SAN's letters and signs for the figurines and signs others write: ♘f3 as Nf3, × as x; a pawn's figurine says nothing. */
214
+ export function withSanLetters(text) {
215
+ return text.replace(/[︎️]/g, "").replace(/[–—−×♔-♟]/g, (sign) => asciiOf[sign] ?? sign);
216
+ }
217
+ // The pieces' letters of other languages that name one piece only: the Italian, French, Spanish and German ones; R is the rook in SAN.
218
+ const foreignPieces = {
219
+ C: ["knight", "N"],
220
+ S: ["knight", "N"],
221
+ A: ["bishop", "B"],
222
+ F: ["bishop", "B"],
223
+ L: ["bishop", "B"],
224
+ T: ["rook", "R"],
225
+ D: ["queen", "Q"],
226
+ };
227
+ // The legal moves of the positions seen last, by their key: about 4 KB each.
228
+ const legalByPosition = new Map();
229
+ const positionsKept = 1_000;
230
+ const goingOn = Object.freeze({ over: false });
231
+ const draws = [
232
+ ["stalemate", (chess) => chess.isStalemate()],
233
+ ["insufficient material", (chess) => chess.isInsufficientMaterial()],
234
+ ["threefold repetition", (chess) => chess.isThreefoldRepetition()],
235
+ ["fifty moves", (chess) => chess.isDrawByFiftyMoves()],
236
+ ];
237
+ function legalMovesOf(chess) {
238
+ const key = chess.hash();
239
+ let moves = legalByPosition.get(key);
240
+ if (moves === undefined) {
241
+ moves = Object.freeze(chess.moves({ verbose: true }).map(toMove));
242
+ if (legalByPosition.size >= positionsKept) {
243
+ legalByPosition.delete(legalByPosition.keys().next().value);
244
+ }
245
+ legalByPosition.set(key, moves);
246
+ }
247
+ return moves;
248
+ }
249
+ function outcomeOf(chess) {
250
+ if (chess.isCheckmate()) {
251
+ return Object.freeze({ over: true, result: chess.turn() === "w" ? "0-1" : "1-0", ending: "checkmate" });
252
+ }
253
+ const draw = draws.find(([, applies]) => applies(chess));
254
+ return draw === undefined ? goingOn : Object.freeze({ over: true, result: "1/2-1/2", ending: draw[0] });
255
+ }
256
+ function toMove({ san, from, to, promotion }) {
257
+ return Object.freeze(promotion === undefined ? { san, from, to } : { san, from, to, promotion: promotion });
258
+ }
259
+ /**
260
+ * The move in SAN: trimmed, without a move number before it, its signs and figurines in ASCII, a piece's small letter in capital (not b, a file too), ++ as
261
+ * #, castling in capital O.
262
+ */
263
+ function normalized(text) {
264
+ return text.trim()
265
+ .replace(/^\d+\s*(?:\.[\s.]*|…\s*)/, "")
266
+ .replace(/[︎️]/g, "")
267
+ // A space after the piece's letter, "N f3", as an AI may show the player how to write (Priya).
268
+ .replace(/^([A-Z♔-♟])\s+(?=[a-h1-8x:])/, "$1")
269
+ .replace(/[–—−×♔-♟]/g, (sign) => asciiOf[sign] ?? sign)
270
+ // A small t too, so that "tf1" is told the rook's R as "Tf1" is.
271
+ .replace(/^[nrqkt](?=[a-h1-8x:])/, (piece) => piece.toUpperCase())
272
+ .replace(/\+\+(?=[!?]*$)/, "#")
273
+ .replace(/^[0Oo]-[0Oo](?:-[0Oo])?(?=[+#]?[!?]*$)/, (castling) => castling.replace(/[0o]/g, "O"));
274
+ }
275
+ /** What the board says when a move was written with a check or a mate it does not give: a sign that the writer sees another position; neutral, since the AI reads it of the player's moves too. */
276
+ function checkNote(input, san) {
277
+ const written = /([+#])[!?]*$/.exec(input)?.[1];
278
+ const given = /[+#]$/.exec(san)?.[0];
279
+ if (written === undefined || written === given) {
280
+ return undefined;
281
+ }
282
+ const bare = san.replace(/[+#]$/, "");
283
+ const truth = given === undefined ? "gives no check" : given === "+" ? "is check, not mate" : "is checkmate";
284
+ return `Written as ${input}, but ${bare} ${truth}.`;
285
+ }
286
+ /** The board of a FEN tag, refused when no game can reach it; without one, the standard position. */
287
+ function startingBoard(fen) {
288
+ if (fen === undefined) {
289
+ return new Chess();
290
+ }
291
+ const { ok, error } = validateFen(fen);
292
+ const chess = ok ? new Chess(fen) : undefined;
293
+ if (chess === undefined || kingInCheckOffTurn(chess)) {
294
+ throw new Error(`The FEN tag is not a valid position (${error ?? "the side not to move is in check"}): fix it, or remove it to start from the standard position.`);
295
+ }
296
+ return chess;
297
+ }
298
+ function kingInCheckOffTurn(chess) {
299
+ const [king] = chess.findPiece({ type: "k", color: chess.turn() === "w" ? "b" : "w" });
300
+ return king !== undefined && chess.isAttacked(king, chess.turn());
301
+ }
302
+ function gameOver({ ending, result }) {
303
+ return { refusal: "over", message: `The game is over by ${ending} (${result}): no move can follow.` };
304
+ }
305
+ /** A move written with a piece's letter of another language: Cf3 for an Italian or French knight, Dd1 for a queen; none for another mistake. */
306
+ function foreignLetter(input, bare) {
307
+ const [, move = "", becomes = ""] = /^(.*[18])=?([A-Z])$/.exec(bare) ?? [];
308
+ if (becomes !== "" && !"QRBN".includes(becomes)) {
309
+ const piece = foreignPieces[becomes];
310
+ const instead = piece === undefined ? "" : ` For a ${piece[0]}, ${move}=${piece[1]}.`;
311
+ return { refusal: "unreadable", message: `"${input}" is not a move: a pawn becomes Q, R, B or N, never ${becomes}.${instead}` };
312
+ }
313
+ const letter = /^([A-Z])[a-h1-8]?[-x:]?[a-h][1-8]/.exec(bare)?.[1];
314
+ if (letter === undefined || "KQRBNP".includes(letter)) {
315
+ return undefined;
316
+ }
317
+ const piece = foreignPieces[letter];
318
+ const instead = piece === undefined ? "" : ` For a ${piece[0]}, ${piece[1]}${bare.slice(1)}.`;
319
+ // T is the Italian, French and Spanish rook, whose R is their king.
320
+ const king = letter === "T" ? " In SAN R is the rook, and K the king." : "";
321
+ return { refusal: "unreadable", message: `"${input}" is not a move: SAN writes the pieces K, Q, R, B and N, and a pawn with none, never ${letter}.${instead}${king}` };
322
+ }
323
+ /** A move that opens with a small letter and a square of another file, or "bb4": a pawn's capture without its x, a piece without its capital. */
324
+ function smallLetter(input, file, target) {
325
+ const files = Math.abs(file.charCodeAt(0) - target.charCodeAt(0));
326
+ const piece = file === "b" ? ["bishop", "B"] : foreignPieces[file.toUpperCase()];
327
+ const hints = [
328
+ files === 0 ? `The pawn's move is ${target}.` : "",
329
+ files === 1 ? `A pawn takes with an x: ${file}x${target}.` : "",
330
+ piece === undefined ? "" : `For a ${piece[0]}, ${piece[1]}${target}.`,
331
+ ].filter((hint) => hint !== "");
332
+ const said = hints.length > 0 ? hints : ["A piece is a capital letter: K, Q, R, B or N."];
333
+ return { refusal: "unreadable", message: `"${input}" is not a move: ${file} is a file. ${said.join(" ")}` };
334
+ }
335
+ /** A move that reads as none, with SAN's examples. */
336
+ function unreadable(input) {
337
+ return { refusal: "unreadable", message: `"${input}" is not a move: write it in SAN (Nf3, exd5, O-O, e8=Q) or as squares (g1f3, e7e8q).` };
338
+ }
339
+ function sideToMove(chess) {
340
+ return chess.turn() === "w" ? "white" : "black";
341
+ }
342
+ function moveLabel(chess, san) {
343
+ return `${chess.moveNumber()}${chess.turn() === "w" ? "." : "..."} ${san}`;
344
+ }
345
+ function attempt(run) {
346
+ try {
347
+ return run();
348
+ }
349
+ catch {
350
+ return undefined;
351
+ }
352
+ }
@@ -0,0 +1,19 @@
1
+ import { Chess, type Square } from "chess.js";
2
+ /**
3
+ * Why a piece cannot go to a square: a piece of its side stands there; it does not move that way; a piece stands in its way; a pawn takes only
4
+ * diagonally, goes two squares only from its first, diagonally only to take, and en passant only right after the double step; the king is in check and
5
+ * the move does not answer it; the move uncovers the king; the king would stand in check; castling with the king or that rook moved, the rook gone, a
6
+ * piece between them, the king in check, or a square it crosses or reaches attacked.
7
+ */
8
+ export type IllegalReason = "own" | "shape" | "blocked" | "pawn-straight" | "pawn-double" | "pawn-diagonal" | "en-passant-late" | "check" | "pinned" | "attacked" | "castle-lost" | "castle-rook-gone" | "castle-blocked" | "castle-check" | "castle-through";
9
+ /**
10
+ * Why a move is not legal, with the square of the piece that makes it so, when one does (the blocker, the piece giving check, the pinner, the attacker, the
11
+ * pawn too late to take), and the square it concerns, when it is not the one the piece went to: the rook's corner, the square castling crosses.
12
+ */
13
+ export interface Illegal {
14
+ readonly reason: IllegalReason;
15
+ readonly by?: Square;
16
+ readonly at?: Square;
17
+ }
18
+ /** Why the piece of the side to move on one square cannot go to another, which its legal moves do not reach; undefined when no rule says why. */
19
+ export declare function whyIllegal(chess: Chess, from: Square, to: Square): Illegal | undefined;
@@ -0,0 +1,141 @@
1
+ // Why the board refuses a move, without judging anything (D-18): the page tells the player who tried it.
2
+ import { Chess } from "chess.js";
3
+ import { fileOf, rankOf, squaresBetween } from "./facts.js";
4
+ /** Why the piece of the side to move on one square cannot go to another, which its legal moves do not reach; undefined when no rule says why. */
5
+ export function whyIllegal(chess, from, to) {
6
+ const piece = chess.get(from);
7
+ if (piece === undefined || piece.color !== chess.turn()) {
8
+ throw new RangeError(`There is no piece of the side to move on ${from}: ask why only of a piece that may move.`);
9
+ }
10
+ // A king going two squares from its first castles, as does one taken onto its own rook, though a piece of its own stands where it goes.
11
+ const castling = piece.type === "k" ? castlingTo(chess, from, to, piece.color) : undefined;
12
+ if (castling !== undefined) {
13
+ return whyNotCastling(chess, from, castling, piece.color);
14
+ }
15
+ // The way a piece moves comes first: a rook taken diagonally onto a piece of its own breaks that rule before the other.
16
+ if (piece.type !== "p" && !reaches(piece.type, from, to)) {
17
+ return { reason: "shape" };
18
+ }
19
+ if (chess.get(to)?.color === piece.color) {
20
+ return { reason: "own" };
21
+ }
22
+ const pawn = piece.type === "p" ? pawnRule(chess, from, to, piece.color) : undefined;
23
+ if (pawn !== undefined) {
24
+ return pawn;
25
+ }
26
+ const blocker = squaresBetween(from, to).find((square) => chess.get(square) !== undefined);
27
+ return blocker !== undefined && piece.type !== "n" ? { reason: "blocked", by: blocker } : kingExposed(chess, from, to, piece.color);
28
+ }
29
+ /** Whether a piece's move from one square reaches another on an empty board: pawns aside. */
30
+ function reaches(type, from, to) {
31
+ const files = Math.abs(fileOf(to) - fileOf(from));
32
+ const ranks = Math.abs(rankOf(to) - rankOf(from));
33
+ const straight = files === 0 || ranks === 0;
34
+ const diagonal = files === ranks;
35
+ switch (type) {
36
+ case "n":
37
+ return files * ranks === 2;
38
+ case "k":
39
+ return Math.max(files, ranks) === 1;
40
+ case "b":
41
+ return diagonal;
42
+ case "r":
43
+ return straight;
44
+ default:
45
+ return straight || diagonal;
46
+ }
47
+ }
48
+ /** What a pawn's own rules refuse, with the piece in its way on a double step, or the pawn it took en passant too late; undefined when they allow the move. */
49
+ function pawnRule(chess, from, to, color) {
50
+ const forward = color === "w" ? 1 : -1;
51
+ const files = Math.abs(fileOf(to) - fileOf(from));
52
+ const ranks = (rankOf(to) - rankOf(from)) * forward;
53
+ const taken = chess.get(to) !== undefined;
54
+ if (files === 1 && ranks === 1) {
55
+ // chess.js leaves the en passant square out of its FEN when no capture there is legal, unless forced.
56
+ const enPassant = chess.fen({ forceEnpassantSquare: true }).split(" ")[3];
57
+ return taken || to === enPassant ? undefined : lateEnPassant(chess, from, to, color) ?? { reason: "pawn-diagonal" };
58
+ }
59
+ if (files !== 0 || ranks < 1 || ranks > 2) {
60
+ return { reason: "shape" };
61
+ }
62
+ if (ranks === 2 && rankOf(from) !== (color === "w" ? 2 : 7)) {
63
+ return { reason: "pawn-double" };
64
+ }
65
+ const between = squaresBetween(from, to).find((square) => chess.get(square) !== undefined);
66
+ if (between !== undefined) {
67
+ return { reason: "blocked", by: between };
68
+ }
69
+ return taken ? { reason: "pawn-straight" } : undefined;
70
+ }
71
+ /** A pawn on its fifth rank taking diagonally to an empty square beside an enemy pawn: en passant, but not right after that pawn's double step. */
72
+ function lateEnPassant(chess, from, to, color) {
73
+ const beside = `${to[0]}${from[1]}`;
74
+ const pawn = chess.get(beside);
75
+ const fifth = rankOf(from) === (color === "w" ? 5 : 4);
76
+ return fifth && pawn?.type === "p" && pawn.color !== color ? { reason: "en-passant-late", by: beside } : undefined;
77
+ }
78
+ /** Why the move would leave its king in check, played on a copy: the king steps into it, a check it does not answer, or a line it uncovers. */
79
+ function kingExposed(chess, from, to, color) {
80
+ const enemy = color === "w" ? "b" : "w";
81
+ const piece = chess.get(from);
82
+ const after = new Chess(chess.fen());
83
+ const enPassant = piece.type === "p" && fileOf(from) !== fileOf(to) && after.get(to) === undefined;
84
+ after.remove(from);
85
+ after.remove(to);
86
+ after.put(piece, to);
87
+ if (enPassant) {
88
+ after.remove(`${to[0]}${from[1]}`);
89
+ }
90
+ const king = piece.type === "k" ? to : after.findPiece({ type: "k", color })[0];
91
+ const attackers = after.attackers(king, enemy);
92
+ if (attackers.length === 0) {
93
+ return undefined;
94
+ }
95
+ if (piece.type === "k") {
96
+ return { reason: "attacked", by: attackers[0] };
97
+ }
98
+ const checkers = chess.inCheck() ? chess.attackers(king, enemy) : [];
99
+ const unanswered = attackers.find((square) => checkers.includes(square));
100
+ return unanswered !== undefined ? { reason: "check", by: unanswered } : { reason: "pinned", by: attackers[0] };
101
+ }
102
+ /** The square a king's move castles to: two squares along its first rank from its own, or its own rook's corner; none for another move. */
103
+ function castlingTo(chess, from, to, color) {
104
+ const home = color === "w" ? "e1" : "e8";
105
+ if (from !== home || to[1] !== home[1]) {
106
+ return undefined;
107
+ }
108
+ const corner = to[0] === "h" ? "g" : to[0] === "a" ? "c" : undefined;
109
+ const rook = chess.get(to);
110
+ if (corner !== undefined && rook?.type === "r" && rook.color === color) {
111
+ return `${corner}${home[1]}`;
112
+ }
113
+ return Math.abs(fileOf(to) - fileOf(from)) === 2 ? to : undefined;
114
+ }
115
+ /** Why the king cannot castle: its right lost, the rook gone, a check, a piece between it and the rook, a square it crosses or reaches attacked. */
116
+ function whyNotCastling(chess, from, to, color) {
117
+ const kingside = fileOf(to) > fileOf(from);
118
+ const corner = `${kingside ? "h" : "a"}${from[1]}`;
119
+ const enemy = color === "w" ? "b" : "w";
120
+ const rook = chess.get(corner);
121
+ if (rook?.type !== "r" || rook.color !== color) {
122
+ return { reason: "castle-rook-gone", at: corner };
123
+ }
124
+ if (!chess.getCastlingRights(color)[kingside ? "k" : "q"]) {
125
+ return { reason: "castle-lost" };
126
+ }
127
+ if (chess.inCheck()) {
128
+ return { reason: "castle-check", by: chess.attackers(from, enemy)[0] };
129
+ }
130
+ const between = squaresBetween(from, corner).find((square) => chess.get(square) !== undefined);
131
+ if (between !== undefined) {
132
+ return { reason: "castle-blocked", by: between };
133
+ }
134
+ for (const square of [...squaresBetween(from, to), to]) {
135
+ const [attacker] = chess.attackers(square, enemy);
136
+ if (attacker !== undefined) {
137
+ return { reason: "castle-through", by: attacker, at: square };
138
+ }
139
+ }
140
+ return undefined;
141
+ }
@@ -0,0 +1,20 @@
1
+ export { boardText, figurinesOf } from "./board-text.ts";
2
+ export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.ts";
3
+ export type { Clock, TimeControl } from "./clock.ts";
4
+ export type { Facts, PieceFacts, PieceLetter, Pin } from "./facts.ts";
5
+ export { Game, withSanLetters } from "./game.ts";
6
+ export type { Ending, Move, Outcome, Played, Position, Promotion, Refusal, Side, Square } from "./game.ts";
7
+ export type { Illegal, IllegalReason } from "./illegal.ts";
8
+ export { type Lead, leadOf, leadText, readLead, withLead } from "./lead.ts";
9
+ export { aiLevels, layersOf, levelForPlayer, levelOf, levelTag, sees, suggestedLevel } from "./levels.ts";
10
+ export type { AiLevel, Layer } from "./levels.ts";
11
+ export { materialOf } from "./material.ts";
12
+ export type { Material } from "./material.ts";
13
+ export { endWithMood, type Mood, moodAt, type MoodNow, moodOf, moods, moodTag, withoutMoods } from "./moods.ts";
14
+ export { bookMoves, indexBook, indexOpenings, inOpenings, openingOf, outOfBookMove } from "./openings.ts";
15
+ export type { Opening, OpeningBook, OpeningIndex } from "./openings.ts";
16
+ export { pgnDate, readPgn, writePgn } from "./pgn.ts";
17
+ export type { BishopFacts, CentreSquare, KingFacts, Outpost, PawnFacts, PawnSquare, Phase, Profile, SideProfile } from "./profile.ts";
18
+ export { movesOnly } from "./record.ts";
19
+ export type { Annotation, GameRecord, RecordedMove } from "./record.ts";
20
+ export { type AiModel, aiModelIn, annotatorOf, arrowOf, type Badge, badges, closingOf, type DrawColour, drawingsIn, drawingsText, type DrawnArrow, type DrawnSquare, gameReviewOf, isReviewed, type MoveReview, reviewed, reviewOf, squareOf, unreviewed, withAiModel, withAnnotator, withGameReview, } from "./review.ts";
@@ -0,0 +1,12 @@
1
+ // The chess core: the game and its rules, the facts of a position, its record, PGN, the openings. It runs in the browser too (D-17, D-18).
2
+ export { boardText, figurinesOf } from "./board-text.js";
3
+ export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.js";
4
+ export { Game, withSanLetters } from "./game.js";
5
+ export { leadOf, leadText, readLead, withLead } from "./lead.js";
6
+ export { aiLevels, layersOf, levelForPlayer, levelOf, levelTag, sees, suggestedLevel } from "./levels.js";
7
+ export { materialOf } from "./material.js";
8
+ export { endWithMood, moodAt, moodOf, moods, moodTag, withoutMoods } from "./moods.js";
9
+ export { bookMoves, indexBook, indexOpenings, inOpenings, openingOf, outOfBookMove } from "./openings.js";
10
+ export { pgnDate, readPgn, writePgn } from "./pgn.js";
11
+ export { movesOnly } from "./record.js";
12
+ export { aiModelIn, annotatorOf, arrowOf, badges, closingOf, drawingsIn, drawingsText, gameReviewOf, isReviewed, reviewed, reviewOf, squareOf, unreviewed, withAiModel, withAnnotator, withGameReview, } from "./review.js";
@@ -0,0 +1,15 @@
1
+ import { type RecordedMove } from "./record.ts";
2
+ /** Who is ahead after a move: pawns from White's side (+1.5 White, -3 Black), or a mate in so many moves (#3 White's, #-3 Black's). */
3
+ export type Lead = {
4
+ readonly pawns: number;
5
+ } | {
6
+ readonly mate: number;
7
+ };
8
+ /** A lead as the AI writes it: +1.5, -3, 0, #3, #-2; none when it is not one. */
9
+ export declare function readLead(text: string): Lead | undefined;
10
+ /** A lead as the PGN and the page write it: +1.5, -3, 0, #3, #-2. */
11
+ export declare function leadText(lead: Lead): string;
12
+ /** The lead a move's comment carries, when the AI estimated one. */
13
+ export declare function leadOf(comment: string | undefined): Lead | undefined;
14
+ /** The move with the AI's lead in place of the one it had, beside what its comment held. */
15
+ export declare function withLead(move: RecordedMove, lead: Lead): RecordedMove;
@@ -0,0 +1,40 @@
1
+ // Who is ahead in a position, as the AI estimates it (D-90): in pawns from White's side, as chess.com shows it, or a mate it sees. It rides in a move's
2
+ // comment as [%lead +1.5] or [%lead #-3], Chesspal's own command and not [%eval], which elsewhere would pass for an engine's. Chesspal keeps it and draws
3
+ // it, never computes it (D-6, D-19).
4
+ import { recordedMove } from "./record.js";
5
+ /** A lead as the AI writes it: +1.5, -3, 0, #3, #-2; none when it is not one. */
6
+ export function readLead(text) {
7
+ const plain = text.trim().replaceAll("−", "-").replace(/^M/i, "#").replace(/^([+-])[#M]/i, "#$1");
8
+ const mate = /^#([+-]?)(\d+)$/.exec(plain);
9
+ if (mate !== null) {
10
+ const moves = Number(mate[2]);
11
+ return moves < 1 || moves > leadLimit ? undefined : { mate: mate[1] === "-" ? -moves : moves };
12
+ }
13
+ if (!/^[+-]?\d+(\.\d+)?$/.test(plain)) {
14
+ return undefined;
15
+ }
16
+ // To a tenth, a half away from zero on both sides; never a negative zero: -0.04 is an even game.
17
+ const figure = Number(plain);
18
+ const pawns = Math.sign(figure) * Math.round(Math.abs(figure) * 10) / 10;
19
+ return Math.abs(pawns) > leadLimit ? undefined : { pawns: pawns === 0 ? 0 : pawns };
20
+ }
21
+ /** A lead as the PGN and the page write it: +1.5, -3, 0, #3, #-2. */
22
+ export function leadText(lead) {
23
+ if ("mate" in lead) {
24
+ return `#${lead.mate}`;
25
+ }
26
+ return lead.pawns > 0 ? `+${lead.pawns}` : `${lead.pawns}`;
27
+ }
28
+ /** The lead a move's comment carries, when the AI estimated one. */
29
+ export function leadOf(comment) {
30
+ const found = leadPattern.exec(comment ?? "")?.[1];
31
+ return found === undefined ? undefined : readLead(found);
32
+ }
33
+ /** The move with the AI's lead in place of the one it had, beside what its comment held. */
34
+ export function withLead(move, lead) {
35
+ const comment = [(move.comment ?? "").replace(/\s*\[%lead [^\]]*\]/g, "").trim(), `[%lead ${leadText(lead)}]`].filter((part) => part !== "").join(" ");
36
+ return recordedMove(move.san, move.nags, comment);
37
+ }
38
+ const leadPattern = /\[%lead ([^\]]*)\]/;
39
+ // The largest lead Chesspal keeps, in pawns or moves to mate: beyond it the AI's figure says nothing more.
40
+ const leadLimit = 99;
@@ -0,0 +1,20 @@
1
+ import type { GameRecord } from "./record.ts";
2
+ /** The AI's levels, the weakest first. */
3
+ export declare const aiLevels: readonly ["beginner", "club", "strong", "master"];
4
+ export type AiLevel = (typeof aiLevels)[number];
5
+ /** The layers of facts: the board as the player sees it (D-59), the tactical view (D-26), the positional profile, the opening book's moves (D-15). */
6
+ declare const layers: readonly ["board", "tactics", "strategy", "theory"];
7
+ export type Layer = (typeof layers)[number];
8
+ /** The PGN tag that keeps a game's level, so the archive keeps it too. */
9
+ export declare const levelTag = "AILevel";
10
+ /** The level to suggest by how the player says they play (D-23), a step below their strength: strong only when they choose it. */
11
+ export declare const levelForPlayer: Readonly<Record<string, AiLevel>>;
12
+ /** The level of a new game the player does not choose: the game before's, else the one that suits how the player says they play, else the former one. */
13
+ export declare function suggestedLevel(before: GameRecord | undefined, playerLevel: string | undefined): AiLevel;
14
+ /** The layers a level sees, the board first. */
15
+ export declare function layersOf(level: AiLevel): readonly Layer[];
16
+ /** Whether a level sees a layer. */
17
+ export declare function sees(level: AiLevel, layer: Layer): boolean;
18
+ /** The level a game is played at, by its tag; a game without one was played before levels came. */
19
+ export declare function levelOf(record: GameRecord): AiLevel;
20
+ export {};