chesspal 0.2.4 → 0.3.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 (183) hide show
  1. package/README.md +11 -12
  2. package/dist/cli/cli.js +83 -3
  3. package/dist/cli/command-line.js +26 -9
  4. package/dist/cli/icon.d.ts +25 -0
  5. package/dist/cli/icon.js +80 -0
  6. package/dist/commands/agents.d.ts +19 -2
  7. package/dist/commands/agents.js +39 -13
  8. package/dist/commands/ai-say.js +3 -4
  9. package/dist/commands/analyse-game.d.ts +4 -0
  10. package/dist/commands/analyse-game.js +46 -13
  11. package/dist/commands/annotate-game.d.ts +2 -0
  12. package/dist/commands/annotate-game.js +40 -18
  13. package/dist/commands/answer-lines.d.ts +2 -0
  14. package/dist/commands/answer-lines.js +13 -3
  15. package/dist/commands/coach.js +4 -2
  16. package/dist/commands/context.d.ts +9 -1
  17. package/dist/commands/context.js +5 -0
  18. package/dist/commands/delete-review.js +9 -5
  19. package/dist/commands/game-summary.d.ts +2 -0
  20. package/dist/commands/game-summary.js +4 -0
  21. package/dist/commands/index.d.ts +3 -2
  22. package/dist/commands/index.js +7 -4
  23. package/dist/commands/init.js +2 -4
  24. package/dist/commands/instructions.d.ts +9 -0
  25. package/dist/commands/instructions.js +30 -0
  26. package/dist/commands/lead-input.d.ts +1 -1
  27. package/dist/commands/lead-input.js +7 -4
  28. package/dist/commands/page-link.js +6 -5
  29. package/dist/commands/past-games.d.ts +1 -1
  30. package/dist/commands/past-games.js +12 -5
  31. package/dist/commands/player-move.js +2 -3
  32. package/dist/commands/profiles.d.ts +23 -9
  33. package/dist/commands/profiles.js +76 -35
  34. package/dist/commands/ratings.d.ts +8 -0
  35. package/dist/commands/ratings.js +45 -0
  36. package/dist/commands/registry.d.ts +1 -1
  37. package/dist/commands/registry.js +3 -2
  38. package/dist/commands/say.d.ts +1 -1
  39. package/dist/commands/say.js +1 -1
  40. package/dist/commands/status.d.ts +3 -1
  41. package/dist/commands/status.js +46 -27
  42. package/dist/commands/study.js +8 -5
  43. package/dist/core/index.d.ts +1 -1
  44. package/dist/core/index.js +1 -1
  45. package/dist/core/review.d.ts +2 -0
  46. package/dist/core/review.js +5 -1
  47. package/dist/games/ai-model.d.ts +4 -2
  48. package/dist/games/ai-model.js +10 -1
  49. package/dist/games/analysis.d.ts +26 -3
  50. package/dist/games/analysis.js +41 -6
  51. package/dist/games/archive.d.ts +6 -3
  52. package/dist/games/archive.js +13 -5
  53. package/dist/games/current-game.d.ts +10 -1
  54. package/dist/games/current-game.js +49 -15
  55. package/dist/games/files.d.ts +5 -0
  56. package/dist/games/files.js +42 -1
  57. package/dist/games/index.d.ts +3 -3
  58. package/dist/games/index.js +3 -3
  59. package/dist/home/data-folder.d.ts +2 -0
  60. package/dist/home/data-folder.js +5 -1
  61. package/dist/home/index.d.ts +1 -1
  62. package/dist/home/index.js +1 -1
  63. package/dist/host-files/agents.d.ts +2 -2
  64. package/dist/host-files/agents.js +12 -6
  65. package/dist/host-files/host-files.d.ts +5 -3
  66. package/dist/host-files/host-files.js +15 -8
  67. package/dist/host-files/index.d.ts +2 -2
  68. package/dist/host-files/index.js +2 -2
  69. package/dist/page/app/app.js +29 -16
  70. package/dist/page/app/title.js +17 -0
  71. package/dist/page/avatars/torretta.js +4 -4
  72. package/dist/page/dialogs/analyses.js +4 -4
  73. package/dist/page/dialogs/dialog.js +4 -1
  74. package/dist/page/dialogs/dialogs.css +84 -10
  75. package/dist/page/dialogs/profiles.js +95 -42
  76. package/dist/page/game/column.js +5 -1
  77. package/dist/page/game/plies.js +7 -4
  78. package/dist/page/guide/blocks.js +67 -0
  79. package/dist/page/guide/en.md +66 -0
  80. package/dist/page/guide/foot.js +18 -0
  81. package/dist/page/guide/guide.css +200 -0
  82. package/dist/page/guide/guide.js +89 -0
  83. package/dist/page/guide/it.md +66 -0
  84. package/dist/page/guide/markdown.js +67 -0
  85. package/dist/page/helps/helps.css +7 -10
  86. package/dist/page/layout/favicon.svg +13 -0
  87. package/dist/page/layout/header.js +3 -3
  88. package/dist/page/layout/icon.icns +0 -0
  89. package/dist/page/layout/icon.ico +0 -0
  90. package/dist/page/layout/icon.png +0 -0
  91. package/dist/page/layout/layout.css +111 -4
  92. package/dist/page/layout/window.js +45 -0
  93. package/dist/page/main.js +4 -1
  94. package/dist/page/players/card.js +1 -1
  95. package/dist/page/profile/profile.js +14 -9
  96. package/dist/page/profile/settings.js +9 -0
  97. package/dist/page/style.css +2 -0
  98. package/dist/page/table/opener.css +65 -0
  99. package/dist/page/table/opener.js +59 -0
  100. package/dist/page/talk/chat.css +10 -3
  101. package/dist/page/talk/talk.css +38 -26
  102. package/dist/page/talk/talk.js +4 -2
  103. package/dist/page/text.js +83 -17
  104. package/dist/page/theme.css +7 -0
  105. package/dist/profiles/index.d.ts +2 -1
  106. package/dist/profiles/index.js +2 -1
  107. package/dist/profiles/profiles.d.ts +10 -15
  108. package/dist/profiles/profiles.js +30 -15
  109. package/dist/profiles/ratings.d.ts +35 -0
  110. package/dist/profiles/ratings.js +61 -0
  111. package/dist/server/bell.d.ts +24 -3
  112. package/dist/server/bell.js +84 -10
  113. package/dist/server/caller.d.ts +2 -1
  114. package/dist/server/caller.js +3 -2
  115. package/dist/server/mcp.d.ts +7 -3
  116. package/dist/server/mcp.js +100 -52
  117. package/dist/server/page/analyses.d.ts +2 -0
  118. package/dist/server/page/analyses.js +20 -5
  119. package/dist/server/page/attach.d.ts +4 -0
  120. package/dist/server/page/attach.js +112 -0
  121. package/dist/server/page/chat.d.ts +60 -0
  122. package/dist/server/page/chat.js +79 -0
  123. package/dist/server/page/events.d.ts +7 -0
  124. package/dist/server/page/events.js +20 -0
  125. package/dist/server/page/files.js +10 -1
  126. package/dist/server/page/game.js +43 -23
  127. package/dist/server/page/page.d.ts +47 -7
  128. package/dist/server/page/page.js +69 -48
  129. package/dist/server/page/profiles.d.ts +5 -0
  130. package/dist/server/page/profiles.js +26 -10
  131. package/dist/server/page/table.d.ts +39 -0
  132. package/dist/server/page/table.js +102 -0
  133. package/dist/server/table/browser.d.ts +6 -0
  134. package/dist/server/table/browser.js +48 -0
  135. package/dist/server/table/hosts/chats.d.ts +7 -0
  136. package/dist/server/table/hosts/chats.js +31 -0
  137. package/dist/server/table/hosts/find.d.ts +2 -0
  138. package/dist/server/table/hosts/find.js +20 -0
  139. package/dist/server/table/hosts/hosts.d.ts +34 -0
  140. package/dist/server/table/hosts/hosts.js +49 -0
  141. package/dist/server/table/hosts/index.d.ts +3 -0
  142. package/dist/server/table/hosts/index.js +4 -0
  143. package/dist/server/table/hosts/play.d.ts +25 -0
  144. package/dist/server/table/hosts/play.js +152 -0
  145. package/dist/server/table/hosts/seat.d.ts +25 -0
  146. package/dist/server/table/hosts/seat.js +74 -0
  147. package/dist/server/table/hosts/terminal.d.ts +6 -0
  148. package/dist/server/table/hosts/terminal.js +63 -0
  149. package/dist/server/table/icon/index.d.ts +32 -0
  150. package/dist/server/table/icon/index.js +83 -0
  151. package/dist/server/table/icon/linux.d.ts +5 -0
  152. package/dist/server/table/icon/linux.js +47 -0
  153. package/dist/server/table/icon/macos.d.ts +3 -0
  154. package/dist/server/table/icon/macos.js +38 -0
  155. package/dist/server/table/icon/windows.d.ts +3 -0
  156. package/dist/server/table/icon/windows.js +48 -0
  157. package/dist/server/table/index.d.ts +6 -0
  158. package/dist/server/table/index.js +7 -0
  159. package/dist/server/table/launch.d.ts +19 -0
  160. package/dist/server/table/launch.js +78 -0
  161. package/dist/server/table/link.d.ts +10 -0
  162. package/dist/server/table/link.js +230 -0
  163. package/dist/server/table/table.d.ts +35 -0
  164. package/dist/server/table/table.js +94 -0
  165. package/dist/sites/chesscom.d.ts +4 -0
  166. package/dist/sites/chesscom.js +25 -0
  167. package/dist/sites/choice.d.ts +17 -0
  168. package/dist/sites/choice.js +13 -0
  169. package/dist/sites/index.d.ts +2 -0
  170. package/dist/sites/index.js +4 -0
  171. package/dist/sites/lichess.d.ts +4 -0
  172. package/dist/sites/lichess.js +32 -0
  173. package/dist/sites/request.d.ts +11 -0
  174. package/dist/sites/request.js +15 -0
  175. package/dist/sites/sites.d.ts +6 -0
  176. package/dist/sites/sites.js +33 -0
  177. package/instructions/analysis.md +1 -1
  178. package/instructions/analyst-agent.md +1 -1
  179. package/instructions/coach.md +2 -1
  180. package/instructions/opponent.md +6 -6
  181. package/instructions/ponder.md +1 -0
  182. package/instructions/start.md +1 -0
  183. package/package.json +1 -1
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import type { Context, SeenPosition } from "../../commands/index.ts";
3
+ /**
4
+ * What the page reads of the chat it serves: the player's words the AI was given (D-53), the game it saw at its last tool, which the doorbell rings it for,
5
+ * the position it moves on, and its host, for the AI's sign and the clean chat a new game asks for (D-57). The page never changes it.
6
+ */
7
+ export interface ChatSeen {
8
+ readonly given: ReadonlySet<string>;
9
+ readonly seen: string | undefined;
10
+ readonly positionSeen: SeenPosition | null | undefined;
11
+ host(): string | undefined;
12
+ }
13
+ /** The chat as the page reads it, in the chat's own process: read from its context at each request, so always as the chat left it. */
14
+ export declare function chatSeenOf(chat: Context): ChatSeen;
15
+ /**
16
+ * The context the page runs its commands with: the chat's folders, time, randomness and fetch, and nothing that is the chat's own, as the position it saw, which
17
+ * would refuse the player's resignation while the AI thinks, or what it was told.
18
+ */
19
+ export declare function pageContext(chat: Context): Context;
20
+ /** The context of a new game started on the page: the page's, with what the chat saw and its host, by which the game before asks for a clean chat (D-57). */
21
+ export declare function asTheChatSees(context: Context, chat: ChatSeen): Context;
22
+ /** What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, and whether its folder needs init again. */
23
+ export declare const chatAttach: z.ZodObject<{
24
+ version: z.ZodString;
25
+ session: z.ZodNullable<z.ZodString>;
26
+ folder: z.ZodString;
27
+ host: z.ZodOptional<z.ZodString>;
28
+ outdated: z.ZodBoolean;
29
+ }, z.core.$strip>;
30
+ export type ChatAttach = z.infer<typeof chatAttach>;
31
+ /**
32
+ * What the chat attached to the table tells it after each tool, and when the page has news no file holds (`news`): what it saw, the words it was given,
33
+ * its host, and the AI's model and host, which the table notes for a game that ends on the page (D-82, D-97).
34
+ */
35
+ export declare const chatCopy: z.ZodObject<{
36
+ token: z.ZodString;
37
+ seen: z.ZodOptional<z.ZodString>;
38
+ positionSeen: z.ZodOptional<z.ZodNullable<z.ZodObject<{
39
+ started: z.ZodOptional<z.ZodString>;
40
+ plies: z.ZodInt;
41
+ }, z.core.$strip>>>;
42
+ given: z.ZodArray<z.ZodString>;
43
+ host: z.ZodOptional<z.ZodString>;
44
+ aiModel: z.ZodOptional<z.ZodObject<{
45
+ model: z.ZodString;
46
+ effort: z.ZodOptional<z.ZodString>;
47
+ }, z.core.$strip>>;
48
+ aiHost: z.ZodOptional<z.ZodString>;
49
+ news: z.ZodBoolean;
50
+ }, z.core.$strip>;
51
+ export type ChatCopy = z.infer<typeof chatCopy>;
52
+ /** The chat attached to the table: the token of its requests, what it said as it attached, and its last copy, with the words given as a set. */
53
+ export interface Attached {
54
+ readonly token: string;
55
+ readonly chat: ChatAttach;
56
+ copy: ChatCopy | undefined;
57
+ given: ReadonlySet<string>;
58
+ }
59
+ /** The chat attached to the table as the page reads it; nobody's while none is: no words given, no game or position seen, no host. */
60
+ export declare function attachedSeen(attached: () => Attached | undefined): ChatSeen;
@@ -0,0 +1,79 @@
1
+ // What the page knows of the chat, and what it runs commands with: its own context, as the command line's, since the chat's refusals are the chat's (D-71).
2
+ // The table knows the chat attached to it from what the chat tells it (D-104).
3
+ import { z } from "zod";
4
+ /** The chat as the page reads it, in the chat's own process: read from its context at each request, so always as the chat left it. */
5
+ export function chatSeenOf(chat) {
6
+ return {
7
+ get given() {
8
+ return chat.given;
9
+ },
10
+ get seen() {
11
+ return chat.seen;
12
+ },
13
+ get positionSeen() {
14
+ return chat.positionSeen;
15
+ },
16
+ host: () => chat.host?.(),
17
+ };
18
+ }
19
+ /**
20
+ * The context the page runs its commands with: the chat's folders, time, randomness and fetch, and nothing that is the chat's own, as the position it saw, which
21
+ * would refuse the player's resignation while the AI thinks, or what it was told.
22
+ */
23
+ export function pageContext(chat) {
24
+ const { folder, home, random, now, pause, platform, language, openFolder, fetch } = chat;
25
+ return {
26
+ folder,
27
+ home,
28
+ random,
29
+ now,
30
+ platform,
31
+ given: new Set(),
32
+ ...(pause === undefined ? {} : { pause }),
33
+ ...(language === undefined ? {} : { language }),
34
+ ...(openFolder === undefined ? {} : { openFolder }),
35
+ ...(fetch === undefined ? {} : { fetch }),
36
+ };
37
+ }
38
+ /** The context of a new game started on the page: the page's, with what the chat saw and its host, by which the game before asks for a clean chat (D-57). */
39
+ export function asTheChatSees(context, chat) {
40
+ return { ...context, host: () => chat.host(), positionSeen: chat.positionSeen };
41
+ }
42
+ /** What a chat says of itself as it attaches to the table (D-104): its version, its session, its folder, its host, and whether its folder needs init again. */
43
+ export const chatAttach = z.object({
44
+ version: z.string(),
45
+ session: z.string().nullable(),
46
+ folder: z.string(),
47
+ host: z.string().optional(),
48
+ outdated: z.boolean(),
49
+ });
50
+ /**
51
+ * What the chat attached to the table tells it after each tool, and when the page has news no file holds (`news`): what it saw, the words it was given,
52
+ * its host, and the AI's model and host, which the table notes for a game that ends on the page (D-82, D-97).
53
+ */
54
+ export const chatCopy = z.object({
55
+ token: z.string(),
56
+ seen: z.string().optional(),
57
+ positionSeen: z.object({ started: z.string().optional(), plies: z.int().min(0) }).nullable().optional(),
58
+ given: z.array(z.string()),
59
+ host: z.string().optional(),
60
+ aiModel: z.object({ model: z.string(), effort: z.string().optional() }).optional(),
61
+ aiHost: z.string().optional(),
62
+ news: z.boolean(),
63
+ });
64
+ /** The chat attached to the table as the page reads it; nobody's while none is: no words given, no game or position seen, no host. */
65
+ export function attachedSeen(attached) {
66
+ const none = new Set();
67
+ return {
68
+ get given() {
69
+ return attached()?.given ?? none;
70
+ },
71
+ get seen() {
72
+ return attached()?.copy?.seen;
73
+ },
74
+ get positionSeen() {
75
+ return attached()?.copy?.positionSeen;
76
+ },
77
+ host: () => attached()?.copy?.host ?? attached()?.chat.host,
78
+ };
79
+ }
@@ -0,0 +1,7 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ /** A page that waits to hear the game has changed: a stream of events, one "changed" for each change, after which the page reads the game again. */
3
+ export declare function keepPosted(request: IncomingMessage, response: ServerResponse, watchers: Set<ServerResponse>): void;
4
+ /** The pages open now hear the game, or what they show of the chat, has changed. */
5
+ export declare function tellWatchers(watchers: Set<ServerResponse>): void;
6
+ /** The pages open now open their New game window; whether there was one. */
7
+ export declare function askNewGame(watchers: Set<ServerResponse>): boolean;
@@ -0,0 +1,20 @@
1
+ /** A page that waits to hear the game has changed: a stream of events, one "changed" for each change, after which the page reads the game again. */
2
+ export function keepPosted(request, response, watchers) {
3
+ response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
4
+ response.write(": posted\n\n");
5
+ watchers.add(response);
6
+ request.on("close", () => watchers.delete(response));
7
+ }
8
+ /** The pages open now hear the game, or what they show of the chat, has changed. */
9
+ export function tellWatchers(watchers) {
10
+ for (const watcher of watchers) {
11
+ watcher.write("data: changed\n\n");
12
+ }
13
+ }
14
+ /** The pages open now open their New game window; whether there was one. */
15
+ export function askNewGame(watchers) {
16
+ for (const watcher of watchers) {
17
+ watcher.write("data: new-game\n\n");
18
+ }
19
+ return watchers.size > 0;
20
+ }
@@ -52,13 +52,22 @@ const html = [
52
52
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
53
53
  '<meta name="color-scheme" content="light dark">',
54
54
  "<title>Chesspal</title>",
55
+ '<link rel="icon" type="image/svg+xml" href="/page/layout/favicon.svg">',
55
56
  '<link rel="stylesheet" href="/page/style.css">',
56
57
  `<script type="importmap">${importMap}</script>`,
57
58
  '<script type="module" src="/page/main.js"></script>',
58
59
  '<div id="app"></div>',
59
60
  "",
60
61
  ].join("\n");
61
- const types = { ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".webp": "image/webp", ".json": "application/json; charset=utf-8", ".ogg": "audio/ogg" };
62
+ const types = {
63
+ ".js": "text/javascript; charset=utf-8",
64
+ ".css": "text/css; charset=utf-8",
65
+ ".svg": "image/svg+xml",
66
+ ".webp": "image/webp",
67
+ ".json": "application/json; charset=utf-8",
68
+ ".ogg": "audio/ogg",
69
+ ".md": "text/markdown; charset=utf-8",
70
+ };
62
71
  function builtFile(path) {
63
72
  const area = /^\/(page|core)\//.exec(path)?.[1];
64
73
  if (area === undefined) {
@@ -1,9 +1,11 @@
1
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, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/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";
4
4
  import { gameFolder, helpsIn } from "../../profiles/index.js";
5
+ import { asTheChatSees } from "./chat.js";
5
6
  import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
6
7
  import { profilesShown } from "./profiles.js";
8
+ import { chatToRelaunch, relaunchChat, tableShown } from "./table.js";
7
9
  /** Answers the page's requests about the game: true when the request was one. */
8
10
  export function answerGame(request, response, url, served) {
9
11
  const route = routes[`${request.method ?? ""} ${url.pathname}`];
@@ -19,6 +21,13 @@ export function chatCalls(served) {
19
21
  const current = loadGame(folder);
20
22
  const held = current !== undefined && aiHeld(served, current);
21
23
  if (served.bell.chatCalls() && held) {
24
+ rerunIfChanged(() => giveHeldTime(served, folder));
25
+ }
26
+ }
27
+ /** The AI's clock, held while nobody heard the player, starts now: the time it was held is not the AI's. */
28
+ function giveHeldTime(served, folder) {
29
+ const current = loadGame(folder);
30
+ if (current?.clock !== undefined && endOf(current.game) === undefined) {
22
31
  saveGame(folder, { ...current, clock: { ...current.clock, since: served.context.now().toISOString() } });
23
32
  }
24
33
  }
@@ -61,7 +70,7 @@ async function moveFromPage(request, response, served) {
61
70
  illegal = true;
62
71
  return problem;
63
72
  };
64
- playMove(gameFolder(served.context.home()), now, "player", move, refuse, message === undefined ? {} : { said: { text: message, at: now } });
73
+ rerunIfChanged(() => playMove(gameFolder(served.context.home()), now, "player", move, refuse, message === undefined ? {} : { said: { text: message, at: now } }));
65
74
  }
66
75
  catch (error) {
67
76
  // The game may have changed since the page read it: the move refused, the AI's turn, a flag fallen.
@@ -90,7 +99,7 @@ async function messageFromPage(request, response, served) {
90
99
  }
91
100
  let current;
92
101
  try {
93
- current = say(gameFolder(served.context.home()), "player", text, served.context.now(), undefined, shown);
102
+ current = rerunIfChanged(() => say(gameFolder(served.context.home()), "player", text, served.context.now(), undefined, shown));
94
103
  }
95
104
  catch (error) {
96
105
  const noGame = refusalOf(served)?.refusal === "no-game";
@@ -106,11 +115,13 @@ async function messageFromPage(request, response, served) {
106
115
  }
107
116
  /**
108
117
  * 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
109
- * before, and no clock means a game without one. One going on goes to the archive unfinished, and the doorbell tells the chat.
118
+ * 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
119
+ * it again.
110
120
  */
111
121
  async function newGameFromPage(request, response, served) {
112
122
  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.';
113
123
  const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
124
+ let relaunching = false;
114
125
  if (body === undefined) {
115
126
  send(response, isJson(request) ? 400 : 415, json, JSON.stringify({ error: asked }));
116
127
  return;
@@ -122,14 +133,19 @@ async function newGameFromPage(request, response, served) {
122
133
  send(response, 400, json, JSON.stringify({ error: asked }));
123
134
  return;
124
135
  }
125
- // The game starts from the page, open: its clocks wait for the player's Start there.
126
- startNewGame(input.data, { ...served.context, pageOpen: () => true }, "page");
136
+ // 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.
137
+ relaunching = chatToRelaunch(served);
138
+ const sight = relaunching ? served.context : asTheChatSees(served.context, served.chat);
139
+ rerunIfChanged(() => startNewGame(input.data, { ...sight, pageOpen: () => true }, "page"));
127
140
  }
128
141
  catch (error) {
129
142
  send(response, 409, json, JSON.stringify(refusalFrom(error)));
130
143
  return;
131
144
  }
132
- served.bell.ring();
145
+ // A seat lost meanwhile leaves the chat as it is: the doorbell tells it.
146
+ if (!(relaunching && relaunchChat(served))) {
147
+ served.bell.ring();
148
+ }
133
149
  send(response, 200, json, JSON.stringify({ started: true }));
134
150
  }
135
151
  /** 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. */
@@ -145,12 +161,7 @@ async function drawFromPage(request, response, served) {
145
161
  return;
146
162
  }
147
163
  try {
148
- if (action === "offer") {
149
- offerDraw(served.context);
150
- }
151
- else {
152
- answerDraw(served.context, action === "accept");
153
- }
164
+ rerunIfChanged(() => action === "offer" ? offerDraw(served.context) : answerDraw(served.context, action === "accept"));
154
165
  }
155
166
  catch (error) {
156
167
  send(response, 409, json, JSON.stringify(refusalFrom(error, refusalOf(served)?.refusal)));
@@ -167,18 +178,26 @@ function startFromPage(request, response, served) {
167
178
  send(response, 415, json, JSON.stringify({ error: "Send the request as JSON: {}." }));
168
179
  return;
169
180
  }
170
- const folder = gameFolder(served.context.home());
171
- const current = loadGame(folder);
172
- if (current?.clock?.waiting !== true || endOf(current.game) !== undefined) {
181
+ const current = rerunIfChanged(() => startClocks(served));
182
+ if (current === undefined) {
173
183
  send(response, 409, json, JSON.stringify({ error: "No clock waits to start: start a game with a clock first." }));
174
184
  return;
175
185
  }
176
- saveGame(folder, { ...current, clock: clockStarted(current.clock, served.context.now()) });
177
186
  if (current.game.position.turn === current.ai) {
178
187
  served.bell.ring();
179
188
  }
180
189
  send(response, 200, json, JSON.stringify({ started: true }));
181
190
  }
191
+ /** Starts the clocks of the game, which wait from its start: the game started, or none when no clock waits. */
192
+ function startClocks(served) {
193
+ const folder = gameFolder(served.context.home());
194
+ const current = loadGame(folder);
195
+ if (current?.clock?.waiting !== true || endOf(current.game) !== undefined) {
196
+ return undefined;
197
+ }
198
+ saveGame(folder, { ...current, clock: clockStarted(current.clock, served.context.now()) });
199
+ return current;
200
+ }
182
201
  /** The player resigns from the page (D-65), whoever is to move: the game ends and goes to the archive, and the doorbell tells the chat. */
183
202
  function resignFromPage(request, response, served) {
184
203
  if (!isJson(request)) {
@@ -186,7 +205,7 @@ function resignFromPage(request, response, served) {
186
205
  return;
187
206
  }
188
207
  try {
189
- endGame.run({ result: "player_resigns" }, served.context);
208
+ rerunIfChanged(() => endGame.run({ result: "player_resigns" }, served.context));
190
209
  }
191
210
  catch (error) {
192
211
  send(response, 409, json, JSON.stringify(refusalFrom(error, refusalOf(served)?.refusal === "no-game" ? "no-game" : undefined)));
@@ -228,7 +247,7 @@ async function studyFromPage(request, response, served) {
228
247
  reviewed = isReviewed(opened.record);
229
248
  brought = !reviewed && !look;
230
249
  // 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.
231
- startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), brought);
250
+ startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), { brought });
232
251
  }
233
252
  catch (error) {
234
253
  send(response, 400, json, JSON.stringify({ error: error.message, refusal: "pgn-unreadable" }));
@@ -249,7 +268,7 @@ function sendGame(response, served) {
249
268
  const current = gameNow(served);
250
269
  // With no game the page still shows the talk before the first (D-77).
251
270
  const spoken = current ?? loadTableTalk(folder);
252
- const heard = heardLines(spoken, served.context.given);
271
+ const heard = heardLines(spoken, served.chat.given);
253
272
  const talk = spoken.talk.map(({ who, text, ply, shown }, index) => ({
254
273
  who,
255
274
  text,
@@ -284,6 +303,7 @@ function sendGame(response, served) {
284
303
  listening: served.bell.listening,
285
304
  ...thinkingShown(served, current),
286
305
  ...(served.outdated ? { outdated: true } : {}),
306
+ ...tableShown(served),
287
307
  ...profilesShown(served.context.home()),
288
308
  };
289
309
  send(response, 200, json, JSON.stringify(body));
@@ -298,7 +318,7 @@ function thinkingShown(served, current) {
298
318
  }
299
319
  const position = { started: current.started, plies: current.game.record.moves.length };
300
320
  if (current.game.position.turn === current.ai) {
301
- const seen = served.context.positionSeen;
321
+ const seen = served.chat.positionSeen;
302
322
  const onIt = served.bell.woke || (seen !== undefined && seen !== null && samePosition(seen, position));
303
323
  return served.bell.inTurn && onIt ? { thinking: "move" } : {};
304
324
  }
@@ -342,7 +362,7 @@ function lastPlayed(context) {
342
362
  }
343
363
  /** 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. */
344
364
  function kept(served, pgn) {
345
- analyseGame.run({ pgn }, { ...served.context, told: undefined });
365
+ analyseGame.run({ pgn }, served.context);
346
366
  return namedGame(served.context, gameInAnalysis(gameFolder(served.context.home())).game);
347
367
  }
348
368
  /** The longest PGN the page brings: a long game with its clocks, and the comments a site adds, which go. */
@@ -382,7 +402,7 @@ function gameNow(served) {
382
402
  if (before !== undefined && aiHeld(served, before)) {
383
403
  return before;
384
404
  }
385
- const current = gameAt(folder, served.context.now());
405
+ const current = rerunIfChanged(() => gameAt(folder, served.context.now()));
386
406
  if (before !== undefined && endOf(before.game) === undefined && current !== undefined && endOf(current.game) !== undefined) {
387
407
  served.bell.ring();
388
408
  }
@@ -1,34 +1,74 @@
1
1
  import { type ServerResponse } from "node:http";
2
2
  import { type Context } from "../../commands/index.ts";
3
3
  import { Bell } from "../bell.ts";
4
- /** The page of a player's folder, served: its address, how to tell it the AI was given words, which change no file, and how to stop it. */
4
+ import { type Attached, type ChatSeen } from "./chat.ts";
5
+ import { type Opening, type TableChats } from "./table.ts";
6
+ /**
7
+ * 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
+ * and was given, which change no file, and how to stop it.
9
+ */
5
10
  export interface Page {
6
11
  readonly url: string;
12
+ /** The page has news no file holds: words the AI was given, which it shows read (D-53), the position the chat moves on, the AI's model (D-82). */
7
13
  heard(): void;
8
14
  /** The chat calls a tool: the page hears it listens, and the AI's clock, held while nobody heard the player, starts now. */
9
- chatCalls(): void;
15
+ chatCalls(): void | Promise<void>;
16
+ /** 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
+ toolEnded(): void;
10
18
  /** Whether a browser has the page open now: one listens to the game's changes. */
11
19
  open(): boolean;
20
+ /** Whether the page is in use: open in a browser, or, on the table, with a chat attached. */
21
+ inUse(): boolean;
12
22
  /** Opens the New game window on every page open now, for the player to choose the game there: whether one was open. */
13
23
  askNewGame(): boolean;
14
24
  close(): Promise<void>;
15
25
  }
26
+ /** How a page is served, besides its context: the opponent's name, as the host gives it, and the table that serves it, when one does (D-104). */
27
+ export interface PageOptions {
28
+ readonly opponent?: () => string | undefined;
29
+ readonly table?: TableEnd;
30
+ }
31
+ /**
32
+ * What the page tells the table that serves it: a request came, so the table is in use, or another Chesspal version asked it to quit; and how the table
33
+ * opens a chat, which the page offers while none is at the table.
34
+ */
35
+ export interface TableEnd {
36
+ used(): void;
37
+ quit(): void;
38
+ readonly chats?: TableChats | undefined;
39
+ }
16
40
  /**
17
- * 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 page
18
- * names the opponent as the host does, once it has connected; the doorbell knows from the context what the chat has read of the player's words.
41
+ * 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
42
+ * 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
43
+ * 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.
19
44
  */
20
- export declare function startPage(context: Context, opponent?: () => string | undefined): Promise<Page>;
45
+ export declare function startPage(context: Context, options?: PageOptions): Promise<Page>;
21
46
  /** What the server keeps of its folder: the doorbell, and the pages that wait to hear the game has changed. */
22
47
  export interface Served {
23
48
  /** The host's folder, which gives the page its ports and the doorbell its chat; the game is in the context's data folder (D-71). */
24
49
  readonly folder: string;
50
+ /** What the page runs its commands with: never the chat's own (D-71). */
25
51
  readonly context: Context;
52
+ /** What the page reads of the chat: the words it was given, the game and the position it saw, its host; on the table, the attached chat's, or nobody's. */
53
+ readonly chat: ChatSeen;
26
54
  readonly opponent: () => string | undefined;
27
55
  readonly bell: Bell;
28
56
  readonly watchers: Set<ServerResponse>;
29
57
  port: number;
30
- /** The folder was set up by an older init: the page asks the player to run it again. Read once, as init runs with the chat closed. */
31
- readonly outdated: boolean;
58
+ /**
59
+ * The chat's folder was set up by an older init: the page asks the player to run it again. Read once, as init runs with the chat closed; on the table,
60
+ * as the chat attached to it says it.
61
+ */
62
+ outdated: boolean;
63
+ /** The table that serves the page, when one does (D-104). */
64
+ readonly table?: TableEnd;
65
+ /** The chat attached to the table, while one is. */
66
+ attached?: Attached;
67
+ /** When the table last opened a chat from the page, which has not attached yet: no second one meanwhile. */
68
+ chatOpened?: {
69
+ readonly at: number;
70
+ readonly how: Opening;
71
+ } | undefined;
32
72
  }
33
73
  /**
34
74
  * 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