chesspal 0.3.5 → 0.4.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 (191) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/cli.js +17 -4
  3. package/dist/cli/command-line.js +22 -12
  4. package/dist/cli/icon.d.ts +2 -0
  5. package/dist/cli/icon.js +11 -1
  6. package/dist/cli/prompt-hook.d.ts +17 -0
  7. package/dist/cli/prompt-hook.js +100 -0
  8. package/dist/commands/analysis/analyse.d.ts +2 -2
  9. package/dist/commands/analysis/analyse.js +10 -16
  10. package/dist/commands/analysis/annotate.d.ts +8 -0
  11. package/dist/commands/analysis/annotate.js +19 -3
  12. package/dist/commands/answers/agents.d.ts +17 -7
  13. package/dist/commands/answers/agents.js +46 -16
  14. package/dist/commands/answers/clock.js +2 -1
  15. package/dist/commands/answers/drawings.js +6 -2
  16. package/dist/commands/answers/index.d.ts +4 -3
  17. package/dist/commands/answers/index.js +4 -3
  18. package/dist/commands/answers/lines.d.ts +5 -3
  19. package/dist/commands/answers/lines.js +17 -9
  20. package/dist/commands/answers/opening-book.d.ts +13 -7
  21. package/dist/commands/answers/opening-book.js +34 -38
  22. package/dist/commands/answers/opening-tags.d.ts +4 -0
  23. package/dist/commands/answers/opening-tags.js +11 -0
  24. package/dist/commands/answers/play-move.d.ts +3 -1
  25. package/dist/commands/answers/play-move.js +9 -6
  26. package/dist/commands/answers/say.js +2 -1
  27. package/dist/commands/answers/summary.d.ts +7 -3
  28. package/dist/commands/answers/summary.js +16 -5
  29. package/dist/commands/answers/view.d.ts +1 -1
  30. package/dist/commands/answers/view.js +2 -2
  31. package/dist/commands/game/ai-move.js +4 -4
  32. package/dist/commands/game/coach.js +7 -6
  33. package/dist/commands/game/end.js +2 -2
  34. package/dist/commands/game/export.d.ts +1 -1
  35. package/dist/commands/game/index.d.ts +1 -1
  36. package/dist/commands/game/index.js +1 -1
  37. package/dist/commands/game/look.d.ts +7 -3
  38. package/dist/commands/game/look.js +9 -4
  39. package/dist/commands/game/new.js +42 -12
  40. package/dist/commands/game/player-move.d.ts +16 -0
  41. package/dist/commands/game/player-move.js +69 -19
  42. package/dist/commands/game/ponder.js +7 -4
  43. package/dist/commands/game/position.d.ts +2 -1
  44. package/dist/commands/game/position.js +9 -4
  45. package/dist/commands/game/status.d.ts +4 -2
  46. package/dist/commands/game/status.js +24 -14
  47. package/dist/commands/index.d.ts +3 -3
  48. package/dist/commands/index.js +3 -3
  49. package/dist/commands/profiles/past-games.d.ts +7 -2
  50. package/dist/commands/profiles/past-games.js +45 -18
  51. package/dist/commands/registry/context.d.ts +2 -0
  52. package/dist/commands/registry/context.js +5 -0
  53. package/dist/commands/registry/index.d.ts +1 -1
  54. package/dist/commands/registry/index.js +1 -1
  55. package/dist/commands/talk/ai-say.d.ts +1 -1
  56. package/dist/commands/talk/ai-say.js +7 -6
  57. package/dist/core/game.d.ts +13 -0
  58. package/dist/core/game.js +29 -3
  59. package/dist/core/index.d.ts +4 -4
  60. package/dist/core/index.js +3 -3
  61. package/dist/core/openings.d.ts +20 -6
  62. package/dist/core/openings.js +45 -22
  63. package/dist/core/pgn.d.ts +4 -0
  64. package/dist/core/pgn.js +35 -6
  65. package/dist/games/analysis.d.ts +7 -4
  66. package/dist/games/analysis.js +5 -3
  67. package/dist/games/archive.d.ts +25 -5
  68. package/dist/games/archive.js +52 -19
  69. package/dist/games/chat-start.d.ts +2 -2
  70. package/dist/games/chat-start.js +6 -1
  71. package/dist/games/current-game.d.ts +2 -2
  72. package/dist/games/current-game.js +3 -2
  73. package/dist/games/index.d.ts +3 -2
  74. package/dist/games/index.js +3 -2
  75. package/dist/games/position-shown.d.ts +24 -0
  76. package/dist/games/position-shown.js +51 -0
  77. package/dist/games/thoughts.d.ts +12 -4
  78. package/dist/games/thoughts.js +27 -10
  79. package/dist/home/index.d.ts +1 -0
  80. package/dist/home/index.js +1 -0
  81. package/dist/home/same-path.d.ts +7 -0
  82. package/dist/home/same-path.js +24 -0
  83. package/dist/host-files/host-files.d.ts +5 -0
  84. package/dist/host-files/host-files.js +21 -7
  85. package/dist/host-files/index.d.ts +1 -1
  86. package/dist/host-files/index.js +1 -1
  87. package/dist/page/app/anchor.js +15 -11
  88. package/dist/page/app/app.js +49 -14
  89. package/dist/page/app/served.js +30 -38
  90. package/dist/page/app/watch.js +97 -0
  91. package/dist/page/avatars/torretta.css +108 -0
  92. package/dist/page/base.css +1 -1
  93. package/dist/page/board/board.css +11 -8
  94. package/dist/page/board/marks.js +2 -2
  95. package/dist/page/board/pointer.js +2 -2
  96. package/dist/page/dialogs/analyses.js +5 -3
  97. package/dist/page/dialogs/dialogs.css +3 -1
  98. package/dist/page/dialogs/rows.js +72 -0
  99. package/dist/page/dialogs/site-games.js +3 -1
  100. package/dist/page/game/column.js +10 -8
  101. package/dist/page/game/game.css +137 -16
  102. package/dist/page/game/move-list.js +7 -5
  103. package/dist/page/game/plies.js +4 -0
  104. package/dist/page/game/review-progress.js +11 -0
  105. package/dist/page/guide/drawings/drawings.css +0 -1
  106. package/dist/page/guide/en.md +2 -2
  107. package/dist/page/guide/it.md +2 -2
  108. package/dist/page/helps/helps.css +5 -5
  109. package/dist/page/helps/helps.js +3 -181
  110. package/dist/page/helps/rows.js +16 -0
  111. package/dist/page/helps/strategy.js +105 -0
  112. package/dist/page/helps/tactics.js +69 -0
  113. package/dist/page/layout/header.js +49 -6
  114. package/dist/page/layout/icon.ico +0 -0
  115. package/dist/page/layout/layout.css +49 -17
  116. package/dist/page/layout/window.js +45 -6
  117. package/dist/page/lead/lead.js +41 -4
  118. package/dist/page/legend/legend.css +4 -16
  119. package/dist/page/legend/legend.js +28 -15
  120. package/dist/page/profile/settings.js +1 -0
  121. package/dist/page/sounds/sounds.js +1 -1
  122. package/dist/page/style.css +1 -0
  123. package/dist/page/table/action.js +42 -0
  124. package/dist/page/table/table.css +57 -39
  125. package/dist/page/table/window.js +67 -67
  126. package/dist/page/talk/talk.css +52 -118
  127. package/dist/page/talk/talk.js +65 -20
  128. package/dist/page/talk/voice.js +92 -21
  129. package/dist/page/text.js +34 -8
  130. package/dist/page/theme.css +13 -0
  131. package/dist/profiles/profiles.js +5 -1
  132. package/dist/server/bell/bell.d.ts +24 -29
  133. package/dist/server/bell/bell.js +79 -133
  134. package/dist/server/bell/index.d.ts +1 -1
  135. package/dist/server/bell/index.js +2 -2
  136. package/dist/server/mcp/mcp.js +49 -7
  137. package/dist/server/page/analyses.js +15 -4
  138. package/dist/server/page/chat.d.ts +5 -1
  139. package/dist/server/page/chat.js +5 -1
  140. package/dist/server/page/files.js +3 -1
  141. package/dist/server/page/game.d.ts +5 -0
  142. package/dist/server/page/game.js +95 -15
  143. package/dist/server/page/http.d.ts +1 -1
  144. package/dist/server/page/page.d.ts +14 -9
  145. package/dist/server/page/page.js +111 -26
  146. package/dist/server/page/table.d.ts +40 -5
  147. package/dist/server/page/table.js +65 -25
  148. package/dist/server/page/update.d.ts +1 -1
  149. package/dist/server/page/update.js +4 -4
  150. package/dist/server/page/windows.d.ts +22 -0
  151. package/dist/server/page/windows.js +45 -0
  152. package/dist/server/ports/index.d.ts +1 -0
  153. package/dist/server/ports/index.js +2 -0
  154. package/dist/server/ports/ports.d.ts +78 -0
  155. package/dist/server/ports/ports.js +222 -0
  156. package/dist/server/table/browser.d.ts +22 -6
  157. package/dist/server/table/browser.js +98 -20
  158. package/dist/server/table/hosts/chats.d.ts +5 -0
  159. package/dist/server/table/hosts/chats.js +11 -4
  160. package/dist/server/table/hosts/hooks.d.ts +6 -0
  161. package/dist/server/table/hosts/hooks.js +38 -0
  162. package/dist/server/table/hosts/hosts.d.ts +13 -5
  163. package/dist/server/table/hosts/hosts.js +16 -10
  164. package/dist/server/table/hosts/index.d.ts +1 -0
  165. package/dist/server/table/hosts/index.js +1 -0
  166. package/dist/server/table/hosts/play.d.ts +7 -3
  167. package/dist/server/table/hosts/play.js +63 -9
  168. package/dist/server/table/icon/index.d.ts +2 -0
  169. package/dist/server/table/icon/index.js +16 -2
  170. package/dist/server/table/index.d.ts +2 -2
  171. package/dist/server/table/index.js +1 -1
  172. package/dist/server/table/launch.d.ts +5 -4
  173. package/dist/server/table/launch.js +18 -5
  174. package/dist/server/table/link.js +4 -1
  175. package/dist/server/table/table.d.ts +7 -0
  176. package/dist/server/table/table.js +40 -12
  177. package/dist/sites/index.d.ts +1 -1
  178. package/dist/sites/index.js +1 -1
  179. package/dist/sites/{chesscom.d.ts → ratings/chesscom.d.ts} +2 -2
  180. package/dist/sites/{chesscom.js → ratings/chesscom.js} +1 -1
  181. package/dist/sites/{choice.d.ts → ratings/choice.d.ts} +1 -1
  182. package/dist/sites/{choice.js → ratings/choice.js} +1 -1
  183. package/dist/sites/ratings/index.d.ts +1 -0
  184. package/dist/sites/ratings/index.js +2 -0
  185. package/dist/sites/{lichess.d.ts → ratings/lichess.d.ts} +2 -2
  186. package/dist/sites/{lichess.js → ratings/lichess.js} +1 -1
  187. package/dist/sites/{sites.d.ts → ratings/ratings.d.ts} +2 -2
  188. package/dist/sites/{sites.js → ratings/ratings.js} +1 -1
  189. package/instructions/opponent.md +1 -1
  190. package/instructions/start.md +1 -1
  191. package/package.json +1 -1
@@ -1,12 +1,13 @@
1
- import { analyseGame, analystOf, answerDraw, bookPlies, ChatNotClear, DrawRefused, endGame, gameAt, gameToStudy, namedGame, newGame, offerDraw, playMove, samePosition, say, shownPosition, 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
- import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, heardLines, loadGame, loadTableTalk, playerPgn, ponderingOn, rerunIfChanged, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
3
+ import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, GameMissing, heardLines, loadGame, loadTableTalk, playerPgn, ponderingUntil, rerunIfChanged, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
4
4
  import { gameFolder, helpsIn } from "../../profiles/index.js";
5
5
  import { version } from "../../version.js";
6
6
  import { asTheChatSees } from "./chat.js";
7
+ import { tellWatchers } from "./events.js";
7
8
  import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
8
9
  import { profilesShown } from "./profiles.js";
9
- import { chatToRelaunch, relaunchChat, tableShown } from "./table.js";
10
+ import { analystAtWork, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
10
11
  import { updateShown } from "./update.js";
11
12
  /** Answers the page's requests about the game: true when the request was one. */
12
13
  export function answerGame(request, response, url, served) {
@@ -26,6 +27,18 @@ export function chatCalls(served) {
26
27
  rerunIfChanged(() => giveHeldTime(served, folder));
27
28
  }
28
29
  }
30
+ /**
31
+ * Whether the AI's sign on the page is not the one the pages were last given: the ponder agent's every thought rewrites thoughts.json, and only the sign
32
+ * that comes or goes is news for them. True when the game cannot be read: they read it again.
33
+ */
34
+ export function thinkingChanged(served) {
35
+ try {
36
+ return thinkingShown(served, loadGame(gameFolder(served.context.home()))).thinking !== thinkingSent.get(served);
37
+ }
38
+ catch {
39
+ return true;
40
+ }
41
+ }
29
42
  /** The AI's clock, held while nobody heard the player, starts now: the time it was held is not the AI's. */
30
43
  function giveHeldTime(served, folder) {
31
44
  const current = loadGame(folder);
@@ -111,19 +124,21 @@ async function messageFromPage(request, response, served) {
111
124
  // 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
112
125
  // analysis every line does: a chat that answered in its own window would not hear the next (Priya).
113
126
  if (shown !== undefined || wakesTheChat(current)) {
114
- served.bell.ring();
127
+ // Words may come while the chat thinks on the AI's move, which they do not end.
128
+ served.bell.ring("game" in current && endOf(current.game) === undefined && current.game.position.turn === current.ai);
115
129
  }
116
130
  send(response, 200, json, JSON.stringify({ said: true }));
117
131
  }
118
132
  /**
119
133
  * A new game from the page, with the sides, the level and the clock the player chose: the sides and the level they leave out are as in the game
120
134
  * before, and no clock means a game without one. One going on goes to the archive unfinished, and the doorbell tells the chat, unless the table starts
121
- * it again.
135
+ * it again. A chat the table holds that must be clear for it, and is at work, has the game wait with the choices kept until it has ended its answer (D-107).
122
136
  */
123
137
  async function newGameFromPage(request, response, served) {
124
138
  const asked = 'Send the new game as JSON: {"ai_colour": "black", "level": "club", "clock": "10+5"}, each one optional; no clock for a game without one.';
125
139
  const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
126
140
  let relaunching = false;
141
+ let chosen;
127
142
  if (body === undefined) {
128
143
  send(response, isJson(request) ? 400 : 415, json, JSON.stringify({ error: asked }));
129
144
  return;
@@ -138,10 +153,17 @@ async function newGameFromPage(request, response, served) {
138
153
  // The game starts from the page, open: its clocks wait for the player's Start there. A chat the table starts again is clean: no /clear is asked.
139
154
  relaunching = chatToRelaunch(served);
140
155
  const sight = relaunching ? served.context : asTheChatSees(served.context, served.chat);
156
+ chosen = input.data;
141
157
  rerunIfChanged(() => startNewGame(input.data, { ...sight, pageOpen: () => true }, "page"));
142
158
  }
143
159
  catch (error) {
144
- send(response, 409, json, JSON.stringify(refusalFrom(error)));
160
+ if (error instanceof ChatNotClear && chosen !== undefined && chatToWaitFor(served)) {
161
+ waitForChat(served, chosen);
162
+ send(response, 200, json, JSON.stringify({ waiting: true }));
163
+ }
164
+ else {
165
+ send(response, 409, json, JSON.stringify(refusalFrom(error)));
166
+ }
145
167
  return;
146
168
  }
147
169
  // A seat lost meanwhile leaves the chat as it is: the doorbell tells it.
@@ -150,6 +172,34 @@ async function newGameFromPage(request, response, served) {
150
172
  }
151
173
  send(response, 200, json, JSON.stringify({ started: true }));
152
174
  }
175
+ /**
176
+ * The new game waits, with the player's choices, for the chat at the table to end its answer; then it starts and the chat starts again, clean, for it, or
177
+ * one gone meanwhile hears the doorbell when another comes. It starts in the profile it was asked in: with another in use by then it is dropped. A game
178
+ * that cannot start leaves everything as it was, and the pages say why.
179
+ */
180
+ function waitForChat(served, chosen) {
181
+ const askedIn = gameFolder(served.context.home());
182
+ const start = () => {
183
+ try {
184
+ if (gameFolder(served.context.home()) !== askedIn) {
185
+ refuseWaited(served, { error: "Another profile is in use since the new game was asked: ask for it again in this one.", refusal: "new-game-profile" });
186
+ return false;
187
+ }
188
+ rerunIfChanged(() => startNewGame(chosen, { ...served.context, pageOpen: () => true }, "page"));
189
+ }
190
+ catch (error) {
191
+ refuseWaited(served, refusalFrom(error));
192
+ return false;
193
+ }
194
+ const relaunching = relaunchChat(served);
195
+ if (!relaunching) {
196
+ served.bell.ring();
197
+ }
198
+ return relaunching;
199
+ };
200
+ served.chatWait = { for: "new-game", start };
201
+ tellWatchers(served.watchers);
202
+ }
153
203
  /** The player's draw from the page (D-65): an offer on their turn, which rings the doorbell, or their answer to the AI's, which rings it when accepted. */
154
204
  async function drawFromPage(request, response, served) {
155
205
  const asked = 'Send the draw as JSON: {"action": "offer"}, "accept" or "decline".';
@@ -233,6 +283,8 @@ async function studyFromPage(request, response, served) {
233
283
  const last = body?.last === true;
234
284
  // A game a link of the page opens is there to look at: the chat is not asked to go over it.
235
285
  const look = body?.look === true;
286
+ // «Analyse again», the review just deleted: the chat starts the analyst without asking, even after one was stopped (Priya).
287
+ const redo = body?.redo === true;
236
288
  if (pgn === undefined && game === undefined && !last) {
237
289
  send(response, 400, json, JSON.stringify({ error: asked, refusal: "pgn-unreadable" }));
238
290
  return;
@@ -249,10 +301,10 @@ async function studyFromPage(request, response, served) {
249
301
  reviewed = isReviewed(opened.record);
250
302
  brought = !reviewed && !look;
251
303
  // A game not yet reviewed is marked as brought, for status to ask the AI to go over it; one reviewed is ready, and the chat sleeps on.
252
- startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), { brought });
304
+ startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), { brought, redo });
253
305
  }
254
306
  catch (error) {
255
- send(response, 400, json, JSON.stringify({ error: error.message, refusal: "pgn-unreadable" }));
307
+ send(response, 400, json, JSON.stringify({ error: error.message, refusal: refusalOfStudy(error) }));
256
308
  return;
257
309
  }
258
310
  if (brought) {
@@ -289,13 +341,15 @@ function sendGame(response, served) {
289
341
  name: archiveName(current),
290
342
  talk,
291
343
  // How many of its first moves are the book's: the page marks them while the game goes on, as the AI's review does after.
292
- book: bookPlies(current.game.record),
344
+ book: bookPlies(current.game),
293
345
  ...(current.clock === undefined ? {} : { clock: current.clock }),
294
346
  ...(offer === undefined ? {} : { drawOffer: offer }),
295
347
  ...(coach === undefined ? {} : { coach }),
296
348
  };
297
349
  const analysis = current === undefined || endOf(current.game) !== undefined ? analysisShown(served) : undefined;
298
350
  const aiModel = aiModelOf(folder);
351
+ const thinking = thinkingShown(served, current);
352
+ thinkingSent.set(served, thinking.thinking);
299
353
  const body = {
300
354
  // The page loaded from another version loads again, its files and all (Federico).
301
355
  version,
@@ -305,7 +359,7 @@ function sendGame(response, served) {
305
359
  opponent: served.opponent(),
306
360
  ...(aiModel === undefined ? {} : { aiModel }),
307
361
  listening: served.bell.listening,
308
- ...thinkingShown(served, current),
362
+ ...thinking,
309
363
  ...(served.outdated ? { outdated: true } : {}),
310
364
  ...updateShown(served),
311
365
  ...tableShown(served),
@@ -327,7 +381,21 @@ function thinkingShown(served, current) {
327
381
  const onIt = served.bell.woke || (seen !== undefined && seen !== null && samePosition(seen, position));
328
382
  return served.bell.inTurn && onIt ? { thinking: "move" } : {};
329
383
  }
330
- return ponderingOn(gameFolder(served.context.home()), thoughtKey(current)) ? { thinking: "ponder" } : {};
384
+ const until = ponderingUntil(gameFolder(served.context.home()), thoughtKey(current));
385
+ const left = until === undefined ? 0 : until.getTime() - served.context.now().getTime();
386
+ if (left <= 0) {
387
+ return {};
388
+ }
389
+ signGoesIn(served, left);
390
+ return { thinking: "ponder" };
391
+ }
392
+ // The AI's sign the pages were last given, one a table.
393
+ const thinkingSent = new WeakMap();
394
+ // The pages hear when the sign of an agent that stopped pondering without a word goes, which no file tells: one timer a table, the latest.
395
+ const signTimers = new WeakMap();
396
+ function signGoesIn(served, wait) {
397
+ clearTimeout(signTimers.get(served));
398
+ signTimers.set(served, setTimeout(() => tellWatchers(served.watchers), wait + 100).unref());
331
399
  }
332
400
  /**
333
401
  * What the coach wrote that the player may see now, in a game going on with the helps on (D-88): its judgments, those whose note names a move they may still
@@ -353,11 +421,10 @@ function analysisShown(served) {
353
421
  try {
354
422
  const { name, record } = namedGame(served.context, analysis.game);
355
423
  // Without a review, an AI is at it only while the chat is in a turn, or hears the player while its analyst holds the game (D-89): one the chat has
356
- // started again since is gone (Priya). On the table the chat's start is in the chat's own folder.
424
+ // started again since is gone (Priya).
357
425
  const { bell } = served;
358
- const chat = { ...served.context, folder: served.attached?.chat.folder ?? served.folder };
359
426
  const reviewed = isReviewed(record);
360
- const reviewing = !reviewed && (bell.inTurn || (bell.listening && analystOf(chat, analysis.game) !== undefined));
427
+ const reviewing = !reviewed && (bell.inTurn || (bell.listening && analystAtWork(served, analysis.game)));
361
428
  // Without a review and with nobody at it, the analysis is incomplete: the page offers to bring it back to the AI (D-109).
362
429
  const state = reviewed ? {} : reviewing ? { reviewing: true } : { incomplete: true };
363
430
  // How far the book goes, which the page cannot tell without the book's moves: its card names the opening on those moves.
@@ -372,10 +439,23 @@ function analysisShown(served) {
372
439
  function lastPlayed(context) {
373
440
  const { name, record } = gameToStudy(context);
374
441
  if (name === undefined) {
375
- throw new Error("The game just over is not in the archive yet: try again in a moment.");
442
+ throw new GameNotArchived();
376
443
  }
377
444
  return { name, record };
378
445
  }
446
+ /** The refusal of the game just over asked to analyse before the archive keeps it. */
447
+ class GameNotArchived extends Error {
448
+ constructor() {
449
+ super("The game just over is not in the archive yet: try again in a moment.");
450
+ }
451
+ }
452
+ /** Why a game to study was refused, for the page to say it: a name no game has, the game just over not kept yet, or a game that cannot be read. */
453
+ function refusalOfStudy(error) {
454
+ if (error instanceof GameMissing) {
455
+ return "game-missing";
456
+ }
457
+ return error instanceof GameNotArchived ? "game-not-archived" : "pgn-unreadable";
458
+ }
379
459
  /** A game brought as a PGN, kept to study as analyse_game keeps it: the one already kept when it is the same game, with its review. */
380
460
  async function kept(served, pgn) {
381
461
  await analyseGame.run({ pgn }, served.context);
@@ -8,7 +8,7 @@ export declare const json = "application/json; charset=utf-8";
8
8
  */
9
9
  export interface Refused {
10
10
  readonly error: string;
11
- readonly refusal?: "no-game" | "ai-to-move" | "illegal" | "clear-chat" | `words-${WordsRefused["reason"]}` | `draw-${DrawRefused["reason"]}` | "profile-game" | "game-going" | "pgn-unreadable" | "update-game" | "update-chat";
11
+ readonly refusal?: "no-game" | "ai-to-move" | "illegal" | "clear-chat" | "new-game-profile" | `words-${WordsRefused["reason"]}` | `draw-${DrawRefused["reason"]}` | "profile-game" | "game-going" | "pgn-unreadable" | "game-missing" | "game-not-archived" | "update-game" | "update-chat";
12
12
  readonly most?: number;
13
13
  }
14
14
  /** Answers a request, with nothing cached, sniffed or referred, and the page's policy unless the headers give another. */
@@ -2,7 +2,8 @@ import { type ServerResponse } from "node:http";
2
2
  import { type Context } from "../../commands/index.ts";
3
3
  import { Bell } from "../bell/index.ts";
4
4
  import { type Attached, type ChatSeen } from "./chat.ts";
5
- import { type Opening, type TableChats } from "./table.ts";
5
+ import { type ChatWait, type Opening, type TableChats, type WaitRefused } from "./table.ts";
6
+ import { type TableWindows } from "./windows.ts";
6
7
  /**
7
8
  * The page of a player's folder, served by the chat's process or by the table it is attached to (D-104): its address, how to tell it what the chat does
8
9
  * and was given, which change no file, and how to stop it.
@@ -15,6 +16,8 @@ export interface Page {
15
16
  chatCalls(): void | Promise<void>;
16
17
  /** A tool of the chat's has ended: the table gets a copy of what the chat saw; the chat's own page reads it from the chat. */
17
18
  toolEnded(): void;
19
+ /** Whether the doorbell finds the page: on its folder's usual ports, at the port it noted in the data folder, or at the table. */
20
+ readonly findable: boolean;
18
21
  /** Whether a browser has the page open now: one listens to the game's changes. */
19
22
  open(): boolean;
20
23
  /** Whether the page is in use: open in a browser, or, on the table, with a chat attached. */
@@ -44,8 +47,8 @@ export interface TableUpdate {
44
47
  restart(): void;
45
48
  }
46
49
  /**
47
- * Serves the page of the context's folder on 127.0.0.1, on a port of its own when free, else on any free one, where the doorbell cannot find it; the
48
- * table's on its own ports alone, which keep two tables apart. The page reads the chat from the context in the chat's process, and on the table from
50
+ * Serves the page of the context's folder on 127.0.0.1, on a port of its own when free, else on any free one, which it notes in the data folder for the
51
+ * doorbell to find it there; the table's on its own ports alone, which keep two tables apart. The page reads the chat from the context in the chat's process, and on the table from
49
52
  * what the chat attached to it tells it (D-104); it names the opponent as the chat's host does. The doorbell knows what the chat has read.
50
53
  */
51
54
  export declare function startPage(context: Context, options?: PageOptions): Promise<Page>;
@@ -75,16 +78,18 @@ export interface Served {
75
78
  readonly at: number;
76
79
  readonly how: Opening;
77
80
  } | undefined;
78
- /** The host the page asked the chat to start again with while it was at work (D-107). */
79
- restartWanted?: {
80
- readonly host: string;
81
- } | undefined;
81
+ /** What the page asked while the chat was at work, waiting for it to end its answer: a start again with another host, or a new game (D-107). */
82
+ chatWait?: ChatWait | undefined;
83
+ /** Why the last new game that waited for the chat could not start, numbered so that each page says it once. */
84
+ waitRefused?: WaitRefused | undefined;
82
85
  /** The newer Chesspal npm has (D-108); the table installing it, installed and waiting to be at rest, or starting again from it. */
83
86
  newer?: string;
84
87
  updating?: "installing" | "installed" | "restarting" | undefined;
88
+ /** The windows the table opened: the newest stays, the others close (D-104). */
89
+ windows?: TableWindows;
85
90
  }
86
91
  /**
87
92
  * The chat has read the game as it is now, at the end of a tool's call: the doorbell wakes it only for what changes it after; and, when the tool `showed` it
88
- * the game, it moves on that position and no other until a tool shows it another (D-71). A game that cannot be read shows none.
93
+ * the game, it moves on that position and no other until a tool shows it another (D-71). A game that cannot be read shows none. The data folder is the call's.
89
94
  */
90
- export declare function chatHasRead(context: Context, showed: boolean): void;
95
+ export declare function chatHasRead(context: Context, showed: boolean, home?: () => string): void;
@@ -2,26 +2,28 @@
2
2
  // kept up to date as the game changes; the doorbell wakes the chat when the player acts there.
3
3
  import { watch } from "node:fs";
4
4
  import { createServer } from "node:http";
5
- import { join } from "node:path";
6
- import { positionIn, samePosition } from "../../commands/index.js";
5
+ import { basename, join } from "node:path";
6
+ import { analystQuietAt, positionIn, samePosition } from "../../commands/index.js";
7
7
  import { aiNameOf } from "../../core/index.js";
8
8
  import { forgetAnalyst, gameStamp, tableTalkStamp } from "../../games/index.js";
9
9
  import { outdatedHostFiles } from "../../host-files/index.js";
10
10
  import { gameFolder } from "../../profiles/index.js";
11
- import { Bell, bellProbe, pagePorts } from "../bell/index.js";
11
+ import { Bell, forgetPagePort, notePagePort } from "../bell/index.js";
12
+ import { bellProbe, pagePorts } from "../ports/index.js";
12
13
  import { analysesOf, answerAnalyses, sendSiteGames } from "./analyses.js";
13
14
  import { answerChat } from "./attach.js";
14
15
  import { attachedSeen, chatSeenOf, pageContext } from "./chat.js";
15
16
  import { askNewGame, keepPosted, tellWatchers } from "./events.js";
16
17
  import { sendFile, sendHtml } from "./files.js";
17
- import { answerGame, chatCalls } from "./game.js";
18
+ import { answerGame, chatCalls, thinkingChanged } from "./game.js";
18
19
  import { json, send } from "./http.js";
19
20
  import { answerProfiles, readRatingsOf } from "./profiles.js";
20
- import { answerTable, restartWhenAtRest, updateWhenAtRest } from "./table.js";
21
+ import { answerTable, startWhenAtRest, updateWhenAtRest } from "./table.js";
21
22
  import { watchForUpdates } from "./update.js";
23
+ import { windowConnected } from "./windows.js";
22
24
  /**
23
- * Serves the page of the context's folder on 127.0.0.1, on a port of its own when free, else on any free one, where the doorbell cannot find it; the
24
- * table's on its own ports alone, which keep two tables apart. The page reads the chat from the context in the chat's process, and on the table from
25
+ * Serves the page of the context's folder on 127.0.0.1, on a port of its own when free, else on any free one, which it notes in the data folder for the
26
+ * doorbell to find it there; the table's on its own ports alone, which keep two tables apart. The page reads the chat from the context in the chat's process, and on the table from
25
27
  * what the chat attached to it tells it (D-104); it names the opponent as the chat's host does. The doorbell knows what the chat has read.
26
28
  */
27
29
  export async function startPage(context, options = {}) {
@@ -34,9 +36,9 @@ export async function startPage(context, options = {}) {
34
36
  context: pageContext(context),
35
37
  chat,
36
38
  opponent: options.opponent ?? (() => aiNameOf(chat.host())),
37
- // A start again asked while the chat was at work comes once it has ended its answer (D-107), as an update installed meanwhile (D-108).
39
+ // A start again or a new game asked while the chat was at work comes once it has ended its answer (D-107), as an update installed meanwhile (D-108).
38
40
  bell: new Bell(() => hasNews(context, chat), () => {
39
- restartWhenAtRest(served);
41
+ startWhenAtRest(served);
40
42
  updateWhenAtRest(served);
41
43
  tellWatchers(watchers);
42
44
  }),
@@ -46,19 +48,36 @@ export async function startPage(context, options = {}) {
46
48
  ...(table === undefined ? {} : { table }),
47
49
  };
48
50
  const server = createServer((request, response) => answer(request, response, served));
49
- const ports = table === undefined ? [...pagePorts(folder), 0] : pagePorts(folder);
51
+ // The folder's real path is read once: it costs a call to the disk for each part of the path.
52
+ const usual = pagePorts(folder);
53
+ const ports = table === undefined ? [...usual, 0] : usual;
50
54
  for (const port of ports) {
51
55
  if (await listened(server, port)) {
52
56
  server.unref();
53
57
  served.port = portOf(server);
58
+ // Noted before the game is watched: the notes' folder, made in the data folder, is no news for the pages.
59
+ const offPorts = table === undefined && !usual.includes(served.port);
60
+ const noted = offPorts ? notePort(served) : undefined;
54
61
  const stopWatchingGame = watchGame(served);
55
62
  const stopAskingNpm = watchForUpdates(served);
63
+ // A chat's process ends without closing its page: the note goes with it.
64
+ const forgetPort = () => {
65
+ if (noted !== undefined) {
66
+ forgetPagePort(noted, folder, served.port);
67
+ }
68
+ };
69
+ if (noted !== undefined) {
70
+ process.once("exit", forgetPort);
71
+ }
56
72
  const stopWatching = () => {
57
73
  stopWatchingGame();
58
74
  stopAskingNpm();
75
+ forgetPort();
76
+ process.off("exit", forgetPort);
59
77
  };
60
78
  return {
61
79
  url: `http://127.0.0.1:${served.port}/`,
80
+ findable: !offPorts || noted !== undefined,
62
81
  heard: () => tellWatchers(watchers),
63
82
  chatCalls: () => chatCalls(served),
64
83
  toolEnded: () => undefined,
@@ -76,14 +95,14 @@ export async function startPage(context, options = {}) {
76
95
  }
77
96
  /**
78
97
  * The chat has read the game as it is now, at the end of a tool's call: the doorbell wakes it only for what changes it after; and, when the tool `showed` it
79
- * the game, it moves on that position and no other until a tool shows it another (D-71). A game that cannot be read shows none.
98
+ * the game, it moves on that position and no other until a tool shows it another (D-71). A game that cannot be read shows none. The data folder is the call's.
80
99
  */
81
- export function chatHasRead(context, showed) {
82
- context.seen = gameSeen(context);
100
+ export function chatHasRead(context, showed, home = context.home) {
101
+ context.seen = gameSeen(home);
83
102
  if (showed) {
84
103
  const before = context.positionSeen;
85
104
  try {
86
- context.positionSeen = positionIn(gameFolder(context.home()));
105
+ context.positionSeen = positionIn(gameFolder(home()));
87
106
  }
88
107
  catch {
89
108
  context.positionSeen = null;
@@ -136,6 +155,7 @@ function answer(request, response, served) {
136
155
  }
137
156
  else if (path === "/api/events") {
138
157
  keepPosted(request, response, served.watchers);
158
+ windowConnected(served, url);
139
159
  // A page that opens reads the player's ratings again after a day (D-106).
140
160
  readRatingsOf(served);
141
161
  }
@@ -147,13 +167,29 @@ function answer(request, response, served) {
147
167
  if (url.searchParams.get("analyst") === "gone") {
148
168
  forgetGoneAnalyst(served);
149
169
  }
150
- served.bell.listen(request, response);
170
+ // Nothing changed the game since the prompt hook played the move for the chat: the chat hears of it from the player's message.
171
+ const shown = url.searchParams.get("shown");
172
+ served.bell.listen(request, response, shown !== null && shown === stampNow(served));
151
173
  }
152
174
  }
175
+ else if (path === "/api/bell/prompt") {
176
+ // The prompt hook: a message starts the chat's turn. A path of its own, which a page of an older Chesspal answers with a 404, not as a hook waiting.
177
+ served.bell.chatPrompted();
178
+ send(response, 200, "text/plain; charset=utf-8", "at work");
179
+ }
153
180
  else {
154
181
  sendFile(response, path);
155
182
  }
156
183
  }
184
+ /** The stamp of the game in the data folder now; none when there is none or the folder cannot be read. */
185
+ function stampNow(served) {
186
+ try {
187
+ return gameStamp(gameFolder(served.context.home()));
188
+ }
189
+ catch {
190
+ return undefined;
191
+ }
192
+ }
157
193
  /** The chat's hook says no analyst runs any more (D-109): the game it held is free, and its review, unless finished, incomplete. */
158
194
  function forgetGoneAnalyst(served) {
159
195
  try {
@@ -176,13 +212,13 @@ function tableChat(served) {
176
212
  * flag that fell, or another profile in use, which wakes the chat at the next ring (D-66). The player's words the chat was given change nothing it has not read.
177
213
  */
178
214
  function hasNews(context, chat) {
179
- return gameSeen(context) !== chat.seen;
215
+ return gameSeen(context.home) !== chat.seen;
180
216
  }
181
217
  // 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
182
218
  // before any game was played here (Priya); none while the data folder cannot be made.
183
- function gameSeen(context) {
219
+ function gameSeen(home) {
184
220
  try {
185
- const folder = gameFolder(context.home());
221
+ const folder = gameFolder(home());
186
222
  return `${folder}
187
223
  ${gameStamp(folder) ?? ""}
188
224
  ${tableTalkStamp(folder) ?? ""}`;
@@ -198,19 +234,46 @@ ${tableTalkStamp(folder) ?? ""}`;
198
234
  */
199
235
  function watchGame(served) {
200
236
  let waiting;
237
+ let quiet;
201
238
  let watching = [];
239
+ let game;
240
+ // An analyst that writes nothing for half an hour counts as gone, which no file tells: the pages, and what waits for the chat at rest, hear it then.
241
+ const analystGoesQuiet = () => {
242
+ clearTimeout(quiet);
243
+ const left = analystQuietIn(served);
244
+ quiet = left === undefined ? undefined : setTimeout(() => {
245
+ tellWatchers(served.watchers);
246
+ startWhenAtRest(served);
247
+ updateWhenAtRest(served);
248
+ }, left + 100).unref();
249
+ };
202
250
  const watchAll = () => {
203
251
  watching.forEach((watcher) => watcher.close());
204
- watching = watchedFolders(served).flatMap((folder) => watchFolder(folder, heard));
252
+ const folders = watchedFolders(served);
253
+ game = folders.length === 0 ? undefined : basename(folders.at(-1));
254
+ watching = folders.flatMap((folder) => watchFolder(folder, heard));
255
+ analystGoesQuiet();
205
256
  };
206
- // A save writes a new file and renames it: the events of one save come together, and the pages hear of it once.
207
- const heard = (name) => {
208
- if ((name === null || watched.test(name)) && waiting === undefined) {
257
+ // A save writes a new file and renames it: the events of one save come together, and the pages hear of it once. Of the ponder agent's thoughts,
258
+ // only its sign coming or going.
259
+ let news = false;
260
+ // On Windows the profiles' folder hears every write in the game's folder, as a change of it: the game folder's own watcher hears what it was.
261
+ const heard = (name, event) => {
262
+ if (name !== null && (!watched.test(name) || (event === "change" && name === game))) {
263
+ return;
264
+ }
265
+ news ||= name !== "thoughts.json";
266
+ if (waiting === undefined) {
209
267
  waiting = setTimeout(() => {
210
268
  waiting = undefined;
269
+ const tell = news || thinkingChanged(served);
270
+ news = false;
211
271
  watchAll();
212
- tellWatchers(served.watchers);
213
- // A game that has ended lets an update installed meanwhile start the table again (D-108).
272
+ if (tell) {
273
+ tellWatchers(served.watchers);
274
+ }
275
+ // An analyst that has ended lets what waited for the chat start (D-107), and a game that has ended an update installed meanwhile (D-108).
276
+ startWhenAtRest(served);
214
277
  updateWhenAtRest(served);
215
278
  }, 30);
216
279
  }
@@ -218,10 +281,21 @@ function watchGame(served) {
218
281
  watchAll();
219
282
  return () => {
220
283
  clearTimeout(waiting);
284
+ clearTimeout(quiet);
221
285
  watching.forEach((watcher) => watcher.close());
222
286
  };
223
287
  }
224
- // None while the data folder cannot be made: each tool call tells the chat why, and the page reads the game as it loads.
288
+ // How long the analyst at work may still write nothing before it counts as gone; none without one, one gone already, or while the data folder cannot be made.
289
+ function analystQuietIn(served) {
290
+ try {
291
+ const left = (analystQuietAt(served.context)?.getTime() ?? 0) - served.context.now().getTime();
292
+ return left > 0 ? left : undefined;
293
+ }
294
+ catch {
295
+ return undefined;
296
+ }
297
+ }
298
+ // None while the data folder cannot be made: each tool call tells the chat why, and the page reads the game as it loads. The game's folder last.
225
299
  function watchedFolders(served) {
226
300
  try {
227
301
  const home = served.context.home();
@@ -234,7 +308,7 @@ function watchedFolders(served) {
234
308
  // A folder watched, without its subfolders; none when it cannot be, or is not there yet: the pages see its files only as they load them.
235
309
  function watchFolder(folder, heard) {
236
310
  try {
237
- const watcher = watch(folder, (event, name) => heard(name));
311
+ const watcher = watch(folder, (event, name) => heard(name, event));
238
312
  watcher.on("error", () => watcher.close());
239
313
  watcher.unref();
240
314
  return [watcher];
@@ -254,6 +328,17 @@ function fromHere(request, port, api) {
254
328
  return hosts.includes(request.headers.host ?? "") && (origin === undefined || hosts.some((host) => origin === `http://${host}`))
255
329
  && (!api || site === undefined || site === "same-origin" || site === "none");
256
330
  }
331
+ /** Notes the port of a page off its folder's usual ports in the data folder: the data folder it noted it in, or none when it could not. */
332
+ function notePort(served) {
333
+ try {
334
+ const home = served.context.home();
335
+ void notePagePort(home, served.folder, served.port);
336
+ return home;
337
+ }
338
+ catch {
339
+ return undefined;
340
+ }
341
+ }
257
342
  function listened(server, port) {
258
343
  return new Promise((done) => {
259
344
  const failed = () => done(false);
@@ -1,4 +1,5 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { type Refused } from "./http.ts";
2
3
  import type { Served } from "./page.ts";
3
4
  /** A host the table offers a chat in: whether it is installed, the models and efforts the player may choose, and the command to open one by hand. */
4
5
  export interface ChatHost {
@@ -19,13 +20,26 @@ export interface TableChats {
19
20
  /** Asks for the chat again: with another host, or `updated`, from the Chesspal just installed (D-108). */
20
21
  relaunch(folder: string, host?: string, updated?: boolean): boolean;
21
22
  }
22
- /** The chat `chesspal play` holds: the model and effort its host started with, none for the host's own. */
23
+ /** The chat `chesspal play` holds: the model and effort its host started with, none for the host's own, and the seat, the play's process. */
23
24
  export interface HeldChat {
24
25
  readonly model?: string | undefined;
25
26
  readonly effort?: string | undefined;
27
+ readonly seat?: number;
26
28
  }
27
29
  /** A chat opening at the table: one the page asked for, the one started again for a new game, or with another model or host (D-107). */
28
30
  export type Opening = "chat" | "again" | "restart";
31
+ /**
32
+ * What waits for the chat at the table to end its answer (D-107): a start again with another host, or a new game that wants a clean chat, which starts
33
+ * it again; the later request replaces the earlier. `start` starts the new game with the choices kept: whether the chat starts again for it; one that
34
+ * cannot start says why to the pages.
35
+ */
36
+ export type ChatWait = {
37
+ readonly for: "restart";
38
+ readonly host: string;
39
+ } | {
40
+ readonly for: "new-game";
41
+ readonly start: () => boolean;
42
+ };
29
43
  /** What the table's page shows of its chat (D-104, D-107). */
30
44
  export interface TableShown {
31
45
  /** Whether a chat is at the table, and its host among those offered, when it says one. */
@@ -36,9 +50,15 @@ export interface TableShown {
36
50
  readonly started?: HeldChat;
37
51
  readonly hosts: readonly ChatHost[];
38
52
  readonly opening?: Opening;
39
- /** A start again asked while the chat was at work, waiting for it to end its answer, or to be started at once or let go from the page. */
40
- readonly waiting?: true;
53
+ /** A start again or a new game asked while the chat was at work, waiting for it to end its answer, or to be started at once or let go from the page. */
54
+ readonly waiting?: ChatWait["for"];
55
+ /** Why the last new game that waited for the chat could not start, numbered so that each page says it once. */
56
+ readonly refused?: WaitRefused;
41
57
  }
58
+ /** A new game that waited for the chat and could not start: why, and its number, larger for each. */
59
+ export type WaitRefused = Refused & {
60
+ readonly number: number;
61
+ };
42
62
  /** What the table's page shows of its chat: whether one is at the table, with which host, the hosts it can open one in, and one opening. */
43
63
  export declare function tableShown(served: Served): {
44
64
  table?: TableShown;
@@ -55,10 +75,25 @@ export declare function chatToRelaunch(served: Served): boolean;
55
75
  * it was asked; the pages then show it opening.
56
76
  */
57
77
  export declare function relaunchChat(served: Served, how?: "again" | "restart", host?: string): boolean;
58
- /** Starts the chat again as the page asked while it was at work, now that it has ended its answer; a chat gone meanwhile takes the request with it. */
59
- export declare function restartWhenAtRest(served: Served): void;
78
+ /**
79
+ * Whether a new game that wants a clean chat waits for the chat at the table to end its answer, then starts it again (D-57, D-107): `chesspal play` holds
80
+ * it, it is at work, and no analyst of it is; with an analyst at work the player is asked for /clear, as with a chat opened by hand.
81
+ */
82
+ export declare function chatToWaitFor(served: Served): boolean;
83
+ /**
84
+ * Does what the page asked while the chat was at work, now that it has ended its answer. A chat gone meanwhile takes a start again with it; a new game
85
+ * starts without it, and loses nothing: the next chat at the table is a clean one.
86
+ */
87
+ export declare function startWhenAtRest(served: Served): void;
88
+ /** A new game that waited for the chat could not start (D-107): the pages say why, each once. */
89
+ export declare function refuseWaited(served: Served, refused: Refused): void;
60
90
  /**
61
91
  * Once npm has installed the newer Chesspal (D-108), and no game goes on and the chat is at rest, as a game or a chat may have started while npm worked:
62
92
  * the chat `chesspal play` holds starts again from it, clean, and once it has left the table, the table closes to start again from it too.
63
93
  */
64
94
  export declare function updateWhenAtRest(served: Served): void;
95
+ /**
96
+ * Whether the chat's analyst is at work, on `game` when named: one whose chat started again since it took its game, or that has written nothing for half an
97
+ * hour, is gone (D-109). The chat's starts are in its own folder: on the table, the attached chat's.
98
+ */
99
+ export declare function analystAtWork(served: Served, game?: string | undefined): boolean;