chesspal 0.2.3 → 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 (214) 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 +41 -14
  8. package/dist/commands/ai-say.d.ts +2 -1
  9. package/dist/commands/ai-say.js +3 -4
  10. package/dist/commands/analyse-game.d.ts +6 -0
  11. package/dist/commands/analyse-game.js +51 -15
  12. package/dist/commands/annotate-game.d.ts +4 -0
  13. package/dist/commands/annotate-game.js +60 -20
  14. package/dist/commands/answer-lines.d.ts +10 -4
  15. package/dist/commands/answer-lines.js +30 -4
  16. package/dist/commands/coach.js +6 -4
  17. package/dist/commands/context.d.ts +11 -1
  18. package/dist/commands/context.js +5 -0
  19. package/dist/commands/delete-review.js +9 -4
  20. package/dist/commands/game-summary.d.ts +9 -2
  21. package/dist/commands/game-summary.js +11 -0
  22. package/dist/commands/index.d.ts +4 -3
  23. package/dist/commands/index.js +8 -5
  24. package/dist/commands/init.js +2 -4
  25. package/dist/commands/instructions.d.ts +9 -0
  26. package/dist/commands/instructions.js +30 -0
  27. package/dist/commands/lead-input.d.ts +1 -1
  28. package/dist/commands/lead-input.js +7 -4
  29. package/dist/commands/look.d.ts +9 -5
  30. package/dist/commands/look.js +19 -9
  31. package/dist/commands/page-link.js +6 -5
  32. package/dist/commands/past-games.d.ts +1 -1
  33. package/dist/commands/past-games.js +12 -5
  34. package/dist/commands/player-move.js +2 -3
  35. package/dist/commands/ponder.js +1 -1
  36. package/dist/commands/position.js +2 -2
  37. package/dist/commands/profiles.d.ts +23 -9
  38. package/dist/commands/profiles.js +76 -35
  39. package/dist/commands/ratings.d.ts +8 -0
  40. package/dist/commands/ratings.js +45 -0
  41. package/dist/commands/registry.d.ts +1 -1
  42. package/dist/commands/registry.js +3 -2
  43. package/dist/commands/say.d.ts +11 -6
  44. package/dist/commands/say.js +21 -13
  45. package/dist/commands/status.d.ts +6 -2
  46. package/dist/commands/status.js +70 -26
  47. package/dist/commands/study.d.ts +11 -1
  48. package/dist/commands/study.js +39 -7
  49. package/dist/commands/view.d.ts +17 -5
  50. package/dist/commands/view.js +67 -10
  51. package/dist/core/facts.d.ts +14 -2
  52. package/dist/core/facts.js +32 -7
  53. package/dist/core/game.d.ts +5 -0
  54. package/dist/core/game.js +9 -0
  55. package/dist/core/index.d.ts +2 -1
  56. package/dist/core/index.js +1 -1
  57. package/dist/core/placement.d.ts +63 -0
  58. package/dist/core/placement.js +259 -0
  59. package/dist/core/possibilities.d.ts +38 -0
  60. package/dist/core/possibilities.js +254 -0
  61. package/dist/core/review.d.ts +6 -1
  62. package/dist/core/review.js +21 -6
  63. package/dist/games/ai-model.d.ts +4 -2
  64. package/dist/games/ai-model.js +10 -1
  65. package/dist/games/analysis.d.ts +31 -4
  66. package/dist/games/analysis.js +44 -5
  67. package/dist/games/archive.d.ts +6 -3
  68. package/dist/games/archive.js +13 -5
  69. package/dist/games/current-game.d.ts +26 -2
  70. package/dist/games/current-game.js +57 -21
  71. package/dist/games/files.d.ts +7 -0
  72. package/dist/games/files.js +48 -1
  73. package/dist/games/index.d.ts +4 -4
  74. package/dist/games/index.js +4 -4
  75. package/dist/games/table-talk.d.ts +2 -0
  76. package/dist/games/table-talk.js +5 -1
  77. package/dist/games/talk.d.ts +4 -3
  78. package/dist/games/talk.js +3 -3
  79. package/dist/home/data-folder.d.ts +2 -0
  80. package/dist/home/data-folder.js +5 -1
  81. package/dist/home/index.d.ts +1 -1
  82. package/dist/home/index.js +1 -1
  83. package/dist/host-files/agents.d.ts +2 -2
  84. package/dist/host-files/agents.js +12 -6
  85. package/dist/host-files/host-files.d.ts +5 -3
  86. package/dist/host-files/host-files.js +15 -8
  87. package/dist/host-files/index.d.ts +2 -2
  88. package/dist/host-files/index.js +2 -2
  89. package/dist/page/app/app.js +59 -22
  90. package/dist/page/app/keys.js +16 -2
  91. package/dist/page/app/served.js +2 -2
  92. package/dist/page/app/title.js +17 -0
  93. package/dist/page/avatars/torretta.js +4 -4
  94. package/dist/page/board/board.js +2 -1
  95. package/dist/page/board/marks.js +4 -24
  96. package/dist/page/board/signs.js +23 -0
  97. package/dist/page/dialogs/analyses.js +4 -4
  98. package/dist/page/dialogs/dialog.js +4 -1
  99. package/dist/page/dialogs/dialogs.css +84 -10
  100. package/dist/page/dialogs/profiles.js +95 -42
  101. package/dist/page/game/column.js +6 -2
  102. package/dist/page/game/game.css +12 -0
  103. package/dist/page/game/move-list.js +2 -2
  104. package/dist/page/game/plies.js +11 -0
  105. package/dist/page/guide/blocks.js +67 -0
  106. package/dist/page/guide/en.md +66 -0
  107. package/dist/page/guide/foot.js +18 -0
  108. package/dist/page/guide/guide.css +200 -0
  109. package/dist/page/guide/guide.js +89 -0
  110. package/dist/page/guide/it.md +66 -0
  111. package/dist/page/guide/markdown.js +67 -0
  112. package/dist/page/helps/helps.css +10 -11
  113. package/dist/page/helps/helps.js +15 -15
  114. package/dist/page/layout/favicon.svg +13 -0
  115. package/dist/page/layout/header.js +3 -3
  116. package/dist/page/layout/icon.icns +0 -0
  117. package/dist/page/layout/icon.ico +0 -0
  118. package/dist/page/layout/icon.png +0 -0
  119. package/dist/page/layout/layout.css +140 -9
  120. package/dist/page/layout/window.js +45 -0
  121. package/dist/page/legend/legend.css +203 -0
  122. package/dist/page/legend/legend.js +169 -0
  123. package/dist/page/legend/view.js +54 -0
  124. package/dist/page/main.js +4 -1
  125. package/dist/page/players/card.js +1 -1
  126. package/dist/page/profile/profile.js +14 -9
  127. package/dist/page/profile/settings.js +9 -0
  128. package/dist/page/style.css +4 -0
  129. package/dist/page/table/opener.css +65 -0
  130. package/dist/page/table/opener.js +59 -0
  131. package/dist/page/talk/chat.css +97 -0
  132. package/dist/page/talk/talk.css +38 -26
  133. package/dist/page/talk/talk.js +31 -7
  134. package/dist/page/text.js +120 -18
  135. package/dist/page/theme.css +7 -0
  136. package/dist/profiles/index.d.ts +2 -1
  137. package/dist/profiles/index.js +2 -1
  138. package/dist/profiles/profiles.d.ts +10 -15
  139. package/dist/profiles/profiles.js +30 -15
  140. package/dist/profiles/ratings.d.ts +35 -0
  141. package/dist/profiles/ratings.js +61 -0
  142. package/dist/server/bell.d.ts +24 -3
  143. package/dist/server/bell.js +84 -10
  144. package/dist/server/caller.d.ts +2 -1
  145. package/dist/server/caller.js +3 -2
  146. package/dist/server/mcp.d.ts +7 -3
  147. package/dist/server/mcp.js +106 -56
  148. package/dist/server/page/analyses.d.ts +2 -0
  149. package/dist/server/page/analyses.js +20 -5
  150. package/dist/server/page/attach.d.ts +4 -0
  151. package/dist/server/page/attach.js +112 -0
  152. package/dist/server/page/chat.d.ts +60 -0
  153. package/dist/server/page/chat.js +79 -0
  154. package/dist/server/page/events.d.ts +7 -0
  155. package/dist/server/page/events.js +20 -0
  156. package/dist/server/page/files.js +10 -1
  157. package/dist/server/page/game.js +62 -29
  158. package/dist/server/page/page.d.ts +47 -7
  159. package/dist/server/page/page.js +74 -51
  160. package/dist/server/page/profiles.d.ts +5 -0
  161. package/dist/server/page/profiles.js +26 -10
  162. package/dist/server/page/table.d.ts +39 -0
  163. package/dist/server/page/table.js +102 -0
  164. package/dist/server/table/browser.d.ts +6 -0
  165. package/dist/server/table/browser.js +48 -0
  166. package/dist/server/table/hosts/chats.d.ts +7 -0
  167. package/dist/server/table/hosts/chats.js +31 -0
  168. package/dist/server/table/hosts/find.d.ts +2 -0
  169. package/dist/server/table/hosts/find.js +20 -0
  170. package/dist/server/table/hosts/hosts.d.ts +34 -0
  171. package/dist/server/table/hosts/hosts.js +49 -0
  172. package/dist/server/table/hosts/index.d.ts +3 -0
  173. package/dist/server/table/hosts/index.js +4 -0
  174. package/dist/server/table/hosts/play.d.ts +25 -0
  175. package/dist/server/table/hosts/play.js +152 -0
  176. package/dist/server/table/hosts/seat.d.ts +25 -0
  177. package/dist/server/table/hosts/seat.js +74 -0
  178. package/dist/server/table/hosts/terminal.d.ts +6 -0
  179. package/dist/server/table/hosts/terminal.js +63 -0
  180. package/dist/server/table/icon/index.d.ts +32 -0
  181. package/dist/server/table/icon/index.js +83 -0
  182. package/dist/server/table/icon/linux.d.ts +5 -0
  183. package/dist/server/table/icon/linux.js +47 -0
  184. package/dist/server/table/icon/macos.d.ts +3 -0
  185. package/dist/server/table/icon/macos.js +38 -0
  186. package/dist/server/table/icon/windows.d.ts +3 -0
  187. package/dist/server/table/icon/windows.js +48 -0
  188. package/dist/server/table/index.d.ts +6 -0
  189. package/dist/server/table/index.js +7 -0
  190. package/dist/server/table/launch.d.ts +19 -0
  191. package/dist/server/table/launch.js +78 -0
  192. package/dist/server/table/link.d.ts +10 -0
  193. package/dist/server/table/link.js +230 -0
  194. package/dist/server/table/table.d.ts +35 -0
  195. package/dist/server/table/table.js +94 -0
  196. package/dist/sites/chesscom.d.ts +4 -0
  197. package/dist/sites/chesscom.js +25 -0
  198. package/dist/sites/choice.d.ts +17 -0
  199. package/dist/sites/choice.js +13 -0
  200. package/dist/sites/index.d.ts +2 -0
  201. package/dist/sites/index.js +4 -0
  202. package/dist/sites/lichess.d.ts +4 -0
  203. package/dist/sites/lichess.js +32 -0
  204. package/dist/sites/request.d.ts +11 -0
  205. package/dist/sites/request.js +15 -0
  206. package/dist/sites/sites.d.ts +6 -0
  207. package/dist/sites/sites.js +33 -0
  208. package/instructions/analysis.md +1 -1
  209. package/instructions/analyst-agent.md +1 -1
  210. package/instructions/coach.md +2 -1
  211. package/instructions/opponent.md +6 -6
  212. package/instructions/ponder.md +1 -0
  213. package/instructions/start.md +1 -0
  214. package/package.json +1 -1
@@ -0,0 +1,112 @@
1
+ // The table's end of the chat attached to it (D-104): the chat attaches and stays while its process lives, says when it calls a tool, sends a copy of what
2
+ // it saw, and asks for the New game window. One chat at a time, of this version alone; a page a chat serves itself has none of these routes.
3
+ import { randomUUID } from "node:crypto";
4
+ import { forgetTheAi, noteAiHost, noteAiModel } from "../../games/index.js";
5
+ import { gameFolder } from "../../profiles/index.js";
6
+ import { version } from "../../version.js";
7
+ import { chatAttach, chatCopy } from "./chat.js";
8
+ import { askNewGame, tellWatchers } from "./events.js";
9
+ import { chatCalls } from "./game.js";
10
+ import { bytesOf, isJson, jsonOf, runRoute, sendJson } from "./http.js";
11
+ /** Answers the chat's requests to the table, under /api/chat; false for any other, and for every request to a page a chat serves itself. */
12
+ export function answerChat(request, response, url, served) {
13
+ const path = url.pathname;
14
+ if (served.table === undefined || request.method !== "POST" || (path !== "/api/chat" && !path.startsWith("/api/chat/"))) {
15
+ return false;
16
+ }
17
+ runRoute(response, async () => {
18
+ const body = isJson(request) ? jsonOf((await bytesOf(request, copyLimit))?.toString("utf8")) : undefined;
19
+ if (body === undefined) {
20
+ sendJson(response, 400, { error: `${path} takes a JSON object.` });
21
+ }
22
+ else if (path === "/api/chat") {
23
+ attach(body, response, served);
24
+ }
25
+ else if (served.attached === undefined || body.token !== served.attached.token) {
26
+ sendJson(response, 409, { error: "This chat is not the one at the table: attach to it again." });
27
+ }
28
+ else {
29
+ answerAttached(path, body, response, served, served.attached);
30
+ }
31
+ });
32
+ return true;
33
+ }
34
+ // A copy holds the player's words the chat was given in its whole life: a long game's are well within this.
35
+ const copyLimit = 1_000_000;
36
+ /**
37
+ * A chat attaches: refused when another is at the table, or its version is another (the older yields, D-104); else it gets its token, and the answer stays
38
+ * open while its process lives. When the connection closes the chat has left, and the table is nobody's again.
39
+ */
40
+ function attach(body, response, served) {
41
+ const parsed = chatAttach.safeParse(body);
42
+ if (!parsed.success) {
43
+ sendJson(response, 400, { error: `A chat attaches with its version, session, folder and host: ${parsed.error.message}` });
44
+ return;
45
+ }
46
+ if (parsed.data.version !== version) {
47
+ sendJson(response, 409, { error: `The table runs Chesspal ${version}, the chat ${parsed.data.version}.`, version });
48
+ return;
49
+ }
50
+ if (served.attached !== undefined) {
51
+ sendJson(response, 409, { error: "Another chat is at the table: this one serves its own page." });
52
+ return;
53
+ }
54
+ const attached = { token: randomUUID(), chat: parsed.data, copy: undefined, given: new Set() };
55
+ served.attached = attached;
56
+ served.chatOpened = undefined;
57
+ served.outdated = parsed.data.outdated;
58
+ response.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
59
+ response.write(`${JSON.stringify({ token: attached.token, open: served.watchers.size > 0 })}\n`);
60
+ response.on("close", () => {
61
+ if (served.attached === attached) {
62
+ served.attached = undefined;
63
+ served.outdated = false;
64
+ forgetChat(served);
65
+ served.bell.chatLeft();
66
+ }
67
+ });
68
+ tellWatchers(served.watchers);
69
+ }
70
+ /** The next chat may play in another host, with another model: the AI's card shows its own from its first move (Federico). */
71
+ function forgetChat(served) {
72
+ try {
73
+ forgetTheAi(gameFolder(served.context.home()));
74
+ }
75
+ catch {
76
+ // A data folder that cannot be read keeps no notes to forget.
77
+ }
78
+ }
79
+ /** The attached chat's requests:it calls a tool, sends a copy of what it saw, or asks for the New game window. */
80
+ function answerAttached(path, body, response, served, attached) {
81
+ if (path === "/api/chat/call") {
82
+ chatCalls(served);
83
+ sendJson(response, 200, { open: served.watchers.size > 0 });
84
+ }
85
+ else if (path === "/api/chat/read") {
86
+ readCopy(body, response, served, attached);
87
+ }
88
+ else if (path === "/api/chat/new-game") {
89
+ sendJson(response, 200, { opened: askNewGame(served.watchers) });
90
+ }
91
+ else {
92
+ sendJson(response, 404, { error: `${path} is no route of the table's: the chat attaches at /api/chat, then calls /api/chat/call, /read and /new-game.` });
93
+ }
94
+ }
95
+ /** Keeps the chat's copy; the AI's model and host noted, as the chat's process notes them, and the pages told when it says they have news. */
96
+ function readCopy(body, response, served, attached) {
97
+ const parsed = chatCopy.safeParse(body);
98
+ if (!parsed.success) {
99
+ sendJson(response, 400, { error: `A copy holds what the chat saw: ${parsed.error.message}` });
100
+ return;
101
+ }
102
+ const copy = parsed.data;
103
+ attached.copy = copy;
104
+ attached.given = new Set(copy.given);
105
+ const folder = gameFolder(served.context.home());
106
+ const host = copy.aiHost !== undefined && noteAiHost(folder, copy.aiHost);
107
+ const model = copy.aiModel !== undefined && noteAiModel(folder, copy.aiModel);
108
+ if (copy.news || host || model) {
109
+ tellWatchers(served.watchers);
110
+ }
111
+ sendJson(response, 200, { open: served.watchers.size > 0 });
112
+ }
@@ -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
- 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
- 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.
@@ -72,39 +81,47 @@ async function moveFromPage(request, response, served) {
72
81
  served.bell.ring();
73
82
  send(response, 200, json, JSON.stringify({ played: true }));
74
83
  }
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. */
84
+ /**
85
+ * 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
86
+ * first; in analysis with the position the page shows, and the doorbell rung for each (D-101).
87
+ */
76
88
  async function messageFromPage(request, response, served) {
77
89
  if (!isJson(request)) {
78
90
  send(response, 415, json, JSON.stringify({ error: 'Send the message as JSON: {"text": "…"}.' }));
79
91
  return;
80
92
  }
81
- const text = textOf(jsonOf(await bodyOf(request))?.text);
93
+ const body = jsonOf(await bodyOf(request));
94
+ const text = textOf(body?.text);
95
+ const shown = typeof body?.game === "string" && typeof body.at === "number" ? shownPosition(served.context, body.game, body.at) : undefined;
82
96
  if (text === undefined) {
83
97
  send(response, 400, json, JSON.stringify({ error: `Send the message as JSON, ${talkLimits.player} characters at most: {"text": "…"}.` }));
84
98
  return;
85
99
  }
86
100
  let current;
87
101
  try {
88
- current = say(gameFolder(served.context.home()), "player", text, served.context.now());
102
+ current = rerunIfChanged(() => say(gameFolder(served.context.home()), "player", text, served.context.now(), undefined, shown));
89
103
  }
90
104
  catch (error) {
91
105
  const noGame = refusalOf(served)?.refusal === "no-game";
92
106
  send(response, 409, json, JSON.stringify(refusalFrom(error, noGame ? "no-game" : undefined)));
93
107
  return;
94
108
  }
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)) {
109
+ // 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
110
+ // analysis every line does: a chat that answered in its own window would not hear the next (Priya).
111
+ if (shown !== undefined || wakesTheChat(current)) {
97
112
  served.bell.ring();
98
113
  }
99
114
  send(response, 200, json, JSON.stringify({ said: true }));
100
115
  }
101
116
  /**
102
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
103
- * 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.
104
120
  */
105
121
  async function newGameFromPage(request, response, served) {
106
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.';
107
123
  const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
124
+ let relaunching = false;
108
125
  if (body === undefined) {
109
126
  send(response, isJson(request) ? 400 : 415, json, JSON.stringify({ error: asked }));
110
127
  return;
@@ -116,14 +133,19 @@ async function newGameFromPage(request, response, served) {
116
133
  send(response, 400, json, JSON.stringify({ error: asked }));
117
134
  return;
118
135
  }
119
- // The game starts from the page, open: its clocks wait for the player's Start there.
120
- 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"));
121
140
  }
122
141
  catch (error) {
123
142
  send(response, 409, json, JSON.stringify(refusalFrom(error)));
124
143
  return;
125
144
  }
126
- 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
+ }
127
149
  send(response, 200, json, JSON.stringify({ started: true }));
128
150
  }
129
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. */
@@ -139,12 +161,7 @@ async function drawFromPage(request, response, served) {
139
161
  return;
140
162
  }
141
163
  try {
142
- if (action === "offer") {
143
- offerDraw(served.context);
144
- }
145
- else {
146
- answerDraw(served.context, action === "accept");
147
- }
164
+ rerunIfChanged(() => action === "offer" ? offerDraw(served.context) : answerDraw(served.context, action === "accept"));
148
165
  }
149
166
  catch (error) {
150
167
  send(response, 409, json, JSON.stringify(refusalFrom(error, refusalOf(served)?.refusal)));
@@ -161,18 +178,26 @@ function startFromPage(request, response, served) {
161
178
  send(response, 415, json, JSON.stringify({ error: "Send the request as JSON: {}." }));
162
179
  return;
163
180
  }
164
- const folder = gameFolder(served.context.home());
165
- const current = loadGame(folder);
166
- if (current?.clock?.waiting !== true || endOf(current.game) !== undefined) {
181
+ const current = rerunIfChanged(() => startClocks(served));
182
+ if (current === undefined) {
167
183
  send(response, 409, json, JSON.stringify({ error: "No clock waits to start: start a game with a clock first." }));
168
184
  return;
169
185
  }
170
- saveGame(folder, { ...current, clock: clockStarted(current.clock, served.context.now()) });
171
186
  if (current.game.position.turn === current.ai) {
172
187
  served.bell.ring();
173
188
  }
174
189
  send(response, 200, json, JSON.stringify({ started: true }));
175
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
+ }
176
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. */
177
202
  function resignFromPage(request, response, served) {
178
203
  if (!isJson(request)) {
@@ -180,7 +205,7 @@ function resignFromPage(request, response, served) {
180
205
  return;
181
206
  }
182
207
  try {
183
- endGame.run({ result: "player_resigns" }, served.context);
208
+ rerunIfChanged(() => endGame.run({ result: "player_resigns" }, served.context));
184
209
  }
185
210
  catch (error) {
186
211
  send(response, 409, json, JSON.stringify(refusalFrom(error, refusalOf(served)?.refusal === "no-game" ? "no-game" : undefined)));
@@ -222,7 +247,7 @@ async function studyFromPage(request, response, served) {
222
247
  reviewed = isReviewed(opened.record);
223
248
  brought = !reviewed && !look;
224
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.
225
- startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), brought);
250
+ startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), { brought });
226
251
  }
227
252
  catch (error) {
228
253
  send(response, 400, json, JSON.stringify({ error: error.message, refusal: "pgn-unreadable" }));
@@ -243,8 +268,15 @@ function sendGame(response, served) {
243
268
  const current = gameNow(served);
244
269
  // With no game the page still shows the talk before the first (D-77).
245
270
  const spoken = current ?? loadTableTalk(folder);
246
- 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] });
271
+ const heard = heardLines(spoken, served.chat.given);
272
+ const talk = spoken.talk.map(({ who, text, ply, shown }, index) => ({
273
+ who,
274
+ text,
275
+ ply,
276
+ ...(heard[index] === undefined ? {} : { heard: heard[index] }),
277
+ // In analysis the page goes back to the move the player wrote about (D-101).
278
+ ...(shown === undefined ? {} : { shown }),
279
+ }));
248
280
  const offer = current === undefined ? undefined : drawOfferOf(current);
249
281
  const coach = coachOn(folder, current);
250
282
  const game = current === undefined ? null : {
@@ -271,6 +303,7 @@ function sendGame(response, served) {
271
303
  listening: served.bell.listening,
272
304
  ...thinkingShown(served, current),
273
305
  ...(served.outdated ? { outdated: true } : {}),
306
+ ...tableShown(served),
274
307
  ...profilesShown(served.context.home()),
275
308
  };
276
309
  send(response, 200, json, JSON.stringify(body));
@@ -285,7 +318,7 @@ function thinkingShown(served, current) {
285
318
  }
286
319
  const position = { started: current.started, plies: current.game.record.moves.length };
287
320
  if (current.game.position.turn === current.ai) {
288
- const seen = served.context.positionSeen;
321
+ const seen = served.chat.positionSeen;
289
322
  const onIt = served.bell.woke || (seen !== undefined && seen !== null && samePosition(seen, position));
290
323
  return served.bell.inTurn && onIt ? { thinking: "move" } : {};
291
324
  }
@@ -329,7 +362,7 @@ function lastPlayed(context) {
329
362
  }
330
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. */
331
364
  function kept(served, pgn) {
332
- analyseGame.run({ pgn }, { ...served.context, told: undefined });
365
+ analyseGame.run({ pgn }, served.context);
333
366
  return namedGame(served.context, gameInAnalysis(gameFolder(served.context.home())).game);
334
367
  }
335
368
  /** The longest PGN the page brings: a long game with its clocks, and the comments a site adds, which go. */
@@ -369,7 +402,7 @@ function gameNow(served) {
369
402
  if (before !== undefined && aiHeld(served, before)) {
370
403
  return before;
371
404
  }
372
- const current = gameAt(folder, served.context.now());
405
+ const current = rerunIfChanged(() => gameAt(folder, served.context.now()));
373
406
  if (before !== undefined && endOf(before.game) === undefined && current !== undefined && endOf(current.game) !== undefined) {
374
407
  served.bell.ring();
375
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