chesspal 0.2.3 → 0.2.4

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 (75) hide show
  1. package/dist/commands/agents.js +2 -1
  2. package/dist/commands/ai-say.d.ts +2 -1
  3. package/dist/commands/analyse-game.d.ts +2 -0
  4. package/dist/commands/analyse-game.js +7 -4
  5. package/dist/commands/annotate-game.d.ts +2 -0
  6. package/dist/commands/annotate-game.js +26 -8
  7. package/dist/commands/answer-lines.d.ts +8 -4
  8. package/dist/commands/answer-lines.js +19 -3
  9. package/dist/commands/coach.js +2 -2
  10. package/dist/commands/context.d.ts +2 -0
  11. package/dist/commands/delete-review.js +3 -2
  12. package/dist/commands/game-summary.d.ts +7 -2
  13. package/dist/commands/game-summary.js +7 -0
  14. package/dist/commands/index.d.ts +1 -1
  15. package/dist/commands/index.js +1 -1
  16. package/dist/commands/look.d.ts +9 -5
  17. package/dist/commands/look.js +19 -9
  18. package/dist/commands/ponder.js +1 -1
  19. package/dist/commands/position.js +2 -2
  20. package/dist/commands/say.d.ts +11 -6
  21. package/dist/commands/say.js +21 -13
  22. package/dist/commands/status.d.ts +3 -1
  23. package/dist/commands/status.js +31 -6
  24. package/dist/commands/study.d.ts +11 -1
  25. package/dist/commands/study.js +31 -2
  26. package/dist/commands/view.d.ts +17 -5
  27. package/dist/commands/view.js +67 -10
  28. package/dist/core/facts.d.ts +14 -2
  29. package/dist/core/facts.js +32 -7
  30. package/dist/core/game.d.ts +5 -0
  31. package/dist/core/game.js +9 -0
  32. package/dist/core/index.d.ts +1 -0
  33. package/dist/core/placement.d.ts +63 -0
  34. package/dist/core/placement.js +259 -0
  35. package/dist/core/possibilities.d.ts +38 -0
  36. package/dist/core/possibilities.js +254 -0
  37. package/dist/core/review.d.ts +4 -1
  38. package/dist/core/review.js +17 -6
  39. package/dist/games/analysis.d.ts +8 -4
  40. package/dist/games/analysis.js +9 -5
  41. package/dist/games/current-game.d.ts +16 -1
  42. package/dist/games/current-game.js +10 -8
  43. package/dist/games/files.d.ts +2 -0
  44. package/dist/games/files.js +7 -1
  45. package/dist/games/index.d.ts +2 -2
  46. package/dist/games/index.js +1 -1
  47. package/dist/games/table-talk.d.ts +2 -0
  48. package/dist/games/table-talk.js +5 -1
  49. package/dist/games/talk.d.ts +4 -3
  50. package/dist/games/talk.js +3 -3
  51. package/dist/page/app/app.js +36 -12
  52. package/dist/page/app/keys.js +16 -2
  53. package/dist/page/app/served.js +2 -2
  54. package/dist/page/board/board.js +2 -1
  55. package/dist/page/board/marks.js +4 -24
  56. package/dist/page/board/signs.js +23 -0
  57. package/dist/page/game/column.js +2 -2
  58. package/dist/page/game/game.css +12 -0
  59. package/dist/page/game/move-list.js +2 -2
  60. package/dist/page/game/plies.js +8 -0
  61. package/dist/page/helps/helps.css +3 -1
  62. package/dist/page/helps/helps.js +15 -15
  63. package/dist/page/layout/layout.css +29 -5
  64. package/dist/page/legend/legend.css +203 -0
  65. package/dist/page/legend/legend.js +169 -0
  66. package/dist/page/legend/view.js +54 -0
  67. package/dist/page/style.css +2 -0
  68. package/dist/page/talk/chat.css +90 -0
  69. package/dist/page/talk/talk.js +27 -5
  70. package/dist/page/text.js +38 -2
  71. package/dist/server/mcp.js +8 -6
  72. package/dist/server/page/game.js +20 -7
  73. package/dist/server/page/page.js +5 -3
  74. package/instructions/opponent.md +1 -1
  75. package/package.json +1 -1
@@ -59,29 +59,35 @@ export function Legend({ review, words }) {
59
59
  }
60
60
  /**
61
61
  * The player's words of the game, the newest by the box they write in with how far it got, and the box: Enter or the button sends the words alone, the next
62
- * move takes them with it. 200 characters from one move of the AI's to the next: once they are written, the box goes until its move (D-54).
62
+ * move takes them with it. 200 characters from one move of the AI's to the next: once they are written, the box goes until its move (D-54). In analysis the
63
+ * AI's answers are in the same thread, and each line of the player's names the move it was written on (D-101).
63
64
  */
64
- export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, canMove, name, problem, text }) {
65
+ export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, canMove, name, problem, about, text }) {
65
66
  const list = useRef(null);
66
67
  useOnlyWhole(list, lines.map((line) => line.text));
67
68
  const left = talkLimits.line - spent;
68
69
  const blank = draft.trim() === "";
70
+ const lastWritten = lines.findLastIndex((line) => line.who === "player");
71
+ const lastAnswer = lines.findLastIndex((line) => line.who === "ai");
72
+ const prompt = about === undefined ? text.writeTo(name) : text.askAbout(name);
69
73
  const submit = () => {
70
74
  if (!blank) {
71
75
  onSend();
72
76
  }
73
77
  };
74
- return (_jsxs(_Fragment, { children: [_jsx("ol", { ref: list, class: "sent", "aria-live": "polite", children: inOrder(lines.map((line, index) => (_jsxs("li", { style: aged(lines.length - 1 - index), children: [_jsx("p", { class: "said", children: line.text }), index === lines.length - 1 ? _jsx(Status, { heard: line.heard, name: name, text: text }) : null] }, index))), newestFirst) }), left <= 0 ? null : (_jsxs("form", { class: "compose", onSubmit: (event) => {
78
+ return (_jsxs(_Fragment, { children: [_jsx("ol", { ref: list, class: "sent", "aria-live": "polite", children: inOrder(lines.map((line, index) => (_jsxs("li", { class: line.who === "ai" ? "from-ai" : undefined, style: about === undefined ? aged(lines.length - 1 - index) : reread(index, lastAnswer), children: [line.who === "ai"
79
+ ? (_jsx("p", { class: "say", children: _jsx(Marked, { text: line.text }) }))
80
+ : (_jsxs("p", { class: "said", children: [about === undefined || line.shown?.move === undefined ? null : _jsx(ShownMove, { move: line.shown.move, onShow: () => about.onShow(line.shown.at), text: text }), line.text] })), index === lastWritten ? _jsx(Status, { heard: line.heard, name: name, text: text }) : null] }, index))), newestFirst) }), left <= 0 ? null : (_jsxs("form", { class: about === undefined ? "compose" : "compose asking", onSubmit: (event) => {
75
81
  event.preventDefault();
76
82
  submit();
77
- }, children: [_jsx("p", { class: "limit", role: "status", children: problem ?? "" }), _jsx("textarea", { rows: 2, maxLength: left, placeholder: text.writeTo(name), "aria-label": text.writeTo(name), value: draft, onInput: (event) => onDraft(event.currentTarget.value), onKeyDown: (event) => {
83
+ }, children: [_jsx("p", { class: "limit", role: "status", children: problem ?? "" }), _jsx("textarea", { rows: 2, maxLength: left, placeholder: prompt, "aria-label": prompt, value: draft, onInput: (event) => onDraft(event.currentTarget.value), onKeyDown: (event) => {
78
84
  if (event.key === "Enter" && !event.shiftKey && !event.isComposing) {
79
85
  event.preventDefault();
80
86
  submit();
81
87
  }
82
88
  } }), _jsxs("div", { class: "foot", children: [spent + draft.length >= talkLimits.line - 50
83
89
  ? _jsxs("span", { class: "count", children: [spent + draft.length, "/", talkLimits.line] })
84
- : _jsx("span", { class: "hint", children: !blank && canMove ? text.withMove : "" }), _jsxs("button", { class: "send", type: "submit", disabled: blank, children: [text.send, _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12H18M13 7L18 12L13 17" }) })] })] })] }))] }));
90
+ : _jsx("span", { class: "hint", children: about === undefined ? !blank && canMove ? text.withMove : "" : _jsx(OnMove, { ply: about.ply, text: text }) }), _jsxs("button", { class: "send", type: "submit", disabled: blank, children: [text.send, _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M5 12H18M13 7L18 12L13 17" }) })] })] })] }))] }));
85
91
  }
86
92
  /**
87
93
  * The talk narrowed to a card beside the board: the Torretta above, the player below, each with their name and side written down the card, a ring on
@@ -90,6 +96,18 @@ export function Compose({ lines, newestFirst, spent, draft, onDraft, onSend, can
90
96
  export function TalkFaces({ name, aiPicture, you, picture, ai, player, aiMoves, playerMoves, unread, text }) {
91
97
  return (_jsxs("div", { class: "faces", children: [_jsx(RailPlayer, { place: "ai", picture: aiPicture ?? _jsx(Torretta, { size: 40 }), name: name, ...(ai === undefined ? {} : { side: text.sides[ai] }), moves: aiMoves, unread: unread }), _jsx(RailPlayer, { place: "you", picture: picture, name: you, ...(player === undefined ? {} : { side: text.sides[player] }), moves: playerMoves })] }));
92
98
  }
99
+ /** The move a line of the player's was written on, 14.♘f3, small at the line's head: a press shows it on the board again (Zoe). */
100
+ function ShownMove({ move, onShow, text }) {
101
+ const [, number = "", san = move] = /^(\d+\.+)(.+)$/.exec(move) ?? [];
102
+ return (_jsxs("button", { type: "button", class: "shown-move", title: text.goTo(move), "aria-label": text.goTo(move), onClick: onShow, children: [_jsx("span", { class: "move-number", children: number }), _jsx(Figurines, { san: san, side: number.endsWith("...") ? "black" : "white" })] }));
103
+ }
104
+ /** What the words go with in analysis: the move shown, or the whole game at its start. */
105
+ function OnMove({ ply, text }) {
106
+ if (ply === undefined) {
107
+ return _jsx(_Fragment, { children: text.onWholeGame });
108
+ }
109
+ return (_jsxs(_Fragment, { children: [text.onMove, " ", _jsxs("span", { class: "move-number", children: [ply.number, ply.side === "white" ? "." : "..."] }), _jsx(Figurines, { san: ply.san, side: ply.side })] }));
110
+ }
93
111
  function OnBoard({ text }) {
94
112
  const point = useContext(PointSquare);
95
113
  return (_jsx(_Fragment, { children: text.split(boardWords).map((part, index) => index % 2 === 1
@@ -130,6 +148,10 @@ function inOrder(items, newestFirst) {
130
148
  function aged(age) {
131
149
  return { opacity: age === 0 ? 1 : Math.max(0.2, 0.4 - (age - 1) * 0.1) };
132
150
  }
151
+ /** In analysis the talk is read again: the AI's last answer and what follows it whole, the older lines a little back, still readable (Zoe). */
152
+ function reread(index, lastAnswer) {
153
+ return { opacity: index >= lastAnswer ? 1 : 0.75 };
154
+ }
133
155
  /**
134
156
  * A list that never scrolls: a bubble that does not fit whole in it is hidden, the oldest first since the newest sit by the card, and shows again when the
135
157
  * window grows.
package/dist/page/text.js CHANGED
@@ -64,7 +64,7 @@ const english = {
64
64
  noGame: "No game yet: press + to start one, or ask for one in the chat.",
65
65
  foldCard: "Fold the card",
66
66
  openCard: "Open the card",
67
- saveMoves: "Moves-only PGN",
67
+ saveMoves: "PGN",
68
68
  saveMovesHint: "The moves alone, to open on Lichess or chess.com",
69
69
  noGameNow: "No game in progress",
70
70
  analysing: "Analysis in progress",
@@ -103,6 +103,8 @@ const english = {
103
103
  send: "Send",
104
104
  sent: "Sent",
105
105
  withMove: "Or move: your words go with it",
106
+ onMove: "On",
107
+ onWholeGame: "On the whole game",
106
108
  talk: "The talk",
107
109
  move: "Move",
108
110
  draw: "draw",
@@ -129,6 +131,7 @@ const english = {
129
131
  startClocks: "Start",
130
132
  thinking: { move: "Thinking about its move", ponder: "Thinking while you play" },
131
133
  asleep: (name) => `${name} is not listening: write a message, such as "hi", in the window where you opened ${name}, not here. Its clock waits until it answers.`,
134
+ asleepInAnalysis: (name) => `${name} is not listening: write to it in the window where you opened it, not here.`,
132
135
  commandHere: (name) => `/clear and the other commands go in the window where you opened ${name}, not here.`,
133
136
  refused: {
134
137
  "no-game": () => "There is no game going on: press + to start one, or ask for one in the chat.",
@@ -149,6 +152,8 @@ const english = {
149
152
  piece: (letter, side) => `${side} ${englishNames[letter]}`,
150
153
  taken: (pieces, ahead) => `Pieces taken: ${pieces.length === 0 ? "none" : pieces.join(", ")}${ahead > 0 ? `; ahead by ${ahead}` : ""}`,
151
154
  writeTo: (name) => `Write to ${name}…`,
155
+ askAbout: (name) => `Ask ${name} about this game…`,
156
+ goTo: (move) => `Go to ${move}`,
152
157
  readBy: (name) => `Read by ${name}`,
153
158
  wrote: (name) => `${name} wrote`,
154
159
  offers: "offers a draw",
@@ -325,6 +330,19 @@ const english = {
325
330
  pawnSquare: (pawn, caught) => `The enemy king is ${caught ? "inside" : "outside"} the square of the pawn on ${pawn}`,
326
331
  opposition: (side) => `${side === "white" ? "White" : "Black"} has the opposition`,
327
332
  },
333
+ legend: {
334
+ title: "Marks on the board",
335
+ hint: "Marks on the board (?)",
336
+ none: "No marks on the board now",
337
+ badge: "the move's judgment",
338
+ sign: (kind, tone) => `${tone === "threat" ? "Your" : tone === "chance" ? "Opponent's" : "A"} ${{ risk: "piece at risk", pin: "pinned piece", skewer: "skewered piece" }[kind]}`,
339
+ pinLine: "The pin's line",
340
+ skewerLine: "The skewer's line",
341
+ drawnBy: (name) => `drawn by ${name}`,
342
+ drawnByCoach: "drawn by the coach",
343
+ control: "How many of White's pieces and of Black's reach the square",
344
+ ends: { crown: "Won", mate: "Checkmated", flag: "Resigned", half: "Draw, half a point each" },
345
+ },
328
346
  };
329
347
  const italian = {
330
348
  language: "it",
@@ -385,7 +403,7 @@ const italian = {
385
403
  noGame: "Nessuna partita: premi + per iniziarne una, o chiedila nella chat.",
386
404
  foldCard: "Chiudi la scheda",
387
405
  openCard: "Apri la scheda",
388
- saveMoves: "PGN solo mosse",
406
+ saveMoves: "PGN",
389
407
  saveMovesHint: "Solo le mosse, da aprire su Lichess o chess.com",
390
408
  noGameNow: "Nessuna partita in corso",
391
409
  analysing: "Analisi in corso",
@@ -424,6 +442,8 @@ const italian = {
424
442
  send: "Manda",
425
443
  sent: "Mandato",
426
444
  withMove: "Oppure muovi: parte con la mossa",
445
+ onMove: "Su",
446
+ onWholeGame: "Su tutta la partita",
427
447
  talk: "Conversazione",
428
448
  move: "Mossa",
429
449
  draw: "patta",
@@ -450,6 +470,7 @@ const italian = {
450
470
  startClocks: "Inizia",
451
471
  thinking: { move: "Sta pensando alla sua mossa", ponder: "Pensa mentre giochi tu" },
452
472
  asleep: (name) => `${name} non ti sta ascoltando: scrivi un messaggio, per esempio "ciao", nella finestra dove hai aperto ${name}, non qui. Il suo orologio aspetta che risponda.`,
473
+ asleepInAnalysis: (name) => `${name} non ti sta ascoltando: scrivigli nella finestra dove l'hai aperto, non qui.`,
453
474
  commandHere: (name) => `/clear e gli altri comandi vanno scritti nella finestra dove hai aperto ${name}, non qui.`,
454
475
  refused: {
455
476
  "no-game": () => "Nessuna partita in corso: premi + per iniziarne una, o chiedila nella chat.",
@@ -471,6 +492,8 @@ const italian = {
471
492
  piece: (letter, side) => `${italianNames[letter]} ${side === "white" ? (letter === "Q" || letter === "R" ? "bianca" : "bianco") : (letter === "Q" || letter === "R" ? "nera" : "nero")}`,
472
493
  taken: (pieces, ahead) => `Pezzi presi: ${pieces.length === 0 ? "nessuno" : pieces.join(", ")}${ahead > 0 ? `; avanti di ${ahead}` : ""}`,
473
494
  writeTo: (name) => `Scrivi a ${name}…`,
495
+ askAbout: (name) => `Chiedi a ${name} di questa partita…`,
496
+ goTo: (move) => `Vai a ${move}`,
474
497
  readBy: (name) => `Letto da ${name}`,
475
498
  wrote: (name) => `${name} ha scritto`,
476
499
  offers: "propone patta",
@@ -647,6 +670,19 @@ const italian = {
647
670
  pawnSquare: (pawn, caught) => `Il re nemico è ${caught ? "dentro il" : "fuori dal"} quadrato del pedone in ${pawn}`,
648
671
  opposition: (side) => `L'opposizione è del ${side === "white" ? "Bianco" : "Nero"}`,
649
672
  },
673
+ legend: {
674
+ title: "Segni sulla scacchiera",
675
+ hint: "Segni sulla scacchiera (?)",
676
+ none: "Nessun segno sulla scacchiera ora",
677
+ badge: "il giudizio della mossa",
678
+ sign: (kind, tone) => `${tone === "threat" ? "Tuo pezzo" : tone === "chance" ? "Pezzo avversario" : "Pezzo"} ${{ risk: "in pericolo", pin: "inchiodato", skewer: "infilato" }[kind]}`,
679
+ pinLine: "Linea dell'inchiodatura",
680
+ skewerLine: "Linea dell'infilata",
681
+ drawnBy: (name) => `disegnato da ${name}`,
682
+ drawnByCoach: "disegnato dall'allenatore",
683
+ control: "Quanti pezzi del Bianco e del Nero raggiungono la casa",
684
+ ends: { crown: "Ha vinto", mate: "Scacco matto", flag: "Ha abbandonato", half: "Patta, mezzo punto a testa" },
685
+ },
650
686
  };
651
687
  // How each piece moves, said when it was taken where it cannot go (Anatolij's words).
652
688
  const englishMoves = {
@@ -154,14 +154,16 @@ function toolConfig(command) {
154
154
  return { description: command.description, inputSchema: z.looseObject({}).meta({ ...shownInput(command), $schema: undefined }), annotations, _meta: meta };
155
155
  }
156
156
  async function runTool(command, input, context, caller, meta) {
157
- const agent = await sharingAgent(command, meta, context);
157
+ const calling = await sharingCaller(command, meta, context);
158
+ const agent = calling !== undefined && "agent" in calling ? calling.agent : undefined;
158
159
  const chat = command.byAgents === undefined && agent === undefined;
159
160
  let refused = false;
160
161
  try {
161
162
  if (chat) {
162
163
  context.calls?.();
163
164
  }
164
- const result = await command.run(checkedInput(command, input), { ...context, caller, ...(agent === undefined ? {} : { agent }) });
165
+ const who = agent !== undefined ? { agent } : calling !== undefined ? { chat: true } : {};
166
+ const result = await command.run(checkedInput(command, input), { ...context, caller, ...who });
165
167
  return { content: [{ type: "text", text: command.format(result, context) }] };
166
168
  }
167
169
  catch (error) {
@@ -177,16 +179,16 @@ async function runTool(command, input, context, caller, meta) {
177
179
  }
178
180
  }
179
181
  /**
180
- * The agent that calls a tool it shares with the chat, as the analyst (D-89), by its type: looked up only in a folder where one may run and while no game
181
- * goes on, so that no call of the chat's waits for it while it plays; none for the chat, or when it cannot be told.
182
+ * Who calls a tool the chat shares with an agent, as the analyst (D-89): the chat, or the agent by its type; looked up only in a folder where one may run
183
+ * and while no game goes on, so that no call of the chat's waits for it while it plays. None when it cannot be told, or for an agent the tool is not shared with.
182
184
  */
183
- async function sharingAgent(command, meta, context) {
185
+ async function sharingCaller(command, meta, context) {
184
186
  const agents = command.sharedWith?.filter((agent) => hasAgent(context.folder, agent, context.host?.())) ?? [];
185
187
  if (agents.length === 0 || gameGoingOn(context)) {
186
188
  return undefined;
187
189
  }
188
190
  const caller = await callingAgent(meta, context.folder);
189
- return caller !== undefined && "agent" in caller && agents.includes(caller.agent) ? caller.agent : undefined;
191
+ return caller !== undefined && "agent" in caller && !agents.includes(caller.agent) ? undefined : caller;
190
192
  }
191
193
  /** Whether a game goes on, or may: a game that cannot be read is the command's to report, as the chat's call (Federico). */
192
194
  function gameGoingOn(context) {
@@ -1,4 +1,4 @@
1
- import { analyseGame, answerDraw, bookPlies, ChatNotClear, DrawRefused, endGame, gameAt, gameToStudy, namedGame, newGame, offerDraw, playMove, samePosition, say, startNewGame, } from "../../commands/index.js";
1
+ import { analyseGame, answerDraw, bookPlies, ChatNotClear, DrawRefused, endGame, gameAt, gameToStudy, namedGame, newGame, offerDraw, playMove, samePosition, say, shownPosition, startNewGame, } from "../../commands/index.js";
2
2
  import { clockStarted, isReviewed, writePgn } from "../../core/index.js";
3
3
  import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, heardLines, loadGame, loadTableTalk, playerPgn, ponderingOn, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
4
4
  import { gameFolder, helpsIn } from "../../profiles/index.js";
@@ -72,28 +72,34 @@ async function moveFromPage(request, response, served) {
72
72
  served.bell.ring();
73
73
  send(response, 200, json, JSON.stringify({ played: true }));
74
74
  }
75
- /** The player's words from the page, without a move: saved with the game as player_say saves them, refused in the player's words, the doorbell rung for the first. */
75
+ /**
76
+ * The player's words from the page, without a move: saved with the game as player_say saves them, refused in the player's words, the doorbell rung for the
77
+ * first; in analysis with the position the page shows, and the doorbell rung for each (D-101).
78
+ */
76
79
  async function messageFromPage(request, response, served) {
77
80
  if (!isJson(request)) {
78
81
  send(response, 415, json, JSON.stringify({ error: 'Send the message as JSON: {"text": "…"}.' }));
79
82
  return;
80
83
  }
81
- const text = textOf(jsonOf(await bodyOf(request))?.text);
84
+ const body = jsonOf(await bodyOf(request));
85
+ const text = textOf(body?.text);
86
+ const shown = typeof body?.game === "string" && typeof body.at === "number" ? shownPosition(served.context, body.game, body.at) : undefined;
82
87
  if (text === undefined) {
83
88
  send(response, 400, json, JSON.stringify({ error: `Send the message as JSON, ${talkLimits.player} characters at most: {"text": "…"}.` }));
84
89
  return;
85
90
  }
86
91
  let current;
87
92
  try {
88
- current = say(gameFolder(served.context.home()), "player", text, served.context.now());
93
+ current = say(gameFolder(served.context.home()), "player", text, served.context.now(), undefined, shown);
89
94
  }
90
95
  catch (error) {
91
96
  const noGame = refusalOf(served)?.refusal === "no-game";
92
97
  send(response, 409, json, JSON.stringify(refusalFrom(error, noGame ? "no-game" : undefined)));
93
98
  return;
94
99
  }
95
- // The first words of a move wake the chat, and between games the first since the AI spoke; the AI finds the rest at its next call (D-54, D-74).
96
- if (wakesTheChat(current)) {
100
+ // The first words of a move wake the chat, and between games the first since the AI spoke; the AI finds the rest at its next call (D-54, D-74). In
101
+ // analysis every line does: a chat that answered in its own window would not hear the next (Priya).
102
+ if (shown !== undefined || wakesTheChat(current)) {
97
103
  served.bell.ring();
98
104
  }
99
105
  send(response, 200, json, JSON.stringify({ said: true }));
@@ -244,7 +250,14 @@ function sendGame(response, served) {
244
250
  // With no game the page still shows the talk before the first (D-77).
245
251
  const spoken = current ?? loadTableTalk(folder);
246
252
  const heard = heardLines(spoken, served.context.given);
247
- const talk = spoken.talk.map(({ who, text, ply }, index) => heard[index] === undefined ? { who, text, ply } : { who, text, ply, heard: heard[index] });
253
+ const talk = spoken.talk.map(({ who, text, ply, shown }, index) => ({
254
+ who,
255
+ text,
256
+ ply,
257
+ ...(heard[index] === undefined ? {} : { heard: heard[index] }),
258
+ // In analysis the page goes back to the move the player wrote about (D-101).
259
+ ...(shown === undefined ? {} : { shown }),
260
+ }));
248
261
  const offer = current === undefined ? undefined : drawOfferOf(current);
249
262
  const coach = coachOn(folder, current);
250
263
  const game = current === undefined ? null : {
@@ -4,7 +4,7 @@ import { watch } from "node:fs";
4
4
  import { createServer } from "node:http";
5
5
  import { join } from "node:path";
6
6
  import { positionIn, samePosition } from "../../commands/index.js";
7
- import { gameStamp } from "../../games/index.js";
7
+ import { gameStamp, tableTalkStamp } from "../../games/index.js";
8
8
  import { outdatedHostFiles } from "../../host-files/index.js";
9
9
  import { gameFolder } from "../../profiles/index.js";
10
10
  import { Bell, bellProbe, pagePorts } from "../bell.js";
@@ -111,12 +111,14 @@ function answer(request, response, served) {
111
111
  function hasNews(context) {
112
112
  return gameSeen(context) !== context.seen;
113
113
  }
114
- // The game the chat sees: the profile's folder, and the stamp of its game.json; none while the data folder cannot be made.
114
+ // The game the chat sees: the profile's folder, and the stamps of its game.json and of the talk with no game, where the player's words go in analysis
115
+ // before any game was played here (Priya); none while the data folder cannot be made.
115
116
  function gameSeen(context) {
116
117
  try {
117
118
  const folder = gameFolder(context.home());
118
119
  return `${folder}
119
- ${gameStamp(folder) ?? ""}`;
120
+ ${gameStamp(folder) ?? ""}
121
+ ${tableTalkStamp(folder) ?? ""}`;
120
122
  }
121
123
  catch {
122
124
  return "";
@@ -10,6 +10,6 @@ This folder is the player's chess table, and you are their opponent: you choose
10
10
  - You have a face on the board page, and it shows how you feel. Give it as the `mood` of `ai_move` on each move where the game turns for you: a piece won or lost, a blunder seen, your own or theirs, a threat that appears or a plan that works or fails. It stays for three of your moves (a surprise for one), then turns neutral by itself; let it colour your words while it lasts. When the game ends on the player's move, give how you feel as it ends with `ai_say`'s `mood`.
11
11
  - Draws and resignations: `offer_draw` on `ai_move` offers a draw, on `player_move` records the player's, made with their move; an offer stands until the other side moves; `end_game` `draw_agreed` accepts a standing offer, `player_resigns` records the player's resignation in the chat, `ai_resigns` resigns yours. On the board page the player offers, answers and resigns there.
12
12
  - The player chooses a new game in the board page's New game window, which asks the sides, the clock and the level: when they ask you for a game, call `new_game` without choices, which opens it there, and don't ask those in the chat. Ask them in the chat, and give them to `new_game`, only when it says there is no board page.
13
- - To go over a game with the player once no game goes on, the one just over or one they bring as a PGN, give it to `analyse_game`: it reads the PGN for you; where Chesspal's answers say to start the analyst agent for it, start that instead, once per game: when it has gone over the game, go over it with the player from what it told you, and never tell them it starts again, even when they ask for the analysis again. While a game goes on, go over no game, not even in your head or from a PGN they paste: tell them it waits until this one ends.
13
+ - To go over a game with the player once no game goes on, the one just over or one they bring as a PGN, give it to `analyse_game`: it reads the PGN for you; where Chesspal's answers say to start the analyst agent for it, start that instead, once per game, and never tell them it starts again, even when they ask for the analysis again. While it works, and after, talk the game over whenever the player asks: `look` and `position` with game and move show you any of its positions. When they show you something the review missed, first correct that move's note with `annotate_game` {game, notes: [{move, badge, note}]}, that move only, then answer. Words from the board page you answer with `ai_say`; words in the chat, in the chat. While a game goes on, go over no game, not even in your head or from a PGN they paste: tell them it waits until this one ends.
14
14
  - How to read the facts: attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now; "At risk" lists the pieces attacked and undefended, attacked by a cheaper piece, or attacked more times than defended.
15
15
  - Talk with the player in their language, in a few words. The facts and the tools are for you: tell the player what a chess player would say, never the lists or the tools' names. Name pieces and moves in words, as people talk at the board ("your bishop took on f7"); when you write a move, yours or the player's, write it with the pieces' figurines, as the tools show it (♞f6 for Nf6, ♝xf7+ for Bxf7+), for both sides and in every language: never with letters, neither N B R Q K nor the player's own (Cf6), even when they write them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chesspal",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
5
5
  "type": "module",
6
6
  "bin": {