chesspal 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +9 -3
  81. package/dist/core/review.js +29 -9
  82. package/dist/games/analysis.d.ts +1 -1
  83. package/dist/games/archive.d.ts +24 -3
  84. package/dist/games/archive.js +96 -22
  85. package/dist/games/coach.d.ts +57 -0
  86. package/dist/games/coach.js +144 -0
  87. package/dist/games/current-game.d.ts +2 -0
  88. package/dist/games/current-game.js +7 -1
  89. package/dist/games/files.js +1 -0
  90. package/dist/games/index.d.ts +4 -3
  91. package/dist/games/index.js +4 -3
  92. package/dist/games/talk.d.ts +1 -5
  93. package/dist/games/talk.js +14 -14
  94. package/dist/home/open-folder.js +1 -1
  95. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  96. package/dist/host-files/agents.js +53 -0
  97. package/dist/host-files/host-files.d.ts +7 -2
  98. package/dist/host-files/host-files.js +27 -12
  99. package/dist/host-files/index.d.ts +2 -2
  100. package/dist/host-files/index.js +3 -3
  101. package/dist/index.d.ts +1 -3
  102. package/dist/index.js +1 -0
  103. package/dist/page/app/anchor.js +88 -0
  104. package/dist/page/{app.js → app/app.js} +112 -230
  105. package/dist/page/app/keys.js +94 -0
  106. package/dist/page/app/phase.js +30 -0
  107. package/dist/page/app/served.js +73 -0
  108. package/dist/page/avatars/avatars.js +19 -0
  109. package/dist/page/avatars/thought.js +10 -0
  110. package/dist/page/base.css +160 -0
  111. package/dist/page/board/board.css +959 -0
  112. package/dist/page/board/board.js +125 -0
  113. package/dist/page/{explain.js → board/explain.js} +1 -1
  114. package/dist/page/board/marks.js +160 -0
  115. package/dist/page/board/motion.js +71 -0
  116. package/dist/page/board/pointer.js +239 -0
  117. package/dist/page/board/squares.js +26 -0
  118. package/dist/page/dialogs/analyses.js +195 -0
  119. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  120. package/dist/page/dialogs/dialogs.css +1228 -0
  121. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  122. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  123. package/dist/page/dialogs/search.js +31 -0
  124. package/dist/page/dialogs/settings.js +42 -0
  125. package/dist/page/dialogs/studied.js +39 -0
  126. package/dist/page/dialogs/study.js +51 -0
  127. package/dist/page/{badges.js → game/badges.js} +1 -1
  128. package/dist/page/{browse.js → game/browse.js} +1 -1
  129. package/dist/page/game/card-fold.js +40 -0
  130. package/dist/page/game/column.js +51 -0
  131. package/dist/page/{ending.js → game/ending.js} +2 -2
  132. package/dist/page/game/game.css +577 -0
  133. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  134. package/dist/page/{plies.js → game/plies.js} +30 -10
  135. package/dist/page/game/result-tile.js +14 -0
  136. package/dist/page/helps/helps.css +689 -0
  137. package/dist/page/helps/helps.js +220 -0
  138. package/dist/page/helps/view.js +127 -0
  139. package/dist/page/layout/fold-tab.js +11 -0
  140. package/dist/page/{folds.js → layout/folds.js} +1 -1
  141. package/dist/page/{header.js → layout/header.js} +5 -4
  142. package/dist/page/layout/layout.css +834 -0
  143. package/dist/page/lead/lead.js +82 -0
  144. package/dist/page/lead/view.js +38 -0
  145. package/dist/page/main.js +2 -2
  146. package/dist/page/pieces/CREDITS.md +12 -29
  147. package/dist/page/pieces/capablanca-small.svg +20 -0
  148. package/dist/page/pieces/capablanca.svg +22 -0
  149. package/dist/page/pieces/nitida-small.svg +3 -2
  150. package/dist/page/pieces/nitida.svg +3 -2
  151. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  152. package/dist/page/pieces/torretta-small.svg +15 -14
  153. package/dist/page/pieces/torretta.svg +17 -15
  154. package/dist/page/{player-card.js → players/card.js} +2 -6
  155. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  156. package/dist/page/{profile.js → profile/profile.js} +9 -5
  157. package/dist/page/{settings.js → profile/settings.js} +5 -4
  158. package/dist/page/style.css +9 -4398
  159. package/dist/page/talk/talk.css +958 -0
  160. package/dist/page/{talk.js → talk/talk.js} +39 -46
  161. package/dist/page/text.js +137 -19
  162. package/dist/page/theme.css +292 -0
  163. package/dist/profiles/index.d.ts +1 -1
  164. package/dist/profiles/profiles.d.ts +15 -4
  165. package/dist/profiles/profiles.js +24 -7
  166. package/dist/server/bell.d.ts +10 -2
  167. package/dist/server/bell.js +28 -12
  168. package/dist/server/caller.d.ts +14 -3
  169. package/dist/server/caller.js +44 -11
  170. package/dist/server/mcp.d.ts +1 -2
  171. package/dist/server/mcp.js +42 -14
  172. package/dist/server/page/analyses.d.ts +23 -0
  173. package/dist/server/page/analyses.js +65 -0
  174. package/dist/server/page/files.d.ts +5 -0
  175. package/dist/server/page/files.js +92 -0
  176. package/dist/server/page/game.d.ts +6 -0
  177. package/dist/server/page/game.js +381 -0
  178. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  179. package/dist/server/{http.js → page/http.js} +11 -0
  180. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  181. package/dist/server/page/page.js +229 -0
  182. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  183. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  184. package/instructions/analysis.md +4 -0
  185. package/instructions/analyst-agent.md +8 -0
  186. package/instructions/analyst.md +3 -7
  187. package/instructions/coach.md +11 -0
  188. package/instructions/opponent.md +4 -4
  189. package/package.json +1 -1
  190. package/dist/host-files/ponder-agents.js +0 -40
  191. package/dist/page/avatars.js +0 -16
  192. package/dist/page/board.js +0 -566
  193. package/dist/page/fold-tab.js +0 -9
  194. package/dist/page/game-column.js +0 -44
  195. package/dist/page/helps-view.js +0 -51
  196. package/dist/page/helps.js +0 -90
  197. package/dist/page/pieces/cburnett-small.svg +0 -20
  198. package/dist/page/pieces/cburnett.svg +0 -20
  199. package/dist/page/settings-panel.js +0 -32
  200. package/dist/page/studied.js +0 -14
  201. package/dist/page/study-dialog.js +0 -58
  202. package/dist/server/analyses.d.ts +0 -17
  203. package/dist/server/analyses.js +0 -46
  204. package/dist/server/page.js +0 -657
  205. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  206. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  207. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  208. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  209. /package/dist/page/{voice.js → talk/voice.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  // A move of the AI or of the player on the current game: played, timed by the clock, saved, and archived when it ends the game.
2
- import { clockAfterMove, clockTag, reviewed } from "../core/index.js";
2
+ import { clockAfterMove, clockTag } from "../core/index.js";
3
3
  import { archiveEnded, endOf, loadGame, saveGame, talkLine } from "../games/index.js";
4
4
  import { gameGoingOn, summaryOf, withThought } from "./game-summary.js";
5
5
  import { colourName } from "./view.js";
@@ -7,7 +7,7 @@ import { colourName } from "./view.js";
7
7
  * Plays a move for one side `now` and saves the game, with the time it took and the clock after it in its comment ([%clk]). A refused move, or words refused
8
8
  * with it, change nothing: it throws the message `refused` writes from the core's.
9
9
  */
10
- export function playMove(folder, now, mover, text, refused, { comment, said, offerDraw = false, allowance = 0, judged } = {}) {
10
+ export function playMove(folder, now, mover, text, refused, { comment, said, offerDraw = false, allowance = 0, coached = false } = {}) {
11
11
  // A flag falls only past the allowance.
12
12
  const before = gameToMove(folder, mover, new Date(now.getTime() - allowance));
13
13
  const { game } = before;
@@ -20,19 +20,10 @@ export function playMove(folder, now, mover, text, refused, { comment, said, off
20
20
  if (offerDraw && game.record.moves.length === 0) {
21
21
  throw new Error("A draw can be offered once each side has made a move: play the move without offer_draw. Nothing changed.");
22
22
  }
23
- if (judged !== undefined && game.record.moves.length === 0) {
24
- throw new Error("The player has made no move yet: play without badge and note. Nothing changed.");
25
- }
26
23
  const played = game.play(text);
27
24
  if (!played.played) {
28
25
  throw new Error(refused(played.message, played.legal));
29
26
  }
30
- if (judged !== undefined) {
31
- // The player's move is the one before the move just played: its badge and note sit in its comment, beside its clock.
32
- const judging = game.record.moves.length - 1;
33
- const { nags, comment: judgedComment } = reviewed(game.record.moves[judging - 1], judged);
34
- game.annotate(judging, { nags, comment: judgedComment ?? "" });
35
- }
36
27
  const clock = before.clock === undefined ? undefined : clockAfterMove(before.clock, position.turn, now, allowance, mover === "player");
37
28
  // A move declines the other side's draw offer; the mover's own stands, and the move carries it in the record as FIDE writes it, (=) (D-65). A move that
38
29
  // ends the game leaves no offer.
@@ -47,7 +38,7 @@ export function playMove(folder, now, mover, text, refused, { comment, said, off
47
38
  }
48
39
  saveGame(folder, current);
49
40
  const { archive, warning } = endOf(game) === undefined ? {} : archiveEnded(folder, current);
50
- const result = { move: `${position.moveNumber}${position.turn === "white" ? "." : "..."}${played.move.san}`, game: withThought(summaryOf(current, now, archive), folder, current) };
41
+ const result = { move: `${position.moveNumber}${position.turn === "white" ? "." : "..."}${played.move.san}`, game: withThought(summaryOf(current, now, archive), folder, current, coached) };
51
42
  if (played.note !== undefined) {
52
43
  result.note = played.note;
53
44
  }
@@ -85,7 +76,8 @@ export function refuseOtherGame(context, folder) {
85
76
  throw new PositionChanged();
86
77
  }
87
78
  }
88
- function samePosition(one, other) {
79
+ /** Whether two positions a chat may see are the same: the same game, as many plies. */
80
+ export function samePosition(one, other) {
89
81
  return one.started === other.started && one.plies === other.plies;
90
82
  }
91
83
  function gameToMove(folder, mover, now) {
@@ -5,13 +5,9 @@ export declare const playerMove: import("./registry.ts").Command<z.ZodObject<{
5
5
  move: z.ZodString;
6
6
  offer_draw: z.ZodOptional<z.ZodBoolean>;
7
7
  }, z.core.$strip>, PlayerMove>;
8
- /** The player's move, with the board page's address once a game while no browser has it open (D-85). */
8
+ /** The player's move, with the board page's address once a game while no browser has it open (D-85), and the coach to start when it ended the game (D-88). */
9
9
  interface PlayerMove extends MovePlayed {
10
10
  invite?: string;
11
+ coach?: true;
11
12
  }
12
- /**
13
- * The time a move in the chat leaves off the player's clock (D-36): the AI writing after its move and reading the player's, about 10 s as Priya measured.
14
- * Claude Code's hooks will time it exactly (T-53); the board page's moves need none.
15
- */
16
- export declare const chatAllowance = 10000;
17
13
  export {};
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
+ import { loadGame } from "../games/index.js";
3
+ import { coachAgent } from "../host-files/index.js";
2
4
  import { gameFolder } from "../profiles/index.js";
3
- import { closingLines, lastMoveText, moveLines, shownMove } from "./game-summary.js";
5
+ import { agentsLine, coachOnEnd, coachRuns, withAnalyst } from "./agents.js";
6
+ import { closingLines, lastMoveText, moveLines, shownMove } from "./answer-lines.js";
7
+ import {} from "./game-summary.js";
4
8
  import { inviteLine, pageInvite } from "./page-link.js";
5
9
  import { playMove, positionIn, refuseUnseenPosition } from "./play-move.js";
6
10
  import { defineCommand } from "./registry.js";
@@ -20,25 +24,28 @@ export const playerMove = defineCommand({
20
24
  positionals: ["move"],
21
25
  run: ({ move, offer_draw: offerDraw = false }, context) => {
22
26
  const folder = gameFolder(context.home());
23
- // A chat that has seen no game yet, new or resumed, plays the player's own move: its answer shows the game the AI decides on (D-71, T-129).
27
+ // A chat that has seen no game yet, new or resumed, plays the player's own move: its answer shows the game the AI decides on (D-71).
24
28
  if (context.positionSeen !== null) {
25
29
  refuseUnseenPosition(context, folder);
26
30
  }
27
31
  const started = context.positionSeen === null ? positionIn(folder).started : context.positionSeen?.started;
28
- const played = playMove(folder, context.now(), "player", move, (message) => `${message} Ask the player again. Nothing changed.`, { allowance: chatAllowance, offerDraw });
32
+ const played = playMove(folder, context.now(), "player", move, (message) => `${message} Ask the player again. Nothing changed.`, { allowance: chatAllowance, offerDraw, coached: coachRuns(context) });
29
33
  // A game over by this move has no more moves to play there: the next game's window gives the address (Federico, round of D-85).
30
34
  const invite = played.game.end === undefined ? pageInvite(context, started) : undefined;
31
- return { ...played, words: newWords(context), ...(invite === undefined ? {} : { invite }) };
35
+ const ended = played.game.end === undefined ? undefined : loadGame(folder);
36
+ const coach = ended !== undefined && coachOnEnd(context, folder, ended);
37
+ return { ...played, game: withAnalyst(played.game, context), words: newWords(context), ...(invite === undefined ? {} : { invite }), ...(coach ? { coach } : {}) };
32
38
  },
33
39
  format: (result) => [
34
40
  `Player: ${result.game.lastMove === undefined ? shownMove(result.move) : lastMoveText(result.game.lastMove)}.`,
35
41
  ...moveLines(result),
36
42
  ...(result.invite === undefined ? [] : [inviteLine(result.invite)]),
43
+ ...(result.coach === true ? [agentsLine([coachAgent])] : []),
37
44
  ...closingLines(result.game, result.words),
38
45
  ].join("\n"),
39
46
  });
40
47
  /**
41
48
  * The time a move in the chat leaves off the player's clock (D-36): the AI writing after its move and reading the player's, about 10 s as Priya measured.
42
- * Claude Code's hooks will time it exactly (T-53); the board page's moves need none.
49
+ * Claude Code's hooks will time it exactly; the board page's moves need none.
43
50
  */
44
- export const chatAllowance = 10_000;
51
+ const chatAllowance = 10_000;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- /** The player's words to the AI, as the board page sends them (T-65): on the command line only, since the AI never writes for the player. */
2
+ /** The player's words to the AI, as the board page sends them: on the command line only, since the AI never writes for the player. */
3
3
  export declare const playerSay: import("./registry.ts").Command<z.ZodObject<{
4
4
  text: z.ZodString;
5
5
  }, z.core.$strip>, string>;
@@ -3,7 +3,7 @@ import { talkLimits } from "../games/index.js";
3
3
  import { gameFolder } from "../profiles/index.js";
4
4
  import { defineCommand } from "./registry.js";
5
5
  import { say } from "./say.js";
6
- /** The player's words to the AI, as the board page sends them (T-65): on the command line only, since the AI never writes for the player. */
6
+ /** The player's words to the AI, as the board page sends them: on the command line only, since the AI never writes for the player. */
7
7
  export const playerSay = defineCommand({
8
8
  name: "player_say",
9
9
  description: "Writes the player's words to the AI, as the board page does: the AI reads them at its next call. "
@@ -39,4 +39,14 @@ export declare const ponder: import("./registry.ts").Command<z.ZodObject<{
39
39
  say: z.ZodOptional<z.ZodString>;
40
40
  done: z.ZodOptional<z.ZodBoolean>;
41
41
  }, z.core.$strip>, Pondered>;
42
+ /**
43
+ * Waits for the player's move, for an agent that works on their time (D-72, D-88): moved once `gone` says so, waited once the agent has waited long enough in
44
+ * all since `since`, else still waiting after a while.
45
+ */
46
+ export declare function waitForPlayer(context: {
47
+ now: () => Date;
48
+ pause?: (milliseconds: number) => Promise<void>;
49
+ }, since: Date, gone: () => boolean): Promise<"moved" | "waited" | "waiting">;
50
+ /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
51
+ export declare function wordsOf(comment: string | undefined): string | undefined;
42
52
  export {};
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
2
  import { Game, levelOf, moodAt, sees, timeLeft } from "../core/index.js";
3
3
  import { currentPlan, endOf, keepThought, loadGame, talkLimits, thoughtKey, waitingSince } from "../games/index.js";
4
+ import { ponderAgents } from "../host-files/index.js";
4
5
  import { gameFolder } from "../profiles/index.js";
5
- import { positionLines, summaryOf } from "./game-summary.js";
6
+ import { positionLines } from "./answer-lines.js";
7
+ import { summaryOf } from "./game-summary.js";
6
8
  import { levelText } from "./levels.js";
7
9
  import { defineCommand, sentence } from "./registry.js";
8
10
  import { say as sayWords } from "./say.js";
@@ -25,7 +27,7 @@ export const ponder = defineCommand({
25
27
  say: z.string().trim().min(1).max(talkLimits.ai).optional().describe("your words for the player on the board page, once, in one short sentence, of what has happened: pieces and moves in words, never in notation"),
26
28
  done: z.boolean().optional().describe("true when you have no more replies to think on: it waits for the player's move"),
27
29
  }),
28
- byAgent: true,
30
+ byAgents: ponderAgents,
29
31
  host: "claude-code",
30
32
  run: async ({ position, reply, move, why, say, done = false }, context) => {
31
33
  const folder = gameFolder(context.home());
@@ -37,6 +39,7 @@ export const ponder = defineCommand({
37
39
  if (reply !== undefined || move !== undefined || why !== undefined || say !== undefined || done) {
38
40
  throw new Error("Give the position's number with it: call ponder {} first, which gives it. Nothing changed.");
39
41
  }
42
+ context.ponders?.({ started: current.started, plies: current.game.record.moves.length });
40
43
  return { start: startOf(current) };
41
44
  }
42
45
  if ((say !== undefined || done) && (reply !== undefined || move !== undefined || why !== undefined || (say !== undefined && done))) {
@@ -47,6 +50,7 @@ export const ponder = defineCommand({
47
50
  return { said: position };
48
51
  }
49
52
  if (done) {
53
+ context.ponders?.(undefined);
50
54
  return await waitForMove(folder, context, current);
51
55
  }
52
56
  if (reply === undefined) {
@@ -96,17 +100,38 @@ export const ponder = defineCommand({
96
100
  },
97
101
  });
98
102
  /**
99
- * How long one call waits for the player's move at most, under the minute a host may give a tool (Priya, T-109), how often it looks, and how long the agent
100
- * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya, round of T-109).
103
+ * Waits for the player's move, for an agent that works on their time (D-72, D-88): moved once `gone` says so, waited once the agent has waited long enough in
104
+ * all since `since`, else still waiting after a while.
101
105
  */
102
- const ponderWait = { most: 50_000, every: 500, inAll: 10 * 60_000 };
106
+ export async function waitForPlayer(context, since, gone) {
107
+ const pause = context.pause ?? ((milliseconds) => new Promise((done) => setTimeout(done, milliseconds)));
108
+ const start = context.now().getTime();
109
+ const end = Math.min(start + playerWait.most, since.getTime() + playerWait.inAll);
110
+ while (context.now().getTime() < end) {
111
+ await pause(playerWait.every);
112
+ if (gone()) {
113
+ return "moved";
114
+ }
115
+ }
116
+ return end < start + playerWait.most ? "waited" : "waiting";
117
+ }
118
+ /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
119
+ export function wordsOf(comment) {
120
+ const words = comment?.replace(/\[%[^\]]*\]/g, "").replace("(=)", "").replace(/\s+/g, " ").trim();
121
+ return words === undefined || words === "" ? undefined : words;
122
+ }
123
+ /**
124
+ * How long one call waits for the player's move at most, under the minute a host may give a tool (Priya), how often it looks, and how long the agent
125
+ * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya).
126
+ */
127
+ const playerWait = { most: 50_000, every: 500, inAll: 10 * 60_000 };
103
128
  /** The game whose position the AI thinks on, read and never written: none once the player has moved, the game is over or their flag has fallen, or the position given is another. */
104
129
  function toPonder(folder, now, position) {
105
130
  const current = loadGame(folder);
106
131
  if (current === undefined || endOf(current.game) !== undefined || current.game.position.turn === current.ai) {
107
132
  return undefined;
108
133
  }
109
- // A flag fallen is the page's or the chat's to write, so that the doorbell rings it (Federico, round of T-109).
134
+ // A flag fallen is the page's or the chat's to write, so that the doorbell rings it (Federico).
110
135
  const { turn } = current.game.position;
111
136
  if (current.clock !== undefined && timeLeft(current.clock, turn, now)[turn] <= 0) {
112
137
  return undefined;
@@ -115,17 +140,9 @@ function toPonder(folder, now, position) {
115
140
  }
116
141
  /** Waits for the player's move: stop once they have moved or the agent has waited long enough in all, else still their move after a while. */
117
142
  async function waitForMove(folder, context, current) {
118
- const pause = context.pause ?? ((milliseconds) => new Promise((done) => setTimeout(done, milliseconds)));
119
143
  const position = current.game.record.moves.length;
120
- const start = context.now().getTime();
121
- const end = Math.min(start + ponderWait.most, waitingSince(folder, thoughtKey(current), context.now()).getTime() + ponderWait.inAll);
122
- while (context.now().getTime() < end) {
123
- await pause(ponderWait.every);
124
- if (toPonder(folder, context.now(), position) === undefined) {
125
- return { stop: "moved" };
126
- }
127
- }
128
- return end < start + ponderWait.most ? { stop: "waited" } : { waiting: position };
144
+ const waited = await waitForPlayer(context, waitingSince(folder, thoughtKey(current), context.now()), () => toPonder(folder, context.now(), position) === undefined);
145
+ return waited === "waiting" ? { waiting: position } : { stop: waited };
129
146
  }
130
147
  function startOf(current) {
131
148
  const { game, ai, talk } = current;
@@ -160,8 +177,3 @@ function startLines({ position, line, plan, mood, comment, talk }) {
160
177
  + `your answer; ponder {position: ${position}, done: true} when you have no more.`,
161
178
  ];
162
179
  }
163
- /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
164
- function wordsOf(comment) {
165
- const words = comment?.replace(/\[%[^\]]*\]/g, "").replace("(=)", "").replace(/\s+/g, " ").trim();
166
- return words === undefined || words === "" ? undefined : words;
167
- }
@@ -4,7 +4,7 @@ import { type Profile } from "../core/index.ts";
4
4
  type Described = Profile & {
5
5
  where?: string;
6
6
  };
7
- /** The positional profile of the game in progress (D-19, T-12), or of any position of a game over (D-68): separate facts, never a score. */
7
+ /** The positional profile of the game in progress (D-19), or of any position of a game over (D-68): separate facts, never a score. */
8
8
  export declare const position: import("./registry.ts").Command<z.ZodObject<{
9
9
  game: z.ZodOptional<z.ZodString>;
10
10
  move: z.ZodOptional<z.ZodString>;
@@ -1,12 +1,13 @@
1
1
  import { z } from "zod";
2
2
  import {} from "../core/index.js";
3
+ import { analystAgent } from "../host-files/index.js";
3
4
  import { gameFolder } from "../profiles/index.js";
4
5
  import { gameAt } from "./clock.js";
5
6
  import { refuseUnseen } from "./levels.js";
6
7
  import { defineCommand } from "./registry.js";
7
8
  import { gameToStudy, positionAfter, positionLine, studyInput } from "./study.js";
8
9
  import { colourName, materialText } from "./view.js";
9
- /** The positional profile of the game in progress (D-19, T-12), or of any position of a game over (D-68): separate facts, never a score. */
10
+ /** The positional profile of the game in progress (D-19), or of any position of a game over (D-68): separate facts, never a score. */
10
11
  export const position = defineCommand({
11
12
  name: "position",
12
13
  description: "Shows the positional facts of the position, from the strong level up and at every level once the game is over: material, pawns (isolated, doubled, backward, passed, islands), open files, rooks and queens on the seventh, bishops, "
@@ -17,6 +18,7 @@ export const position = defineCommand({
17
18
  input: z.object(studyInput),
18
19
  readOnly: true,
19
20
  idempotent: true,
21
+ sharedWith: [analystAgent],
20
22
  run: ({ game, move }, context) => {
21
23
  if (game !== undefined || move !== undefined) {
22
24
  const studied = gameToStudy(context, game);
@@ -10,7 +10,10 @@ export declare const profiles: import("./registry.ts").Command<z.ZodObject<{}, z
10
10
  profiles: PlayerProfile[];
11
11
  active: string;
12
12
  }>;
13
- /** A new profile, put in use once the game going on has ended; the one Chesspal made at the first start, while new, becomes it, game and all (D-71). */
13
+ /**
14
+ * A new profile, put in use unless asked not to; while a game goes on it is made all the same and waits, the player in use staying. The one Chesspal made at
15
+ * the first start, while new, becomes it, game and all (D-71).
16
+ */
14
17
  export declare const newProfile: import("./registry.ts").Command<z.ZodObject<{
15
18
  name: z.ZodString;
16
19
  level: z.ZodOptional<z.ZodEnum<{
@@ -37,7 +40,10 @@ export declare const newProfile: import("./registry.ts").Command<z.ZodObject<{
37
40
  cat: "cat";
38
41
  owl: "owl";
39
42
  }>>;
40
- }, z.core.$strip>, PlayerProfile>;
43
+ use: z.ZodOptional<z.ZodBoolean>;
44
+ }, z.core.$strip>, PlayerProfile & {
45
+ readonly inUse: boolean;
46
+ }>;
41
47
  /** Another profile put in use, once the game going on has ended. */
42
48
  export declare const useProfile: import("./registry.ts").Command<z.ZodObject<{
43
49
  profile: z.ZodString;
@@ -49,6 +55,11 @@ export declare const setAvatar: import("./registry.ts").Command<z.ZodObject<{
49
55
  profile: string;
50
56
  avatar: string;
51
57
  }>;
58
+ /** The player's names on Lichess and chess.com, by which the games they load are theirs (D-93): each one given is written, an empty one goes. */
59
+ export declare const setAccounts: import("./registry.ts").Command<z.ZodObject<{
60
+ lichess: z.ZodOptional<z.ZodString>;
61
+ chesscom: z.ZodOptional<z.ZodString>;
62
+ }, z.core.$strip>, PlayerProfile>;
52
63
  /** A drawn avatar chosen for a profile: the player's own image goes, or it would still show. */
53
64
  export declare function chooseDrawnAvatar(context: Context, id: string, avatar: DrawnAvatar): void;
54
65
  /**
@@ -56,5 +67,3 @@ export declare function chooseDrawnAvatar(context: Context, id: string, avatar:
56
67
  * system's account until the player completes it (D-71).
57
68
  */
58
69
  export declare function playerLine(context: Context): string | undefined;
59
- /** Throws while a game goes on in the profile in use. */
60
- export declare function refuseWhilePlaying(context: Context): void;
@@ -20,39 +20,43 @@ export const profiles = defineCommand({
20
20
  cliOnly: true,
21
21
  readOnly: true,
22
22
  idempotent: true,
23
- run: (_input, context) => ({ profiles: listProfiles(context.home()), active: activeProfile(context.home()).id }),
23
+ run: (input, context) => ({ profiles: listProfiles(context.home()), active: activeProfile(context.home()).id }),
24
24
  format: ({ profiles, active }) => profiles.map((profile) => `${profile.id === active ? "*" : " "} ${profile.id}: ${playerText(profile)}`).join("\n"),
25
25
  });
26
- /** A new profile, put in use once the game going on has ended; the one Chesspal made at the first start, while new, becomes it, game and all (D-71). */
26
+ /**
27
+ * A new profile, put in use unless asked not to; while a game goes on it is made all the same and waits, the player in use staying. The one Chesspal made at
28
+ * the first start, while new, becomes it, game and all (D-71).
29
+ */
27
30
  export const newProfile = defineCommand({
28
31
  name: "new_profile",
29
- description: "Makes a player's profile and puts it in use: their games, picture and page settings go in profiles/<id>/ of Chesspal's data folder; the "
30
- + "profile Chesspal made at the first start becomes this one, its folder renamed, while nobody has completed it. Example: chesspal new_profile Anna --level regular "
31
- + "--rating 1450 --site lichess.",
32
+ description: "Makes a player's profile and puts it in use, unless --use false or a game is going on: their games, picture and page settings go in "
33
+ + "profiles/<id>/ of Chesspal's data folder; the profile Chesspal made at the first start becomes this one, its folder renamed, while nobody has completed "
34
+ + "it. Example: chesspal new_profile Anna --level regular --rating 1450 --site lichess.",
32
35
  input: z.object({
33
36
  name: z.string().trim().min(1).max(nameLimit).regex(/^[^\p{Cc}]+$/u, "one line of text").describe("the player's name"),
34
37
  level: z.enum(levels).optional().describe("how they play: learning, casual, regular or tournament"),
35
38
  rating: z.int().min(ratingLimits.lowest).max(ratingLimits.highest).optional().describe("their rating, with site"),
36
39
  site: z.enum(ratingSites).optional().describe("where the rating comes from: lichess, chess.com or fide"),
37
40
  avatar: z.enum(drawnAvatars).optional().describe(`a drawn avatar: ${drawnAvatars.join(", ")}`),
41
+ use: z.boolean().optional().describe("false to make it without putting it in use"),
38
42
  }),
39
43
  cliOnly: true,
40
44
  positionals: ["name"],
41
- run: ({ name, level, rating, site, avatar }, context) => {
45
+ run: ({ name, level, rating, site, avatar, use }, context) => {
42
46
  if ((rating === undefined) !== (site === undefined)) {
43
47
  throw new Error("Give the rating and its site together, such as --rating 1450 --site lichess, or neither. Nothing changed.");
44
48
  }
45
- if (activeProfile(context.home()).new !== true) {
46
- refuseWhilePlaying(context);
47
- }
48
- return createProfile(context.home(), {
49
+ const first = activeProfile(context.home()).new === true;
50
+ const inUse = first || (use !== false && !isPlaying(context));
51
+ const profile = createProfile(context.home(), {
49
52
  name,
50
53
  ...(level === undefined ? {} : { level }),
51
54
  ...(rating === undefined || site === undefined ? {} : { rating: { value: rating, site } }),
52
55
  ...(avatar === undefined ? {} : { avatar }),
53
- });
56
+ }, inUse);
57
+ return { ...profile, inUse };
54
58
  },
55
- format: (profile) => `Profile ${profile.id} made, and in use: ${playerText(profile)}.`,
59
+ format: (profile) => `Profile ${profile.id} made${profile.inUse ? ", and in use" : ", not in use"}: ${playerText(profile)}.`,
56
60
  });
57
61
  /** Another profile put in use, once the game going on has ended. */
58
62
  export const useProfile = defineCommand({
@@ -94,6 +98,25 @@ export const setAvatar = defineCommand({
94
98
  },
95
99
  format: ({ profile, avatar }) => `The picture of ${profile} is ${avatar}.`,
96
100
  });
101
+ /** The player's names on Lichess and chess.com, by which the games they load are theirs (D-93): each one given is written, an empty one goes. */
102
+ export const setAccounts = defineCommand({
103
+ name: "set_accounts",
104
+ description: "Sets the player's names on Lichess and chess.com in the profile in use, by which a game loaded from there is theirs; an empty name removes it. "
105
+ + "Example: chesspal set_accounts --lichess anna_k --chesscom AnnaK.",
106
+ input: z.object({
107
+ lichess: accountName().optional().describe("the player's name on Lichess"),
108
+ chesscom: accountName().optional().describe("the player's name on chess.com"),
109
+ }),
110
+ cliOnly: true,
111
+ idempotent: true,
112
+ run: (names, context) => {
113
+ const profile = activeProfile(context.home());
114
+ const lichess = names.lichess ?? profile.accounts?.lichess ?? "";
115
+ const chesscom = names.chesscom ?? profile.accounts?.chesscom ?? "";
116
+ return updateProfile(context.home(), profile.id, { accounts: { ...(lichess === "" ? {} : { lichess }), ...(chesscom === "" ? {} : { chesscom }) } });
117
+ },
118
+ format: (profile) => `${profile.id} is ${accountsText(profile.accounts)}.`,
119
+ });
97
120
  /** A drawn avatar chosen for a profile: the player's own image goes, or it would still show. */
98
121
  export function chooseDrawnAvatar(context, id, avatar) {
99
122
  updateProfile(context.home(), id, { avatar });
@@ -108,17 +131,28 @@ export function playerLine(context) {
108
131
  return profile.new === true ? undefined : `Player: ${playerText(profile)}.`;
109
132
  }
110
133
  /** Throws while a game goes on in the profile in use. */
111
- export function refuseWhilePlaying(context) {
112
- const current = gameAt(gameFolder(context.home()), context.now());
113
- if (current !== undefined && endOf(current.game) === undefined) {
134
+ function refuseWhilePlaying(context) {
135
+ if (isPlaying(context)) {
114
136
  throw new GameGoingOn();
115
137
  }
116
138
  }
139
+ function isPlaying(context) {
140
+ const current = gameAt(gameFolder(context.home()), context.now());
141
+ return current !== undefined && endOf(current.game) === undefined;
142
+ }
117
143
  const levelText = { learning: "has just learned", casual: "plays now and then", regular: "plays often", tournament: "plays tournaments" };
118
144
  const siteText = { "lichess": "Lichess", "chess.com": "Chess.com", "fide": "FIDE" };
119
145
  function playerText({ name, level, rating }) {
120
146
  return [name, ...(level === undefined ? [] : [levelText[level]]), ...(rating === undefined ? [] : [`${siteText[rating.site]} ${rating.value}`])].join(", ");
121
147
  }
148
+ /** A name on Lichess or chess.com: their letters, digits, underscores and hyphens; empty to remove it. */
149
+ function accountName() {
150
+ return z.string().trim().max(nameLimit).regex(/^[\w-]*$/, "A name on Lichess or chess.com has only letters, digits, underscores and hyphens.");
151
+ }
152
+ function accountsText(accounts) {
153
+ const named = [...(accounts?.lichess === undefined ? [] : [`${accounts.lichess} on Lichess`]), ...(accounts?.chesscom === undefined ? [] : [`${accounts.chesscom} on chess.com`])];
154
+ return named.length === 0 ? "on neither Lichess nor chess.com" : named.join(" and ");
155
+ }
122
156
  function imageFile(path) {
123
157
  try {
124
158
  return readFileSync(path);
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { Context } from "./context.ts";
3
- /** A command is defined once: the command line and the MCP server are generated from it (D-5), and later the page's actions. */
3
+ /** A command is defined once: the command line and the MCP server are generated from it (D-5); the page's routes call the same commands. */
4
4
  export interface Command<Schema extends z.ZodObject = z.ZodObject, Result = unknown> {
5
5
  name: string;
6
6
  /** Written for an AI: what the command does (the first sentence, shown in the list of commands), when to use it, an example. */
@@ -16,8 +16,13 @@ export interface Command<Schema extends z.ZodObject = z.ZodObject, Result = unkn
16
16
  alwaysLoad?: boolean;
17
17
  /** Its answer shows the chat the game as it is: the chat may move on it, and on no other (D-71). */
18
18
  showsGame?: boolean;
19
- /** An agent calls it apart from the chat, as ponder (D-72): the doorbell's record of what the chat has read stays as it was. */
20
- byAgent?: boolean;
19
+ /**
20
+ * The agents that call it apart from the chat, as ponder (D-72), by their names: the doorbell's record of what the chat has read stays as it was, and the
21
+ * server refuses it to the chat and to other agents (Priya).
22
+ */
23
+ byAgents?: readonly string[];
24
+ /** The agents that call it besides the chat, as the analyst analyse_game (D-89): their calls leave the doorbell's record of what the chat has read as it was. */
25
+ sharedWith?: readonly string[];
21
26
  /** The only host the MCP server offers it to, by the client's name, as ponder to Claude Code: elsewhere its description would cost tokens for nothing. */
22
27
  host?: string;
23
28
  /** Only the command line has it, as init: the MCP server leaves it out, and its description costs the AI no tokens. */
@@ -1,3 +1,4 @@
1
+ // What a command is, and the reading of its input schema that the command line and the MCP server share.
1
2
  import { z } from "zod";
2
3
  /** Checks a command's types where it is written; the command is used as it is. */
3
4
  export function defineCommand(command) {
@@ -14,17 +15,6 @@ export function sentence(most) {
14
15
  export function inputProblems(command, issues, given, written) {
15
16
  return issues.map((issue) => inputIssue(command, issue, given, written)).join(" ");
16
17
  }
17
- function inputIssue(command, issue, given, written) {
18
- const path = issue.path.map(String).join(".");
19
- if (issue.code === "unrecognized_keys") {
20
- return path === "" ? `${command} has no parameter ${issue.keys.map(written).join(", ")}.` : `${written(path)} has no ${issue.keys.join(", ")}.`;
21
- }
22
- // A parameter left out, whatever zod calls it.
23
- if (issue.path.length === 1 && given[path] === undefined) {
24
- return `${command} needs ${written(path)}.`;
25
- }
26
- return path === "" ? `${issue.message}.` : `${written(path)}: ${issue.message}.`;
27
- }
28
18
  /**
29
19
  * What keeps a field of a command's input from being given on the command line: one named no-x, read as turning x off, or json, read as answering in JSON;
30
20
  * undefined for any other. A positional is never an option, so json is its name freely.
@@ -52,6 +42,17 @@ export function takesNumbers(field) {
52
42
  const definition = baseDefinition(field);
53
43
  return definition.type === "number" || (definition.type === "array" && definition.element !== undefined && baseType(definition.element) === "number");
54
44
  }
45
+ function inputIssue(command, issue, given, written) {
46
+ const path = issue.path.map(String).join(".");
47
+ if (issue.code === "unrecognized_keys") {
48
+ return path === "" ? `${command} has no parameter ${issue.keys.map(written).join(", ")}.` : `${written(path)} has no ${issue.keys.join(", ")}.`;
49
+ }
50
+ // A parameter left out, whatever zod calls it.
51
+ if (issue.path.length === 1 && given[path] === undefined) {
52
+ return `${command} needs ${written(path)}.`;
53
+ }
54
+ return path === "" ? `${issue.message}.` : `${written(path)}: ${issue.message}.`;
55
+ }
55
56
  function baseDefinition(field) {
56
57
  let definition = field.def;
57
58
  while (definition.innerType && ["optional", "default", "nullable"].includes(definition.type)) {
@@ -6,7 +6,7 @@ import type { Context } from "./context.ts";
6
6
  * With no game the line goes in the talk before it (D-77). A line refused changes nothing: it throws, in words for whoever spoke.
7
7
  */
8
8
  export declare function say(folder: string, who: Sayer, text: string, at: Date, mood?: Mood): Talk;
9
- /** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once. */
9
+ /** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once; never to an agent (D-89). */
10
10
  export declare function newWords(context: Context): string[];
11
11
  /** Every word of the player's the AI has not answered, given again: status gives them all, for a chat that lost them in a summary. */
12
12
  export declare function unansweredWords(context: Context): string[];
@@ -1,4 +1,4 @@
1
- // The talk on the board page (D-51, T-65): a line of the player's or the AI's, saved with the game, and the player's words the AI has not been given yet.
1
+ // The talk on the board page (D-51): a line of the player's or the AI's, saved with the game, and the player's words the AI has not been given yet.
2
2
  import { endWithMood } from "../core/index.js";
3
3
  import { archiveEnded, endOf, loadGame, loadTableTalk, saveGame, saveTableTalk, talkLimits, talkLine, unheardWords } from "../games/index.js";
4
4
  import { gameFolder, helpsIn } from "../profiles/index.js";
@@ -26,22 +26,9 @@ export function say(folder, who, text, at, mood) {
26
26
  }
27
27
  return current;
28
28
  }
29
- /** The most a line of the AI's may hold in the games of `folder`: an analyst's note with the helps on (D-78); a longer one with them off is refused. */
30
- function aiMost(folder, who, text) {
31
- if (who !== "ai") {
32
- return talkLimits.ai;
33
- }
34
- if (helpsIn(folder)) {
35
- return talkLimits.aiHelps;
36
- }
37
- if (text.trim().length > talkLimits.ai) {
38
- throw new Error(`The player has the helps off: write at most ${talkLimits.ai} characters. Nothing changed.`);
39
- }
40
- return talkLimits.ai;
41
- }
42
- /** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once. */
29
+ /** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once; never to an agent (D-89). */
43
30
  export function newWords(context) {
44
- return give(context, waitingWords(context));
31
+ return context.agent === undefined ? give(context, waitingWords(context)) : [];
45
32
  }
46
33
  /** Every word of the player's the AI has not answered, given again: status gives them all, for a chat that lost them in a summary. */
47
34
  export function unansweredWords(context) {
@@ -69,6 +56,19 @@ export function give(context, words) {
69
56
  }
70
57
  return words.map(({ text }) => text);
71
58
  }
59
+ /** The most a line of the AI's may hold in the games of `folder`: an analyst's note with the helps on (D-78); a longer one with them off is refused. */
60
+ function aiMost(folder, who, text) {
61
+ if (who !== "ai") {
62
+ return talkLimits.ai;
63
+ }
64
+ if (helpsIn(folder)) {
65
+ return talkLimits.aiHelps;
66
+ }
67
+ if (text.trim().length > talkLimits.ai) {
68
+ throw new Error(`The player has the helps off: write at most ${talkLimits.ai} characters. Nothing changed.`);
69
+ }
70
+ return talkLimits.ai;
71
+ }
72
72
  /** A line of the talk with no game, which starts it the first time; no mood, which only the end of a game takes. */
73
73
  function sayAtTable(folder, who, text, at, mood) {
74
74
  if (who === "agent") {
@@ -0,0 +1,8 @@
1
+ /** The player's language by its English name when it is not English. */
2
+ export interface Speech {
3
+ language: string;
4
+ }
5
+ /** The player's speech, from the page's settings or the system's language; undefined in English, which the AI speaks unless told. */
6
+ export declare function speechOf(folder: string, systemLanguage: string | undefined): Speech | undefined;
7
+ /** What the AI is told of the player's speech once a chat (Priya); the moves are SAN in every language (D-87). */
8
+ export declare function speechLine({ language }: Speech): string;
@@ -0,0 +1,20 @@
1
+ // The player's language, which the AI is told once a chat when it is not English: the moves stay in SAN (D-87).
2
+ import { languageIn } from "../profiles/index.js";
3
+ /** The player's speech, from the page's settings or the system's language; undefined in English, which the AI speaks unless told. */
4
+ export function speechOf(folder, systemLanguage) {
5
+ const language = languageName(languageIn(folder, systemLanguage));
6
+ return language === undefined ? undefined : { language };
7
+ }
8
+ /** What the AI is told of the player's speech once a chat (Priya); the moves are SAN in every language (D-87). */
9
+ export function speechLine({ language }) {
10
+ return `The player speaks ${language}.`;
11
+ }
12
+ /** A language by its English name when it is not English (Italian), from the way the system or the page names it (it-IT, it). */
13
+ function languageName(language) {
14
+ const code = language?.split(/[-_]/)[0]?.toLowerCase() ?? "";
15
+ if (!/^[a-z]{2,3}$/.test(code) || code === "en" || code === "und") {
16
+ return undefined;
17
+ }
18
+ const name = new Intl.DisplayNames(["en"], { type: "language" }).of(code);
19
+ return name === undefined || name === code ? undefined : name;
20
+ }