chesspal 0.3.6 → 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.
- package/dist/cli/cli.js +7 -2
- package/dist/cli/command-line.js +5 -3
- package/dist/cli/prompt-hook.d.ts +17 -0
- package/dist/cli/prompt-hook.js +100 -0
- package/dist/commands/analysis/analyse.js +8 -14
- package/dist/commands/analysis/annotate.d.ts +8 -0
- package/dist/commands/analysis/annotate.js +19 -3
- package/dist/commands/answers/agents.d.ts +7 -5
- package/dist/commands/answers/agents.js +16 -9
- package/dist/commands/answers/clock.js +2 -1
- package/dist/commands/answers/drawings.js +6 -2
- package/dist/commands/answers/index.d.ts +4 -3
- package/dist/commands/answers/index.js +4 -3
- package/dist/commands/answers/lines.d.ts +5 -3
- package/dist/commands/answers/lines.js +14 -7
- package/dist/commands/answers/opening-book.d.ts +13 -7
- package/dist/commands/answers/opening-book.js +34 -38
- package/dist/commands/answers/opening-tags.d.ts +4 -0
- package/dist/commands/answers/opening-tags.js +11 -0
- package/dist/commands/answers/play-move.d.ts +3 -1
- package/dist/commands/answers/play-move.js +9 -6
- package/dist/commands/answers/say.js +2 -1
- package/dist/commands/answers/summary.d.ts +5 -2
- package/dist/commands/answers/summary.js +16 -5
- package/dist/commands/game/ai-move.js +4 -4
- package/dist/commands/game/coach.js +7 -6
- package/dist/commands/game/end.js +2 -2
- package/dist/commands/game/export.d.ts +1 -1
- package/dist/commands/game/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/new.js +41 -11
- package/dist/commands/game/player-move.d.ts +16 -0
- package/dist/commands/game/player-move.js +69 -19
- package/dist/commands/game/ponder.js +4 -1
- package/dist/commands/game/status.d.ts +1 -1
- package/dist/commands/game/status.js +6 -6
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +3 -3
- package/dist/commands/profiles/past-games.d.ts +1 -1
- package/dist/commands/profiles/past-games.js +21 -10
- package/dist/commands/registry/context.d.ts +2 -0
- package/dist/commands/registry/context.js +5 -0
- package/dist/commands/registry/index.d.ts +1 -1
- package/dist/commands/registry/index.js +1 -1
- package/dist/commands/talk/ai-say.d.ts +1 -1
- package/dist/commands/talk/ai-say.js +6 -5
- package/dist/core/game.d.ts +13 -0
- package/dist/core/game.js +29 -3
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +3 -3
- package/dist/core/openings.d.ts +20 -6
- package/dist/core/openings.js +45 -22
- package/dist/core/pgn.d.ts +4 -0
- package/dist/core/pgn.js +35 -6
- package/dist/games/archive.d.ts +25 -5
- package/dist/games/archive.js +52 -19
- package/dist/games/chat-start.d.ts +2 -2
- package/dist/games/chat-start.js +6 -1
- package/dist/games/current-game.d.ts +2 -2
- package/dist/games/current-game.js +3 -2
- package/dist/games/index.d.ts +2 -1
- package/dist/games/index.js +2 -1
- package/dist/games/position-shown.d.ts +24 -0
- package/dist/games/position-shown.js +51 -0
- package/dist/home/index.d.ts +1 -1
- package/dist/home/index.js +1 -1
- package/dist/home/same-path.d.ts +6 -1
- package/dist/home/same-path.js +22 -3
- package/dist/host-files/host-files.d.ts +5 -0
- package/dist/host-files/host-files.js +13 -4
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/app.js +30 -7
- package/dist/page/app/served.js +9 -4
- package/dist/page/avatars/torretta.css +4 -4
- package/dist/page/base.css +1 -1
- package/dist/page/board/board.css +4 -2
- package/dist/page/dialogs/dialogs.css +3 -1
- package/dist/page/game/column.js +10 -8
- package/dist/page/game/game.css +86 -6
- package/dist/page/game/plies.js +4 -0
- package/dist/page/game/review-progress.js +11 -0
- package/dist/page/guide/drawings/drawings.css +0 -1
- package/dist/page/guide/en.md +2 -2
- package/dist/page/guide/it.md +2 -2
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/layout.css +32 -15
- package/dist/page/layout/window.js +41 -2
- package/dist/page/legend/legend.css +4 -16
- package/dist/page/legend/legend.js +28 -15
- package/dist/page/profile/settings.js +1 -0
- package/dist/page/table/action.js +42 -0
- package/dist/page/table/table.css +57 -39
- package/dist/page/table/window.js +67 -67
- package/dist/page/talk/talk.css +52 -9
- package/dist/page/talk/talk.js +65 -20
- package/dist/page/talk/voice.js +92 -21
- package/dist/page/text.js +24 -8
- package/dist/server/bell/bell.d.ts +24 -27
- package/dist/server/bell/bell.js +79 -143
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -2
- package/dist/server/mcp/mcp.js +49 -7
- package/dist/server/page/analyses.js +15 -4
- package/dist/server/page/files.js +3 -1
- package/dist/server/page/game.d.ts +5 -0
- package/dist/server/page/game.js +78 -13
- package/dist/server/page/http.d.ts +1 -1
- package/dist/server/page/page.d.ts +14 -9
- package/dist/server/page/page.js +111 -26
- package/dist/server/page/table.d.ts +38 -4
- package/dist/server/page/table.js +61 -23
- package/dist/server/page/windows.d.ts +22 -0
- package/dist/server/page/windows.js +45 -0
- package/dist/server/ports/index.d.ts +1 -0
- package/dist/server/ports/index.js +2 -0
- package/dist/server/ports/ports.d.ts +78 -0
- package/dist/server/ports/ports.js +222 -0
- package/dist/server/table/browser.d.ts +22 -6
- package/dist/server/table/browser.js +98 -20
- package/dist/server/table/hosts/hooks.d.ts +6 -0
- package/dist/server/table/hosts/hooks.js +38 -0
- package/dist/server/table/hosts/hosts.d.ts +13 -5
- package/dist/server/table/hosts/hosts.js +16 -10
- package/dist/server/table/hosts/index.d.ts +1 -0
- package/dist/server/table/hosts/index.js +1 -0
- package/dist/server/table/hosts/play.d.ts +6 -4
- package/dist/server/table/hosts/play.js +58 -16
- package/dist/server/table/index.d.ts +1 -1
- package/dist/server/table/launch.d.ts +5 -4
- package/dist/server/table/launch.js +18 -5
- package/dist/server/table/link.js +1 -0
- package/dist/server/table/table.d.ts +7 -0
- package/dist/server/table/table.js +39 -11
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bookOpening, deleteGame, deleteReview, siteGames, SiteGamesRefused } from "../../commands/index.js";
|
|
2
2
|
import { aiModelIn, annotatorOf, isReviewed, leadOf, reviewOf } from "../../core/index.js";
|
|
3
|
-
import { archivedGame, archivedGames, gameInAnalysis, gameStemOf, keptGame, keptGames } from "../../games/index.js";
|
|
3
|
+
import { archivedGame, archivedGames, archivedGameUnplayed, gameInAnalysis, gameStemOf, hasListTags, keptGame, keptGames } from "../../games/index.js";
|
|
4
4
|
import { gameFolder } from "../../profiles/index.js";
|
|
5
5
|
import { bodyOf, isJson, jsonOf, runRoute, sendJson, textOf } from "./http.js";
|
|
6
6
|
/** The games to analyse in the profile in use, the latest first, all of them for the page's search: those kept to study and those played, reviewed or not. */
|
|
@@ -10,7 +10,7 @@ export function analysesOf(home) {
|
|
|
10
10
|
const stamp = (name) => gameStemOf(name) ?? "";
|
|
11
11
|
const names = [...keptGames(folder), ...archivedGames(folder)].sort((one, other) => stamp(other).localeCompare(stamp(one)));
|
|
12
12
|
const taken = gameInAnalysis(folder)?.analyst?.game;
|
|
13
|
-
return names.flatMap((name) => itemOf(name, () => name.startsWith("study/") ? keptGame(folder, name) :
|
|
13
|
+
return names.flatMap((name) => itemOf(name, () => name.startsWith("study/") ? withBookOpening(keptGame(folder, name)) : archivedListed(folder, name), taken === name));
|
|
14
14
|
}
|
|
15
15
|
/** Answers the page's deletions, of a game or of its review alone, done as the command line does them: true when the request was one. */
|
|
16
16
|
export function answerAnalyses(request, response, url, served) {
|
|
@@ -51,10 +51,22 @@ export function sendSiteGames(response, served) {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
const deletions = { "/api/delete-game": deleteGame, "/api/delete-review": deleteReview };
|
|
54
|
+
/**
|
|
55
|
+
* A game of the archive for the list: its moves taken as written, not played, when the archive wrote its PlyCount, with its opening's tags, none for a game
|
|
56
|
+
* that has none; a game an older Chesspal archived is read whole, its opening looked up.
|
|
57
|
+
*/
|
|
58
|
+
function archivedListed(folder, name) {
|
|
59
|
+
const unplayed = archivedGameUnplayed(folder, name);
|
|
60
|
+
return hasListTags(unplayed.tags) ? { record: unplayed, opening: unplayed.tags.Opening } : withBookOpening(archivedGame(folder, name));
|
|
61
|
+
}
|
|
62
|
+
function withBookOpening(record) {
|
|
63
|
+
return { record, opening: bookOpening(record)?.name };
|
|
64
|
+
}
|
|
54
65
|
function itemOf(name, read, taken) {
|
|
55
66
|
let record;
|
|
67
|
+
let opening;
|
|
56
68
|
try {
|
|
57
|
-
record = read();
|
|
69
|
+
({ record, opening } = read());
|
|
58
70
|
}
|
|
59
71
|
catch {
|
|
60
72
|
// A game that cannot be read stays out of the list.
|
|
@@ -66,7 +78,6 @@ function itemOf(name, read, taken) {
|
|
|
66
78
|
}
|
|
67
79
|
const { tags } = record;
|
|
68
80
|
const date = tags.Date === undefined || tags.Date.includes("?") ? undefined : tags.Date;
|
|
69
|
-
const opening = bookOpening(record)?.name;
|
|
70
81
|
const aiModel = aiModelIn(record);
|
|
71
82
|
const site = tags.Site === undefined || /^[?\-\s]*$/.test(tags.Site) ? undefined : tags.Site;
|
|
72
83
|
const signed = annotatorOf(record);
|
|
@@ -47,8 +47,10 @@ const policy = `default-src 'self'; script-src 'self' 'sha256-${createHash("sha2
|
|
|
47
47
|
+ "base-uri 'none'; form-action 'none'; frame-ancestors 'none'";
|
|
48
48
|
const html = [
|
|
49
49
|
"<!doctype html>",
|
|
50
|
-
'<html lang="en">',
|
|
50
|
+
'<html lang="en" translate="no">',
|
|
51
51
|
'<meta charset="utf-8">',
|
|
52
|
+
// The page speaks the player's language, set once the app starts, and moves are never translated: no browser offers to.
|
|
53
|
+
'<meta name="google" content="notranslate">',
|
|
52
54
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
53
55
|
'<meta name="color-scheme" content="light dark">',
|
|
54
56
|
"<title>Chesspal</title>",
|
|
@@ -4,3 +4,8 @@ import type { Served } from "./page.ts";
|
|
|
4
4
|
export declare function answerGame(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
|
|
5
5
|
/** The chat calls a tool: when nobody heard the player before, the AI's clock, held since, starts now, with the time it was held given back. */
|
|
6
6
|
export declare function chatCalls(served: Served): void;
|
|
7
|
+
/**
|
|
8
|
+
* 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
|
|
9
|
+
* that comes or goes is news for them. True when the game cannot be read: they read it again.
|
|
10
|
+
*/
|
|
11
|
+
export declare function thinkingChanged(served: Served): boolean;
|
package/dist/server/page/game.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { analyseGame,
|
|
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, ponderingUntil, 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
7
|
import { tellWatchers } from "./events.js";
|
|
8
8
|
import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
|
|
9
9
|
import { profilesShown } from "./profiles.js";
|
|
10
|
-
import { chatToRelaunch, relaunchChat, tableShown } from "./table.js";
|
|
10
|
+
import { analystAtWork, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
|
|
11
11
|
import { updateShown } from "./update.js";
|
|
12
12
|
/** Answers the page's requests about the game: true when the request was one. */
|
|
13
13
|
export function answerGame(request, response, url, served) {
|
|
@@ -27,6 +27,18 @@ export function chatCalls(served) {
|
|
|
27
27
|
rerunIfChanged(() => giveHeldTime(served, folder));
|
|
28
28
|
}
|
|
29
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
|
+
}
|
|
30
42
|
/** The AI's clock, held while nobody heard the player, starts now: the time it was held is not the AI's. */
|
|
31
43
|
function giveHeldTime(served, folder) {
|
|
32
44
|
const current = loadGame(folder);
|
|
@@ -112,19 +124,21 @@ async function messageFromPage(request, response, served) {
|
|
|
112
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
|
|
113
125
|
// analysis every line does: a chat that answered in its own window would not hear the next (Priya).
|
|
114
126
|
if (shown !== undefined || wakesTheChat(current)) {
|
|
115
|
-
|
|
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);
|
|
116
129
|
}
|
|
117
130
|
send(response, 200, json, JSON.stringify({ said: true }));
|
|
118
131
|
}
|
|
119
132
|
/**
|
|
120
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
|
|
121
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
|
|
122
|
-
* 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).
|
|
123
136
|
*/
|
|
124
137
|
async function newGameFromPage(request, response, served) {
|
|
125
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.';
|
|
126
139
|
const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
|
|
127
140
|
let relaunching = false;
|
|
141
|
+
let chosen;
|
|
128
142
|
if (body === undefined) {
|
|
129
143
|
send(response, isJson(request) ? 400 : 415, json, JSON.stringify({ error: asked }));
|
|
130
144
|
return;
|
|
@@ -139,10 +153,17 @@ async function newGameFromPage(request, response, served) {
|
|
|
139
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.
|
|
140
154
|
relaunching = chatToRelaunch(served);
|
|
141
155
|
const sight = relaunching ? served.context : asTheChatSees(served.context, served.chat);
|
|
156
|
+
chosen = input.data;
|
|
142
157
|
rerunIfChanged(() => startNewGame(input.data, { ...sight, pageOpen: () => true }, "page"));
|
|
143
158
|
}
|
|
144
159
|
catch (error) {
|
|
145
|
-
|
|
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
|
+
}
|
|
146
167
|
return;
|
|
147
168
|
}
|
|
148
169
|
// A seat lost meanwhile leaves the chat as it is: the doorbell tells it.
|
|
@@ -151,6 +172,34 @@ async function newGameFromPage(request, response, served) {
|
|
|
151
172
|
}
|
|
152
173
|
send(response, 200, json, JSON.stringify({ started: true }));
|
|
153
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
|
+
}
|
|
154
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. */
|
|
155
204
|
async function drawFromPage(request, response, served) {
|
|
156
205
|
const asked = 'Send the draw as JSON: {"action": "offer"}, "accept" or "decline".';
|
|
@@ -255,7 +304,7 @@ async function studyFromPage(request, response, served) {
|
|
|
255
304
|
startAnalysis(gameFolder(served.context.home()), opened.name, served.context.now(), { brought, redo });
|
|
256
305
|
}
|
|
257
306
|
catch (error) {
|
|
258
|
-
send(response, 400, json, JSON.stringify({ error: error.message, refusal:
|
|
307
|
+
send(response, 400, json, JSON.stringify({ error: error.message, refusal: refusalOfStudy(error) }));
|
|
259
308
|
return;
|
|
260
309
|
}
|
|
261
310
|
if (brought) {
|
|
@@ -292,13 +341,15 @@ function sendGame(response, served) {
|
|
|
292
341
|
name: archiveName(current),
|
|
293
342
|
talk,
|
|
294
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.
|
|
295
|
-
book: bookPlies(current.game
|
|
344
|
+
book: bookPlies(current.game),
|
|
296
345
|
...(current.clock === undefined ? {} : { clock: current.clock }),
|
|
297
346
|
...(offer === undefined ? {} : { drawOffer: offer }),
|
|
298
347
|
...(coach === undefined ? {} : { coach }),
|
|
299
348
|
};
|
|
300
349
|
const analysis = current === undefined || endOf(current.game) !== undefined ? analysisShown(served) : undefined;
|
|
301
350
|
const aiModel = aiModelOf(folder);
|
|
351
|
+
const thinking = thinkingShown(served, current);
|
|
352
|
+
thinkingSent.set(served, thinking.thinking);
|
|
302
353
|
const body = {
|
|
303
354
|
// The page loaded from another version loads again, its files and all (Federico).
|
|
304
355
|
version,
|
|
@@ -308,7 +359,7 @@ function sendGame(response, served) {
|
|
|
308
359
|
opponent: served.opponent(),
|
|
309
360
|
...(aiModel === undefined ? {} : { aiModel }),
|
|
310
361
|
listening: served.bell.listening,
|
|
311
|
-
...
|
|
362
|
+
...thinking,
|
|
312
363
|
...(served.outdated ? { outdated: true } : {}),
|
|
313
364
|
...updateShown(served),
|
|
314
365
|
...tableShown(served),
|
|
@@ -338,6 +389,8 @@ function thinkingShown(served, current) {
|
|
|
338
389
|
signGoesIn(served, left);
|
|
339
390
|
return { thinking: "ponder" };
|
|
340
391
|
}
|
|
392
|
+
// The AI's sign the pages were last given, one a table.
|
|
393
|
+
const thinkingSent = new WeakMap();
|
|
341
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.
|
|
342
395
|
const signTimers = new WeakMap();
|
|
343
396
|
function signGoesIn(served, wait) {
|
|
@@ -368,11 +421,10 @@ function analysisShown(served) {
|
|
|
368
421
|
try {
|
|
369
422
|
const { name, record } = namedGame(served.context, analysis.game);
|
|
370
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
|
|
371
|
-
// started again since is gone (Priya).
|
|
424
|
+
// started again since is gone (Priya).
|
|
372
425
|
const { bell } = served;
|
|
373
|
-
const chat = { ...served.context, folder: served.attached?.chat.folder ?? served.folder };
|
|
374
426
|
const reviewed = isReviewed(record);
|
|
375
|
-
const reviewing = !reviewed && (bell.inTurn || (bell.listening &&
|
|
427
|
+
const reviewing = !reviewed && (bell.inTurn || (bell.listening && analystAtWork(served, analysis.game)));
|
|
376
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).
|
|
377
429
|
const state = reviewed ? {} : reviewing ? { reviewing: true } : { incomplete: true };
|
|
378
430
|
// How far the book goes, which the page cannot tell without the book's moves: its card names the opening on those moves.
|
|
@@ -387,10 +439,23 @@ function analysisShown(served) {
|
|
|
387
439
|
function lastPlayed(context) {
|
|
388
440
|
const { name, record } = gameToStudy(context);
|
|
389
441
|
if (name === undefined) {
|
|
390
|
-
throw new
|
|
442
|
+
throw new GameNotArchived();
|
|
391
443
|
}
|
|
392
444
|
return { name, record };
|
|
393
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
|
+
}
|
|
394
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. */
|
|
395
460
|
async function kept(served, pgn) {
|
|
396
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,
|
|
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
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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;
|
package/dist/server/page/page.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
219
|
+
function gameSeen(home) {
|
|
184
220
|
try {
|
|
185
|
-
const folder = gameFolder(
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
//
|
|
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);
|