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,33 @@
1
+ /** The AI's levels, the weakest first. */
2
+ export const aiLevels = ["beginner", "club", "strong", "master"];
3
+ /** 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). */
4
+ const layers = ["board", "tactics", "strategy", "theory"];
5
+ /** The PGN tag that keeps a game's level, so the archive keeps it too. */
6
+ export const levelTag = "AILevel";
7
+ /** The level of the games played before levels came: the AI saw the board, the tactical view and the positional profile. */
8
+ const formerLevel = "strong";
9
+ /** The level to suggest by how the player says they play (D-23), a step below their strength: strong only when they choose it. */
10
+ export const levelForPlayer = { learning: "beginner", casual: "beginner", regular: "club", tournament: "master" };
11
+ /** 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. */
12
+ export function suggestedLevel(before, playerLevel) {
13
+ if (before !== undefined) {
14
+ return levelOf(before);
15
+ }
16
+ return playerLevel !== undefined && Object.hasOwn(levelForPlayer, playerLevel) ? levelForPlayer[playerLevel] : formerLevel;
17
+ }
18
+ /** The layers a level sees, the board first. */
19
+ export function layersOf(level) {
20
+ return layers.slice(0, aiLevels.indexOf(level) + 1);
21
+ }
22
+ /** Whether a level sees a layer. */
23
+ export function sees(level, layer) {
24
+ return layers.indexOf(layer) <= aiLevels.indexOf(level);
25
+ }
26
+ /** The level a game is played at, by its tag; a game without one was played before levels came. */
27
+ export function levelOf(record) {
28
+ const tag = record.tags[levelTag];
29
+ return isLevel(tag) ? tag : formerLevel;
30
+ }
31
+ function isLevel(text) {
32
+ return aiLevels.includes(text);
33
+ }
@@ -0,0 +1,9 @@
1
+ import type { PieceLetter } from "./facts.ts";
2
+ import type { Side } from "./game.ts";
3
+ /** What each side has taken, the most valuable first, and the material balance in pawns: above zero when White is ahead. */
4
+ export interface Material {
5
+ readonly taken: Readonly<Record<Side, readonly PieceLetter[]>>;
6
+ readonly balance: number;
7
+ }
8
+ /** The material of a FEN: the pieces each side took, from what the other has left against a full set, and the balance at 1, 3, 3, 5 and 9 pawns. */
9
+ export declare function materialOf(fen: string): Material;
@@ -0,0 +1,12 @@
1
+ /** The material of a FEN: the pieces each side took, from what the other has left against a full set, and the balance at 1, 3, 3, 5 and 9 pawns. */
2
+ export function materialOf(fen) {
3
+ const placement = fen.split(" ")[0] ?? "";
4
+ const count = (letter) => placement.split(letter).length - 1;
5
+ const takenFrom = (side) => pieces.flatMap((piece) => Array(Math.max(0, fullSet[piece] - count(side === "white" ? piece : piece.toLowerCase()))).fill(piece));
6
+ const balance = pieces.reduce((sum, piece) => sum + values[piece] * (count(piece) - count(piece.toLowerCase())), 0);
7
+ return Object.freeze({ taken: Object.freeze({ white: Object.freeze(takenFrom("black")), black: Object.freeze(takenFrom("white")) }), balance });
8
+ }
9
+ // The pieces that can be taken, the most valuable first, with how many each side starts with and their usual value in pawns.
10
+ const pieces = ["Q", "R", "B", "N", "P"];
11
+ const fullSet = { Q: 1, R: 2, B: 2, N: 2, P: 8 };
12
+ const values = { Q: 9, R: 5, B: 3, N: 3, P: 1 };
@@ -0,0 +1,21 @@
1
+ import type { Game, Side } from "./game.ts";
2
+ import type { GameRecord } from "./record.ts";
3
+ /** The moods of the AI, which the Torretta shows. */
4
+ export declare const moods: readonly ["neutral", "happy", "surprised", "worried", "thoughtful", "sad"];
5
+ /** A mood of the AI. */
6
+ export type Mood = (typeof moods)[number];
7
+ /** The AI's mood at a point of a game, and how many more of its moves keep it before it fades: 0 when its next move ends it. */
8
+ export interface MoodNow {
9
+ readonly mood: Mood;
10
+ readonly left: number;
11
+ }
12
+ /** The mood as a move's comment carries it. */
13
+ export declare function moodTag(mood: Mood): string;
14
+ /** The mood a move's comment carries, if it carries one. */
15
+ export declare function moodOf(comment: string | undefined): Mood | undefined;
16
+ /** Gives the AI's mood on the last move of a game that has ended, in place of the one it had: the face the game ends with. A game without moves keeps none. */
17
+ export declare function endWithMood(game: Game, mood: Mood): void;
18
+ /** The AI's mood after the first `plies` moves of a record: the last it gave, while it lasts, else neutral. A mood on the player's move is the one the game ended with. */
19
+ export declare function moodAt(record: GameRecord, ai: Side, plies?: number): MoodNow;
20
+ /** The record without moods: another game's are not the AI's. */
21
+ export declare function withoutMoods(record: GameRecord): GameRecord;
@@ -0,0 +1,45 @@
1
+ /** The moods of the AI, which the Torretta shows. */
2
+ export const moods = ["neutral", "happy", "surprised", "worried", "thoughtful", "sad"];
3
+ /** How many of the AI's moves each mood lasts, the one it comes with included: a surprise is a flash (Julián). */
4
+ const moodMoves = { neutral: 0, happy: 3, surprised: 1, worried: 3, thoughtful: 3, sad: 3 };
5
+ /** The mood as a move's comment carries it. */
6
+ export function moodTag(mood) {
7
+ return `[%mood ${mood}]`;
8
+ }
9
+ /** The mood a move's comment carries, if it carries one. */
10
+ export function moodOf(comment) {
11
+ const found = moodPattern.exec(comment ?? "")?.[1];
12
+ return moods.find((mood) => mood === found);
13
+ }
14
+ /** Gives the AI's mood on the last move of a game that has ended, in place of the one it had: the face the game ends with. A game without moves keeps none. */
15
+ export function endWithMood(game, mood) {
16
+ const plies = game.record.moves.length;
17
+ const comment = game.record.moves[plies - 1]?.comment?.replace(moodPattern, "");
18
+ if (plies > 0) {
19
+ game.annotate(plies, { comment: comment === undefined || comment === "" ? moodTag(mood) : `${comment} ${moodTag(mood)}` });
20
+ }
21
+ }
22
+ /** The AI's mood after the first `plies` moves of a record: the last it gave, while it lasts, else neutral. A mood on the player's move is the one the game ended with. */
23
+ export function moodAt(record, ai, plies = record.moves.length) {
24
+ // The ply count from white's move of the first move number: 1 when black moves first.
25
+ const offset = record.tags.FEN?.split(" ")[1] === "b" ? 1 : 0;
26
+ const aiMoves = (index) => ((index + offset) % 2 === 0 ? "white" : "black") === ai;
27
+ let since = 0;
28
+ for (let index = Math.min(plies, record.moves.length) - 1; index >= 0; index--) {
29
+ const mood = moodOf(record.moves[index]?.comment);
30
+ if (mood !== undefined) {
31
+ const left = moodMoves[mood] - 1 - since;
32
+ return mood === "neutral" || left < 0 ? neutral : { mood, left };
33
+ }
34
+ if (aiMoves(index)) {
35
+ since++;
36
+ }
37
+ }
38
+ return neutral;
39
+ }
40
+ /** The record without moods: another game's are not the AI's. */
41
+ export function withoutMoods(record) {
42
+ return { ...record, moves: record.moves.map((move) => ({ ...move, comment: move.comment?.replace(moodPattern, "") })) };
43
+ }
44
+ const neutral = { mood: "neutral", left: 0 };
45
+ const moodPattern = /\s*\[%mood ([a-z]+)\]/;
@@ -0,0 +1,35 @@
1
+ import type { GameRecord } from "./record.ts";
2
+ /** The openings by position: the position's key (its FEN without the move counters) to its ECO code and name. */
3
+ export type OpeningIndex = Readonly<Record<string, readonly [eco: string, name: string]>>;
4
+ /** The opening a game is in, or the last one it was in: its ECO code, its name split in family and variation, and the ply that reached it. */
5
+ export interface Opening {
6
+ readonly eco: string;
7
+ /** The whole name, as Lichess writes it: Sicilian Defense: Najdorf Variation, English Attack. */
8
+ readonly name: string;
9
+ /** The name before the colon: Sicilian Defense. */
10
+ readonly family: string;
11
+ /** The name after the colon, when there is one: Najdorf Variation, English Attack. */
12
+ readonly variation?: string;
13
+ /** How many plies the game had when it reached the opening's position. */
14
+ readonly ply: number;
15
+ /** Whether the game's position now is a named one; when not, it left the book at the ply after `ply`. */
16
+ readonly inBook: boolean;
17
+ }
18
+ /** The book's moves: each position on a line of Lichess's files to the moves its lines play from it, the theory the AI knows at the master level (D-15, D-60). */
19
+ export type OpeningBook = Readonly<Record<string, readonly string[]>>;
20
+ /** The index of the openings in Lichess's TSV files (eco, name, pgn), each line keyed by the position its moves reach. */
21
+ export declare function indexOpenings(tsv: string): Record<string, [string, string]>;
22
+ /**
23
+ * The book of Lichess's TSV files: every position on their lines, keyed as the index keys them, to the moves the lines play from it, the move most lines go
24
+ * through first: how much theory a move has, never how good it is. A move under one line in fifty from its position is left out; one whose lines are all named
25
+ * traps carries the trap's name, as the files write it: Nxe5 (Mortimer Trap).
26
+ */
27
+ export declare function indexBook(tsv: string): Record<string, string[]>;
28
+ /** The moves the book knows from a position, in SAN; none out of the book. */
29
+ export declare function bookMoves(fen: string, book: OpeningBook): readonly string[];
30
+ /** Whether a line of the book reaches a position, by any order of moves: one its lines play on from, or one an opening is named after. */
31
+ export declare function inOpenings(fen: string, index: OpeningIndex, book: OpeningBook): boolean;
32
+ /** The opening of a game from the standard position, the last named one its moves reached; none before the first, or from another position. */
33
+ export declare function openingOf(record: GameRecord, index: OpeningIndex): Opening | undefined;
34
+ /** The move number at which a game that reached an opening at `ply` left the book. */
35
+ export declare function outOfBookMove(opening: Opening): number;
@@ -0,0 +1,98 @@
1
+ // The openings by position (D-15, D-16): Lichess's names, indexed by the position each line reaches, so transpositions find them too.
2
+ import { Chess } from "chess.js";
3
+ /** No line of the book is longer: past it, no position is looked up. */
4
+ const bookDepth = 40;
5
+ /** The index of the openings in Lichess's TSV files (eco, name, pgn), each line keyed by the position its moves reach. */
6
+ export function indexOpenings(tsv) {
7
+ const index = {};
8
+ for (const { eco, name, moves } of linesOf(tsv)) {
9
+ const chess = new Chess();
10
+ moves.forEach((move) => chess.move(move));
11
+ index[positionKey(chess.fen())] = [eco, name];
12
+ }
13
+ return index;
14
+ }
15
+ /**
16
+ * The book of Lichess's TSV files: every position on their lines, keyed as the index keys them, to the moves the lines play from it, the move most lines go
17
+ * through first: how much theory a move has, never how good it is. A move under one line in fifty from its position is left out; one whose lines are all named
18
+ * traps carries the trap's name, as the files write it: Nxe5 (Mortimer Trap).
19
+ */
20
+ export function indexBook(tsv) {
21
+ const lines = new Map();
22
+ for (const { name, moves } of linesOf(tsv)) {
23
+ const trap = name.split(/[:,]/).map((part) => part.trim()).find((part) => /\bTrap\b/.test(part));
24
+ const chess = new Chess();
25
+ for (const move of moves) {
26
+ const key = positionKey(chess.fen());
27
+ const san = chess.move(move).san;
28
+ const counts = lines.get(key) ?? new Map();
29
+ const count = counts.get(san) ?? { lines: 0, traps: [] };
30
+ counts.set(san, { lines: count.lines + 1, traps: trap === undefined ? count.traps : [...count.traps, trap] });
31
+ lines.set(key, counts);
32
+ }
33
+ }
34
+ return Object.fromEntries([...lines].map(([key, counts]) => [key, bookOf(counts)]));
35
+ }
36
+ /** The moves the book knows from a position, in SAN; none out of the book. */
37
+ export function bookMoves(fen, book) {
38
+ return book[positionKey(fen)] ?? [];
39
+ }
40
+ /** Whether a line of the book reaches a position, by any order of moves: one its lines play on from, or one an opening is named after. */
41
+ export function inOpenings(fen, index, book) {
42
+ const key = positionKey(fen);
43
+ return book[key] !== undefined || index[key] !== undefined;
44
+ }
45
+ /** The opening of a game from the standard position, the last named one its moves reached; none before the first, or from another position. */
46
+ export function openingOf(record, index) {
47
+ if (record.tags.FEN !== undefined && record.tags.FEN !== new Chess().fen()) {
48
+ return undefined;
49
+ }
50
+ const chess = new Chess();
51
+ const plies = Math.min(record.moves.length, bookDepth);
52
+ let found;
53
+ for (let ply = 1; ply <= plies; ply++) {
54
+ chess.move(record.moves[ply - 1].san);
55
+ const named = index[positionKey(chess.fen())];
56
+ if (named !== undefined) {
57
+ found = { named, ply };
58
+ }
59
+ }
60
+ if (found === undefined) {
61
+ return undefined;
62
+ }
63
+ const [eco, name] = found.named;
64
+ const colon = name.indexOf(":");
65
+ const family = colon < 0 ? name : name.slice(0, colon);
66
+ const opening = { eco, name, family, ply: found.ply, inBook: found.ply === record.moves.length };
67
+ return colon < 0 ? opening : { ...opening, variation: name.slice(colon + 1).trim() };
68
+ }
69
+ /** The move number at which a game that reached an opening at `ply` left the book. */
70
+ export function outOfBookMove(opening) {
71
+ return Math.floor(opening.ply / 2) + 1;
72
+ }
73
+ /** The moves from a position that at least one line in fifty plays, the most played first, with the trap's name when all their lines are traps. */
74
+ function bookOf(counts) {
75
+ const total = [...counts.values()].reduce((sum, count) => sum + count.lines, 0);
76
+ return [...counts]
77
+ .filter(([, count]) => count.lines * 50 >= total)
78
+ .sort(([, one], [, other]) => other.lines - one.lines)
79
+ .map(([san, count]) => count.traps.length === count.lines ? `${san} (${[...new Set(count.traps)].join(" / ")})` : san);
80
+ }
81
+ /** The lines of Lichess's TSV files, their moves in SAN; one past the book's depth throws. */
82
+ function linesOf(tsv) {
83
+ return tsv.split(/\r?\n/).flatMap((line) => {
84
+ const [eco, name, pgn] = line.split("\t");
85
+ if (eco === undefined || name === undefined || pgn === undefined || eco === "eco") {
86
+ return [];
87
+ }
88
+ const moves = pgn.split(/\s+/).filter((token) => token !== "" && !/^\d+\.+$/.test(token));
89
+ if (moves.length > bookDepth) {
90
+ throw new Error(`The opening ${eco} ${name} is ${moves.length} plies long, past the book's depth of ${bookDepth}: raise bookDepth.`);
91
+ }
92
+ return [{ eco, name, moves }];
93
+ });
94
+ }
95
+ /** A position without its move counters: the same position reached in another order is the same key. */
96
+ function positionKey(fen) {
97
+ return fen.split(" ").slice(0, 4).join(" ");
98
+ }