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.
- package/README.md +11 -12
- package/dist/cli/cli.js +83 -3
- package/dist/cli/command-line.js +26 -9
- package/dist/cli/icon.d.ts +25 -0
- package/dist/cli/icon.js +80 -0
- package/dist/commands/agents.d.ts +19 -2
- package/dist/commands/agents.js +39 -13
- package/dist/commands/ai-say.js +3 -4
- package/dist/commands/analyse-game.d.ts +4 -0
- package/dist/commands/analyse-game.js +46 -13
- package/dist/commands/annotate-game.d.ts +2 -0
- package/dist/commands/annotate-game.js +40 -18
- package/dist/commands/answer-lines.d.ts +2 -0
- package/dist/commands/answer-lines.js +13 -3
- package/dist/commands/coach.js +4 -2
- package/dist/commands/context.d.ts +9 -1
- package/dist/commands/context.js +5 -0
- package/dist/commands/delete-review.js +9 -5
- package/dist/commands/game-summary.d.ts +2 -0
- package/dist/commands/game-summary.js +4 -0
- package/dist/commands/index.d.ts +3 -2
- package/dist/commands/index.js +7 -4
- package/dist/commands/init.js +2 -4
- package/dist/commands/instructions.d.ts +9 -0
- package/dist/commands/instructions.js +30 -0
- package/dist/commands/lead-input.d.ts +1 -1
- package/dist/commands/lead-input.js +7 -4
- package/dist/commands/page-link.js +6 -5
- package/dist/commands/past-games.d.ts +1 -1
- package/dist/commands/past-games.js +12 -5
- package/dist/commands/player-move.js +2 -3
- package/dist/commands/profiles.d.ts +23 -9
- package/dist/commands/profiles.js +76 -35
- package/dist/commands/ratings.d.ts +8 -0
- package/dist/commands/ratings.js +45 -0
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/registry.js +3 -2
- package/dist/commands/say.d.ts +1 -1
- package/dist/commands/say.js +1 -1
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +46 -27
- package/dist/commands/study.js +8 -5
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/review.d.ts +2 -0
- package/dist/core/review.js +5 -1
- package/dist/games/ai-model.d.ts +4 -2
- package/dist/games/ai-model.js +10 -1
- package/dist/games/analysis.d.ts +26 -3
- package/dist/games/analysis.js +41 -6
- package/dist/games/archive.d.ts +6 -3
- package/dist/games/archive.js +13 -5
- package/dist/games/current-game.d.ts +10 -1
- package/dist/games/current-game.js +49 -15
- package/dist/games/files.d.ts +5 -0
- package/dist/games/files.js +42 -1
- package/dist/games/index.d.ts +3 -3
- package/dist/games/index.js +3 -3
- package/dist/home/data-folder.d.ts +2 -0
- package/dist/home/data-folder.js +5 -1
- package/dist/home/index.d.ts +1 -1
- package/dist/home/index.js +1 -1
- package/dist/host-files/agents.d.ts +2 -2
- package/dist/host-files/agents.js +12 -6
- package/dist/host-files/host-files.d.ts +5 -3
- package/dist/host-files/host-files.js +15 -8
- package/dist/host-files/index.d.ts +2 -2
- package/dist/host-files/index.js +2 -2
- package/dist/page/app/app.js +29 -16
- package/dist/page/app/title.js +17 -0
- package/dist/page/avatars/torretta.js +4 -4
- package/dist/page/dialogs/analyses.js +4 -4
- package/dist/page/dialogs/dialog.js +4 -1
- package/dist/page/dialogs/dialogs.css +84 -10
- package/dist/page/dialogs/profiles.js +95 -42
- package/dist/page/game/column.js +5 -1
- package/dist/page/game/plies.js +7 -4
- package/dist/page/guide/blocks.js +67 -0
- package/dist/page/guide/en.md +66 -0
- package/dist/page/guide/foot.js +18 -0
- package/dist/page/guide/guide.css +200 -0
- package/dist/page/guide/guide.js +89 -0
- package/dist/page/guide/it.md +66 -0
- package/dist/page/guide/markdown.js +67 -0
- package/dist/page/helps/helps.css +7 -10
- package/dist/page/layout/favicon.svg +13 -0
- package/dist/page/layout/header.js +3 -3
- package/dist/page/layout/icon.icns +0 -0
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/icon.png +0 -0
- package/dist/page/layout/layout.css +111 -4
- package/dist/page/layout/window.js +45 -0
- package/dist/page/main.js +4 -1
- package/dist/page/players/card.js +1 -1
- package/dist/page/profile/profile.js +14 -9
- package/dist/page/profile/settings.js +9 -0
- package/dist/page/style.css +2 -0
- package/dist/page/table/opener.css +65 -0
- package/dist/page/table/opener.js +59 -0
- package/dist/page/talk/chat.css +10 -3
- package/dist/page/talk/talk.css +38 -26
- package/dist/page/talk/talk.js +4 -2
- package/dist/page/text.js +83 -17
- package/dist/page/theme.css +7 -0
- package/dist/profiles/index.d.ts +2 -1
- package/dist/profiles/index.js +2 -1
- package/dist/profiles/profiles.d.ts +10 -15
- package/dist/profiles/profiles.js +30 -15
- package/dist/profiles/ratings.d.ts +35 -0
- package/dist/profiles/ratings.js +61 -0
- package/dist/server/bell.d.ts +24 -3
- package/dist/server/bell.js +84 -10
- package/dist/server/caller.d.ts +2 -1
- package/dist/server/caller.js +3 -2
- package/dist/server/mcp.d.ts +7 -3
- package/dist/server/mcp.js +100 -52
- package/dist/server/page/analyses.d.ts +2 -0
- package/dist/server/page/analyses.js +20 -5
- package/dist/server/page/attach.d.ts +4 -0
- package/dist/server/page/attach.js +112 -0
- package/dist/server/page/chat.d.ts +60 -0
- package/dist/server/page/chat.js +79 -0
- package/dist/server/page/events.d.ts +7 -0
- package/dist/server/page/events.js +20 -0
- package/dist/server/page/files.js +10 -1
- package/dist/server/page/game.js +43 -23
- package/dist/server/page/page.d.ts +47 -7
- package/dist/server/page/page.js +69 -48
- package/dist/server/page/profiles.d.ts +5 -0
- package/dist/server/page/profiles.js +26 -10
- package/dist/server/page/table.d.ts +39 -0
- package/dist/server/page/table.js +102 -0
- package/dist/server/table/browser.d.ts +6 -0
- package/dist/server/table/browser.js +48 -0
- package/dist/server/table/hosts/chats.d.ts +7 -0
- package/dist/server/table/hosts/chats.js +31 -0
- package/dist/server/table/hosts/find.d.ts +2 -0
- package/dist/server/table/hosts/find.js +20 -0
- package/dist/server/table/hosts/hosts.d.ts +34 -0
- package/dist/server/table/hosts/hosts.js +49 -0
- package/dist/server/table/hosts/index.d.ts +3 -0
- package/dist/server/table/hosts/index.js +4 -0
- package/dist/server/table/hosts/play.d.ts +25 -0
- package/dist/server/table/hosts/play.js +152 -0
- package/dist/server/table/hosts/seat.d.ts +25 -0
- package/dist/server/table/hosts/seat.js +74 -0
- package/dist/server/table/hosts/terminal.d.ts +6 -0
- package/dist/server/table/hosts/terminal.js +63 -0
- package/dist/server/table/icon/index.d.ts +32 -0
- package/dist/server/table/icon/index.js +83 -0
- package/dist/server/table/icon/linux.d.ts +5 -0
- package/dist/server/table/icon/linux.js +47 -0
- package/dist/server/table/icon/macos.d.ts +3 -0
- package/dist/server/table/icon/macos.js +38 -0
- package/dist/server/table/icon/windows.d.ts +3 -0
- package/dist/server/table/icon/windows.js +48 -0
- package/dist/server/table/index.d.ts +6 -0
- package/dist/server/table/index.js +7 -0
- package/dist/server/table/launch.d.ts +19 -0
- package/dist/server/table/launch.js +78 -0
- package/dist/server/table/link.d.ts +10 -0
- package/dist/server/table/link.js +230 -0
- package/dist/server/table/table.d.ts +35 -0
- package/dist/server/table/table.js +94 -0
- package/dist/sites/chesscom.d.ts +4 -0
- package/dist/sites/chesscom.js +25 -0
- package/dist/sites/choice.d.ts +17 -0
- package/dist/sites/choice.js +13 -0
- package/dist/sites/index.d.ts +2 -0
- package/dist/sites/index.js +4 -0
- package/dist/sites/lichess.d.ts +4 -0
- package/dist/sites/lichess.js +32 -0
- package/dist/sites/request.d.ts +11 -0
- package/dist/sites/request.js +15 -0
- package/dist/sites/sites.d.ts +6 -0
- package/dist/sites/sites.js +33 -0
- package/instructions/analysis.md +1 -1
- package/instructions/analyst-agent.md +1 -1
- package/instructions/coach.md +2 -1
- package/instructions/opponent.md +6 -6
- package/instructions/ponder.md +1 -0
- package/instructions/start.md +1 -0
- 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 = {
|
|
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) {
|
package/dist/server/page/game.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
171
|
-
|
|
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.
|
|
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.
|
|
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 },
|
|
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
|
-
|
|
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
|
|
18
|
-
*
|
|
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,
|
|
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
|
-
/**
|
|
31
|
-
|
|
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
|