chesspal 0.1.0 → 0.2.1
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 +45 -82
- package/dist/cli/chat-hook.js +1 -1
- package/dist/cli/command-line.d.ts +1 -2
- package/dist/cli/command-line.js +2 -3
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +5 -14
- package/dist/commands/ai-move.js +28 -35
- package/dist/commands/ai-say.d.ts +3 -1
- package/dist/commands/ai-say.js +24 -6
- package/dist/commands/analyse-game.d.ts +4 -0
- package/dist/commands/analyse-game.js +43 -16
- package/dist/commands/annotate-game.d.ts +4 -0
- package/dist/commands/annotate-game.js +65 -12
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +16 -5
- package/dist/commands/context.js +1 -0
- package/dist/commands/delete-game.d.ts +7 -0
- package/dist/commands/delete-game.js +30 -0
- package/dist/commands/delete-review.d.ts +8 -0
- package/dist/commands/delete-review.js +34 -0
- package/dist/commands/drawings.d.ts +0 -2
- package/dist/commands/drawings.js +5 -3
- package/dist/commands/end-game.js +5 -2
- package/dist/commands/export-game.d.ts +1 -1
- package/dist/commands/export-game.js +4 -3
- package/dist/commands/game-summary.d.ts +21 -38
- package/dist/commands/game-summary.js +30 -167
- package/dist/commands/index.d.ts +15 -0
- package/dist/commands/index.js +47 -2
- package/dist/commands/init.d.ts +3 -3
- package/dist/commands/init.js +4 -4
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -0
- package/dist/commands/look.js +4 -1
- package/dist/commands/new-game.d.ts +2 -3
- package/dist/commands/new-game.js +7 -4
- package/dist/commands/play-move.d.ts +8 -15
- package/dist/commands/play-move.js +5 -13
- package/dist/commands/player-move.d.ts +2 -6
- package/dist/commands/player-move.js +13 -6
- package/dist/commands/player-say.d.ts +1 -1
- package/dist/commands/player-say.js +1 -1
- package/dist/commands/ponder.d.ts +10 -0
- package/dist/commands/ponder.js +33 -21
- package/dist/commands/position.d.ts +1 -1
- package/dist/commands/position.js +3 -1
- package/dist/commands/profiles.d.ts +13 -4
- package/dist/commands/profiles.js +49 -15
- package/dist/commands/registry.d.ts +8 -3
- package/dist/commands/registry.js +12 -11
- package/dist/commands/say.d.ts +1 -1
- package/dist/commands/say.js +16 -16
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +9 -3
- package/dist/commands/status.js +40 -15
- package/dist/commands/study-games.js +1 -1
- package/dist/commands/study.js +3 -19
- package/dist/core/facts.js +15 -15
- package/dist/core/game.d.ts +3 -1
- package/dist/core/game.js +8 -4
- package/dist/core/illegal.js +1 -1
- package/dist/core/index.d.ts +5 -4
- package/dist/core/index.js +5 -4
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -0
- package/dist/core/levels.d.ts +1 -1
- package/dist/core/levels.js +1 -1
- package/dist/core/moods.d.ts +0 -2
- package/dist/core/moods.js +2 -2
- package/dist/core/openings.d.ts +0 -2
- package/dist/core/openings.js +1 -1
- package/dist/core/profile.js +1 -1
- package/dist/core/record.d.ts +1 -1
- package/dist/core/record.js +1 -1
- package/dist/core/review.d.ts +18 -3
- package/dist/core/review.js +52 -9
- package/dist/games/ai-model.d.ts +4 -0
- package/dist/games/ai-model.js +9 -0
- package/dist/games/analysis.d.ts +1 -1
- package/dist/games/archive.d.ts +25 -4
- package/dist/games/archive.js +99 -25
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +2 -0
- package/dist/games/current-game.js +7 -1
- package/dist/games/files.js +1 -0
- package/dist/games/index.d.ts +5 -4
- package/dist/games/index.js +5 -4
- package/dist/games/talk.d.ts +1 -5
- package/dist/games/talk.js +14 -14
- package/dist/home/open-folder.js +1 -1
- package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +7 -2
- package/dist/host-files/host-files.js +27 -12
- package/dist/host-files/index.d.ts +2 -2
- package/dist/host-files/index.js +3 -3
- package/dist/index.d.ts +1 -3
- package/dist/index.js +1 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/{app.js → app/app.js} +116 -231
- package/dist/page/app/keys.js +94 -0
- package/dist/page/app/phase.js +30 -0
- package/dist/page/app/served.js +73 -0
- package/dist/page/avatars/avatars.js +19 -0
- package/dist/page/avatars/thought.js +10 -0
- package/dist/page/base.css +160 -0
- package/dist/page/board/board.css +959 -0
- package/dist/page/board/board.js +125 -0
- package/dist/page/{explain.js → board/explain.js} +1 -1
- package/dist/page/board/marks.js +160 -0
- package/dist/page/board/motion.js +71 -0
- package/dist/page/board/pointer.js +239 -0
- package/dist/page/board/squares.js +26 -0
- package/dist/page/dialogs/analyses.js +195 -0
- package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
- package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
- package/dist/page/dialogs/search.js +31 -0
- package/dist/page/dialogs/settings.js +42 -0
- package/dist/page/dialogs/studied.js +39 -0
- package/dist/page/dialogs/study.js +51 -0
- package/dist/page/{badges.js → game/badges.js} +1 -1
- package/dist/page/{browse.js → game/browse.js} +1 -1
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +55 -0
- package/dist/page/{ending.js → game/ending.js} +2 -2
- package/dist/page/game/game.css +577 -0
- package/dist/page/{move-list.js → game/move-list.js} +10 -6
- package/dist/page/{plies.js → game/plies.js} +30 -10
- package/dist/page/game/result-tile.js +14 -0
- package/dist/page/helps/helps.css +689 -0
- package/dist/page/helps/helps.js +220 -0
- package/dist/page/helps/view.js +127 -0
- package/dist/page/layout/fold-tab.js +11 -0
- package/dist/page/{folds.js → layout/folds.js} +1 -1
- package/dist/page/{header.js → layout/header.js} +5 -4
- package/dist/page/layout/layout.css +834 -0
- package/dist/page/lead/lead.js +82 -0
- package/dist/page/lead/view.js +38 -0
- package/dist/page/main.js +2 -2
- package/dist/page/pieces/CREDITS.md +12 -29
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +3 -2
- package/dist/page/pieces/nitida.svg +3 -2
- package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
- package/dist/page/pieces/torretta-small.svg +15 -14
- package/dist/page/pieces/torretta.svg +17 -15
- package/dist/page/{player-card.js → players/card.js} +2 -6
- package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
- package/dist/page/{profile.js → profile/profile.js} +9 -5
- package/dist/page/{settings.js → profile/settings.js} +5 -4
- package/dist/page/style.css +9 -4398
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/{talk.js → talk/talk.js} +39 -46
- package/dist/page/text.js +137 -19
- package/dist/page/theme.css +292 -0
- package/dist/profiles/index.d.ts +1 -1
- package/dist/profiles/profiles.d.ts +15 -4
- package/dist/profiles/profiles.js +24 -7
- package/dist/server/bell.d.ts +10 -2
- package/dist/server/bell.js +28 -12
- package/dist/server/caller.d.ts +14 -3
- package/dist/server/caller.js +44 -11
- package/dist/server/mcp.d.ts +2 -3
- package/dist/server/mcp.js +53 -19
- package/dist/server/page/analyses.d.ts +23 -0
- package/dist/server/page/analyses.js +65 -0
- package/dist/server/page/files.d.ts +5 -0
- package/dist/server/page/files.js +92 -0
- package/dist/server/page/game.d.ts +6 -0
- package/dist/server/page/game.js +381 -0
- package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
- package/dist/server/{http.js → page/http.js} +11 -0
- package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
- package/dist/server/page/page.js +229 -0
- package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
- package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +3 -7
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +4 -4
- package/package.json +1 -1
- package/dist/host-files/ponder-agents.js +0 -40
- package/dist/page/avatars.js +0 -16
- package/dist/page/board.js +0 -566
- package/dist/page/fold-tab.js +0 -9
- package/dist/page/game-column.js +0 -44
- package/dist/page/helps-view.js +0 -51
- package/dist/page/helps.js +0 -90
- package/dist/page/pieces/cburnett-small.svg +0 -20
- package/dist/page/pieces/cburnett.svg +0 -20
- package/dist/page/settings-panel.js +0 -32
- package/dist/page/studied.js +0 -14
- package/dist/page/study-dialog.js +0 -58
- package/dist/server/analyses.d.ts +0 -17
- package/dist/server/analyses.js +0 -46
- package/dist/server/page.js +0 -657
- /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
- /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
- /package/dist/page/{model-name.js → players/model-name.js} +0 -0
- /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
- /package/dist/page/{voice.js → talk/voice.js} +0 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { analyseGame, answerDraw, bookPlies, ChatNotClear, DrawRefused, endGame, gameAt, gameToStudy, namedGame, newGame, offerDraw, playMove, samePosition, say, startNewGame, } from "../../commands/index.js";
|
|
2
|
+
import { clockStarted, isReviewed, writePgn } from "../../core/index.js";
|
|
3
|
+
import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, heardLines, loadGame, loadTableTalk, playerPgn, saveGame, startAnalysis, talkLimits, wakesTheChat, WordsRefused, } from "../../games/index.js";
|
|
4
|
+
import { gameFolder, helpsIn } from "../../profiles/index.js";
|
|
5
|
+
import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
|
|
6
|
+
import { profilesShown } from "./profiles.js";
|
|
7
|
+
/** Answers the page's requests about the game: true when the request was one. */
|
|
8
|
+
export function answerGame(request, response, url, served) {
|
|
9
|
+
const route = routes[`${request.method ?? ""} ${url.pathname}`];
|
|
10
|
+
if (route === undefined) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
runRoute(response, () => route(request, response, served));
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
/** 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. */
|
|
17
|
+
export function chatCalls(served) {
|
|
18
|
+
const folder = gameFolder(served.context.home());
|
|
19
|
+
const current = loadGame(folder);
|
|
20
|
+
const held = current !== undefined && aiHeld(served, current);
|
|
21
|
+
if (served.bell.chatCalls() && held) {
|
|
22
|
+
saveGame(folder, { ...current, clock: { ...current.clock, since: served.context.now().toISOString() } });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const routes = {
|
|
26
|
+
"GET /api/game": (request, response, served) => sendGame(response, served),
|
|
27
|
+
"POST /api/move": moveFromPage,
|
|
28
|
+
"POST /api/message": messageFromPage,
|
|
29
|
+
"POST /api/new": newGameFromPage,
|
|
30
|
+
"POST /api/draw": drawFromPage,
|
|
31
|
+
"POST /api/resign": resignFromPage,
|
|
32
|
+
"POST /api/start": startFromPage,
|
|
33
|
+
"POST /api/study": studyFromPage,
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* The player's move from the page, with their words when they wrote some: played as player_move plays it (D-47) and saved with the words, refused in the
|
|
37
|
+
* player's words, and the doorbell rung for the chat.
|
|
38
|
+
*/
|
|
39
|
+
async function moveFromPage(request, response, served) {
|
|
40
|
+
// A form of another site can post text, never JSON without asking first: the type keeps it out, with the origin.
|
|
41
|
+
if (!isJson(request)) {
|
|
42
|
+
send(response, 415, json, JSON.stringify({ error: 'Send the move as JSON: {"move": "e2e4"}.' }));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const body = jsonOf(await bodyOf(request));
|
|
46
|
+
const move = textOf(body?.move);
|
|
47
|
+
if (move === undefined || (body?.message !== undefined && typeof body.message !== "string")) {
|
|
48
|
+
send(response, 400, json, JSON.stringify({ error: 'Send the move as JSON, in SAN or as squares, with your words if you like: {"move": "e2e4", "message": "…"}.' }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const refused = refusalOf(served);
|
|
52
|
+
if (refused !== undefined) {
|
|
53
|
+
send(response, 409, json, JSON.stringify(refused));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const message = textOf(body?.message);
|
|
57
|
+
let illegal = false;
|
|
58
|
+
try {
|
|
59
|
+
const now = served.context.now();
|
|
60
|
+
const refuse = (problem) => {
|
|
61
|
+
illegal = true;
|
|
62
|
+
return problem;
|
|
63
|
+
};
|
|
64
|
+
playMove(gameFolder(served.context.home()), now, "player", move, refuse, message === undefined ? {} : { said: { text: message, at: now } });
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// The game may have changed since the page read it: the move refused, the AI's turn, a flag fallen.
|
|
68
|
+
send(response, 409, json, JSON.stringify(refusalFrom(error, illegal ? "illegal" : refusalOf(served)?.refusal)));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// The pages hear of the move from the watcher, as of every other.
|
|
72
|
+
served.bell.ring();
|
|
73
|
+
send(response, 200, json, JSON.stringify({ played: true }));
|
|
74
|
+
}
|
|
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. */
|
|
76
|
+
async function messageFromPage(request, response, served) {
|
|
77
|
+
if (!isJson(request)) {
|
|
78
|
+
send(response, 415, json, JSON.stringify({ error: 'Send the message as JSON: {"text": "…"}.' }));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const text = textOf(jsonOf(await bodyOf(request))?.text);
|
|
82
|
+
if (text === undefined) {
|
|
83
|
+
send(response, 400, json, JSON.stringify({ error: `Send the message as JSON, ${talkLimits.player} characters at most: {"text": "…"}.` }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let current;
|
|
87
|
+
try {
|
|
88
|
+
current = say(gameFolder(served.context.home()), "player", text, served.context.now());
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const noGame = refusalOf(served)?.refusal === "no-game";
|
|
92
|
+
send(response, 409, json, JSON.stringify(refusalFrom(error, noGame ? "no-game" : undefined)));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
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)) {
|
|
97
|
+
served.bell.ring();
|
|
98
|
+
}
|
|
99
|
+
send(response, 200, json, JSON.stringify({ said: true }));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 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.
|
|
104
|
+
*/
|
|
105
|
+
async function newGameFromPage(request, response, served) {
|
|
106
|
+
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
|
+
const body = isJson(request) ? jsonOf(await bodyOf(request)) : undefined;
|
|
108
|
+
if (body === undefined) {
|
|
109
|
+
send(response, isJson(request) ? 400 : 415, json, JSON.stringify({ error: asked }));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const before = loadGame(gameFolder(served.context.home()));
|
|
114
|
+
const input = newGame.input.safeParse({ ai_colour: body.ai_colour ?? before?.ai ?? "black", level: body.level, clock: body.clock, replace: true });
|
|
115
|
+
if (!input.success) {
|
|
116
|
+
send(response, 400, json, JSON.stringify({ error: asked }));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
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");
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
send(response, 409, json, JSON.stringify(refusalFrom(error)));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
served.bell.ring();
|
|
127
|
+
send(response, 200, json, JSON.stringify({ started: true }));
|
|
128
|
+
}
|
|
129
|
+
/** 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. */
|
|
130
|
+
async function drawFromPage(request, response, served) {
|
|
131
|
+
const asked = 'Send the draw as JSON: {"action": "offer"}, "accept" or "decline".';
|
|
132
|
+
if (!isJson(request)) {
|
|
133
|
+
send(response, 415, json, JSON.stringify({ error: asked }));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const action = jsonOf(await bodyOf(request))?.action;
|
|
137
|
+
if (action !== "offer" && action !== "accept" && action !== "decline") {
|
|
138
|
+
send(response, 400, json, JSON.stringify({ error: asked }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
if (action === "offer") {
|
|
143
|
+
offerDraw(served.context);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
answerDraw(served.context, action === "accept");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
send(response, 409, json, JSON.stringify(refusalFrom(error, refusalOf(served)?.refusal)));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (action !== "decline") {
|
|
154
|
+
served.bell.ring();
|
|
155
|
+
}
|
|
156
|
+
send(response, 200, json, JSON.stringify({ done: true }));
|
|
157
|
+
}
|
|
158
|
+
/** The player starts the clocks from the page, which wait from the game's start; the doorbell wakes the chat when its move is first, waiting for this. */
|
|
159
|
+
function startFromPage(request, response, served) {
|
|
160
|
+
if (!isJson(request)) {
|
|
161
|
+
send(response, 415, json, JSON.stringify({ error: "Send the request as JSON: {}." }));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const folder = gameFolder(served.context.home());
|
|
165
|
+
const current = loadGame(folder);
|
|
166
|
+
if (current?.clock?.waiting !== true || endOf(current.game) !== undefined) {
|
|
167
|
+
send(response, 409, json, JSON.stringify({ error: "No clock waits to start: start a game with a clock first." }));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
saveGame(folder, { ...current, clock: clockStarted(current.clock, served.context.now()) });
|
|
171
|
+
if (current.game.position.turn === current.ai) {
|
|
172
|
+
served.bell.ring();
|
|
173
|
+
}
|
|
174
|
+
send(response, 200, json, JSON.stringify({ started: true }));
|
|
175
|
+
}
|
|
176
|
+
/** 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
|
+
function resignFromPage(request, response, served) {
|
|
178
|
+
if (!isJson(request)) {
|
|
179
|
+
send(response, 415, json, JSON.stringify({ error: "Send the request as JSON: {}." }));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
endGame.run({ result: "player_resigns" }, served.context);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
send(response, 409, json, JSON.stringify(refusalFrom(error, refusalOf(served)?.refusal === "no-game" ? "no-game" : undefined)));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
served.bell.ring();
|
|
190
|
+
send(response, 200, json, JSON.stringify({ resigned: true }));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* A game the player brings from elsewhere to analyse: kept to study and put in analysis as analyse_game does with a PGN, the page showing it at once,
|
|
194
|
+
* and the doorbell rung for the chat to go over it. Never while a game goes on.
|
|
195
|
+
*/
|
|
196
|
+
async function studyFromPage(request, response, served) {
|
|
197
|
+
const asked = 'Send the game as JSON: {"pgn": "[White \\"…\\"] … 1. e4 e5 …"}, one to open again by its name: {"game": "study/2026-11-04-153012.pgn"}, '
|
|
198
|
+
+ 'or the game just over: {"last": true}.';
|
|
199
|
+
if (!isJson(request)) {
|
|
200
|
+
send(response, 415, json, JSON.stringify({ error: asked }));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const body = jsonOf((await bytesOf(request, pgnLimit))?.toString("utf8"));
|
|
204
|
+
const pgn = textOf(body?.pgn);
|
|
205
|
+
const game = textOf(body?.game);
|
|
206
|
+
const last = body?.last === true;
|
|
207
|
+
// A game a link of the page opens is there to look at: the chat is not asked to go over it.
|
|
208
|
+
const look = body?.look === true;
|
|
209
|
+
if (pgn === undefined && game === undefined && !last) {
|
|
210
|
+
send(response, 400, json, JSON.stringify({ error: asked, refusal: "pgn-unreadable" }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (refusalOf(served)?.refusal !== "no-game") {
|
|
214
|
+
send(response, 409, json, JSON.stringify({ error: "A game is going on: finish it before you bring another to analyse.", refusal: "game-going" }));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let reviewed;
|
|
218
|
+
let brought;
|
|
219
|
+
try {
|
|
220
|
+
// Without what the chat was told: the analyst's instructions stay for the AI's own analyse_game.
|
|
221
|
+
const opened = last ? lastPlayed(served.context) : game === undefined ? kept(served, pgn) : namedGame(served.context, game);
|
|
222
|
+
reviewed = isReviewed(opened.record);
|
|
223
|
+
brought = !reviewed && !look;
|
|
224
|
+
// 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);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
send(response, 400, json, JSON.stringify({ error: error.message, refusal: "pgn-unreadable" }));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (brought) {
|
|
232
|
+
served.bell.ring();
|
|
233
|
+
}
|
|
234
|
+
send(response, 200, json, JSON.stringify({ kept: true, reviewed }));
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The game in progress or just ended, as the player may see it: its PGN without the AI's plans while it goes on, the AI's side, when it started, and the
|
|
238
|
+
* talk on the page, each line of the player's with how far it got (D-53), and the draw offer that stands (D-65); the AI's name and model (D-82); and the
|
|
239
|
+
* profiles.
|
|
240
|
+
*/
|
|
241
|
+
function sendGame(response, served) {
|
|
242
|
+
const folder = gameFolder(served.context.home());
|
|
243
|
+
const current = gameNow(served);
|
|
244
|
+
// With no game the page still shows the talk before the first (D-77).
|
|
245
|
+
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] });
|
|
248
|
+
const offer = current === undefined ? undefined : drawOfferOf(current);
|
|
249
|
+
const coach = coachOn(folder, current);
|
|
250
|
+
const game = current === undefined ? null : {
|
|
251
|
+
pgn: playerPgn(folder, current),
|
|
252
|
+
ai: current.ai,
|
|
253
|
+
started: current.started,
|
|
254
|
+
// Its name in the archive, for the page's address.
|
|
255
|
+
name: archiveName(current),
|
|
256
|
+
talk,
|
|
257
|
+
// 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.
|
|
258
|
+
book: bookPlies(current.game.record),
|
|
259
|
+
...(current.clock === undefined ? {} : { clock: current.clock }),
|
|
260
|
+
...(offer === undefined ? {} : { drawOffer: offer }),
|
|
261
|
+
...(coach === undefined ? {} : { coach }),
|
|
262
|
+
};
|
|
263
|
+
const analysis = current === undefined || endOf(current.game) !== undefined ? analysisShown(served) : undefined;
|
|
264
|
+
const aiModel = aiModelOf(folder);
|
|
265
|
+
const body = {
|
|
266
|
+
game,
|
|
267
|
+
...(game === null && talk.length > 0 ? { talk } : {}),
|
|
268
|
+
...(analysis === undefined ? {} : { analysis }),
|
|
269
|
+
opponent: served.opponent(),
|
|
270
|
+
...(aiModel === undefined ? {} : { aiModel }),
|
|
271
|
+
listening: served.bell.listening,
|
|
272
|
+
...thinkingShown(served, current),
|
|
273
|
+
...(served.outdated ? { outdated: true } : {}),
|
|
274
|
+
...profilesShown(served.context.home()),
|
|
275
|
+
};
|
|
276
|
+
send(response, 200, json, JSON.stringify(body));
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* What the AI thinks on now, for the page's sign: its move, while the chat is in a turn the doorbell woke it for or on the position it is to move in; the
|
|
280
|
+
* player's time, while the ponder agent thinks, before it waits for their move (D-72). Never the coach's, which is not the opponent (D-88).
|
|
281
|
+
*/
|
|
282
|
+
function thinkingShown(served, current) {
|
|
283
|
+
if (current === undefined || endOf(current.game) !== undefined) {
|
|
284
|
+
return {};
|
|
285
|
+
}
|
|
286
|
+
const position = { started: current.started, plies: current.game.record.moves.length };
|
|
287
|
+
if (current.game.position.turn === current.ai) {
|
|
288
|
+
const seen = served.context.positionSeen;
|
|
289
|
+
const onIt = served.bell.woke || (seen !== undefined && seen !== null && samePosition(seen, position));
|
|
290
|
+
return served.bell.inTurn && onIt ? { thinking: "move" } : {};
|
|
291
|
+
}
|
|
292
|
+
return served.pondering !== undefined && samePosition(served.pondering, position) ? { thinking: "ponder" } : {};
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* What the coach wrote that the player may see now, in a game going on with the helps on (D-88): its judgments, those whose note names a move they may still
|
|
296
|
+
* play once they have moved, its hint for the position it was given in, and who it saw ahead (D-90); none without anything. Once the game is over its
|
|
297
|
+
* PGN has the judgments and the leads.
|
|
298
|
+
*/
|
|
299
|
+
function coachOn(folder, current) {
|
|
300
|
+
if (current === undefined || endOf(current.game) !== undefined || !helpsIn(folder)) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
const shown = coachShown(coachWords(folder, current), current);
|
|
304
|
+
return shown.judgments.length === 0 && shown.hint === undefined && shown.leads === undefined ? undefined : shown;
|
|
305
|
+
}
|
|
306
|
+
/** The game in analysis, once no game goes on (D-69): its name and its PGN with the AI's review; none without one, or when it cannot be read. */
|
|
307
|
+
function analysisShown(served) {
|
|
308
|
+
const analysis = gameInAnalysis(gameFolder(served.context.home()));
|
|
309
|
+
if (analysis === undefined) {
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
const { name, record } = namedGame(served.context, analysis.game);
|
|
314
|
+
// How far the book goes, which the page cannot tell without the book's moves: its card names the opening on those moves.
|
|
315
|
+
return { name, pgn: writePgn(record), book: bookPlies(record) };
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// A game gone from the archive is no longer shown.
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/** The game just over, or else the last one played, as analyse_game takes it without a name: always one of the archive. */
|
|
323
|
+
function lastPlayed(context) {
|
|
324
|
+
const { name, record } = gameToStudy(context);
|
|
325
|
+
if (name === undefined) {
|
|
326
|
+
throw new Error("The game just over is not in the archive yet: try again in a moment.");
|
|
327
|
+
}
|
|
328
|
+
return { name, record };
|
|
329
|
+
}
|
|
330
|
+
/** 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
|
+
function kept(served, pgn) {
|
|
332
|
+
analyseGame.run({ pgn }, { ...served.context, told: undefined });
|
|
333
|
+
return namedGame(served.context, gameInAnalysis(gameFolder(served.context.home())).game);
|
|
334
|
+
}
|
|
335
|
+
/** The longest PGN the page brings: a long game with its clocks, and the comments a site adds, which go. */
|
|
336
|
+
const pgnLimit = 256 * 1024;
|
|
337
|
+
/** Why the player cannot move now; undefined when they can. */
|
|
338
|
+
function refusalOf(served) {
|
|
339
|
+
try {
|
|
340
|
+
const current = gameNow(served);
|
|
341
|
+
if (current === undefined || endOf(current.game) !== undefined) {
|
|
342
|
+
return { error: "There is no game going on: start one in the chat.", refusal: "no-game" };
|
|
343
|
+
}
|
|
344
|
+
return current.game.position.turn === current.ai ? { error: "Wait for the AI's move.", refusal: "ai-to-move" } : undefined;
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
return { error: error.message };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** The refusal of an error thrown by a command, with its code when the error or the caller knows it. */
|
|
351
|
+
function refusalFrom(error, refusal) {
|
|
352
|
+
const message = error.message;
|
|
353
|
+
if (error instanceof ChatNotClear) {
|
|
354
|
+
return { error: message, refusal: "clear-chat" };
|
|
355
|
+
}
|
|
356
|
+
if (error instanceof WordsRefused) {
|
|
357
|
+
return { error: message, refusal: `words-${error.reason}`, most: error.most };
|
|
358
|
+
}
|
|
359
|
+
if (error instanceof DrawRefused) {
|
|
360
|
+
return { error: message, refusal: `draw-${error.reason}` };
|
|
361
|
+
}
|
|
362
|
+
return refusal === undefined ? { error: message } : { error: message, refusal };
|
|
363
|
+
}
|
|
364
|
+
/** The game as the page reads it now: a flag that has fallen ends it (D-13), and the doorbell tells the chat, which hears how from status. */
|
|
365
|
+
function gameNow(served) {
|
|
366
|
+
const folder = gameFolder(served.context.home());
|
|
367
|
+
const before = loadGame(folder);
|
|
368
|
+
// The AI's clock is held while nobody hears the player: its flag cannot fall before the chat is there to move.
|
|
369
|
+
if (before !== undefined && aiHeld(served, before)) {
|
|
370
|
+
return before;
|
|
371
|
+
}
|
|
372
|
+
const current = gameAt(folder, served.context.now());
|
|
373
|
+
if (before !== undefined && endOf(before.game) === undefined && current !== undefined && endOf(current.game) !== undefined) {
|
|
374
|
+
served.bell.ring();
|
|
375
|
+
}
|
|
376
|
+
return current;
|
|
377
|
+
}
|
|
378
|
+
/** Whether the AI's clock is held: a game with a clock, going on, the AI to move, and nobody hearing the player. */
|
|
379
|
+
function aiHeld(served, current) {
|
|
380
|
+
return !served.bell.listening && current.clock !== undefined && endOf(current.game) === undefined && current.game.position.turn === current.ai;
|
|
381
|
+
}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { DrawRefused } from "../../commands/index.ts";
|
|
3
|
+
import type { WordsRefused } from "../../games/index.ts";
|
|
2
4
|
export declare const json = "application/json; charset=utf-8";
|
|
5
|
+
/**
|
|
6
|
+
* A request refused, as the page reads it: the server's words, and the code of a refusal the page says in the player's language, with the characters
|
|
7
|
+
* left for words too long; without a code, the page shows the words.
|
|
8
|
+
*/
|
|
9
|
+
export interface Refused {
|
|
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";
|
|
12
|
+
readonly most?: number;
|
|
13
|
+
}
|
|
3
14
|
/** Answers a request, with nothing cached, sniffed or referred, and the page's policy unless the headers give another. */
|
|
4
15
|
export declare function send(response: ServerResponse, status: number, type: string, body: string | Buffer, headers?: Record<string, string>): void;
|
|
5
16
|
/** Answers with JSON. */
|
|
6
17
|
export declare function sendJson(response: ServerResponse, status: number, body: unknown): void;
|
|
18
|
+
/** Runs the answer to a request: an error it did not expect, such as a file that cannot be written, is told to the page, and the server goes on. */
|
|
19
|
+
export declare function runRoute(response: ServerResponse, route: () => void | Promise<void>): void;
|
|
7
20
|
/** Whether a request sends JSON: a form of another site can post text, never JSON without asking first. */
|
|
8
21
|
export declare function isJson(request: IncomingMessage): boolean;
|
|
9
22
|
/** An object from JSON; undefined for anything else. */
|
|
@@ -15,6 +15,17 @@ export function send(response, status, type, body, headers = {}) {
|
|
|
15
15
|
export function sendJson(response, status, body) {
|
|
16
16
|
send(response, status, json, JSON.stringify(body));
|
|
17
17
|
}
|
|
18
|
+
/** Runs the answer to a request: an error it did not expect, such as a file that cannot be written, is told to the page, and the server goes on. */
|
|
19
|
+
export function runRoute(response, route) {
|
|
20
|
+
Promise.resolve().then(route).catch((error) => {
|
|
21
|
+
if (response.headersSent) {
|
|
22
|
+
response.end();
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
18
29
|
/** Whether a request sends JSON: a form of another site can post text, never JSON without asking first. */
|
|
19
30
|
export function isJson(request) {
|
|
20
31
|
return (request.headers["content-type"] ?? "").startsWith("application/json");
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { type ServerResponse } from "node:http";
|
|
2
|
-
import type
|
|
3
|
-
import {
|
|
4
|
-
import { WordsRefused } from "../games/index.ts";
|
|
5
|
-
import { Bell } from "./bell.ts";
|
|
2
|
+
import { type Context, type SeenPosition } from "../../commands/index.ts";
|
|
3
|
+
import { Bell } from "../bell.ts";
|
|
6
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. */
|
|
7
5
|
export interface Page {
|
|
8
6
|
readonly url: string;
|
|
9
7
|
heard(): void;
|
|
10
8
|
/** The chat calls a tool: the page hears it listens, and the AI's clock, held while nobody heard the player, starts now. */
|
|
11
9
|
chatCalls(): void;
|
|
10
|
+
/** The AI starts thinking on the player's time, on a position, or is done (D-72): the page shows it. */
|
|
11
|
+
ponders(position: SeenPosition | undefined): void;
|
|
12
12
|
/** Whether a browser has the page open now: one listens to the game's changes. */
|
|
13
13
|
open(): boolean;
|
|
14
14
|
/** Opens the New game window on every page open now, for the player to choose the game there: whether one was open. */
|
|
@@ -29,15 +29,10 @@ export interface Served {
|
|
|
29
29
|
readonly bell: Bell;
|
|
30
30
|
readonly watchers: Set<ServerResponse>;
|
|
31
31
|
port: number;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
export interface Refused {
|
|
38
|
-
readonly error: string;
|
|
39
|
-
readonly refusal?: "no-game" | "ai-to-move" | "illegal" | "clear-chat" | `words-${WordsRefused["reason"]}` | `draw-${DrawRefused["reason"]}` | "profile-game" | "game-going" | "pgn-unreadable";
|
|
40
|
-
readonly most?: number;
|
|
32
|
+
/** 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. */
|
|
33
|
+
readonly outdated: boolean;
|
|
34
|
+
/** The position the AI thinks on during the player's time (D-72), until it is done; none else. */
|
|
35
|
+
pondering?: SeenPosition | undefined;
|
|
41
36
|
}
|
|
42
37
|
/**
|
|
43
38
|
* 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
|