chesspal 0.5.0 → 0.7.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 +3 -3
- package/dist/commands/analysis/analyse.js +7 -2
- package/dist/commands/analysis/annotate.js +6 -2
- package/dist/commands/answers/agents.d.ts +2 -0
- package/dist/commands/answers/agents.js +5 -0
- package/dist/commands/answers/index.d.ts +2 -2
- package/dist/commands/answers/index.js +2 -2
- package/dist/commands/answers/lines.js +3 -1
- package/dist/commands/answers/say.d.ts +7 -1
- package/dist/commands/answers/say.js +25 -7
- package/dist/commands/answers/study.d.ts +27 -5
- package/dist/commands/answers/study.js +121 -10
- package/dist/commands/game/look.d.ts +1 -0
- package/dist/commands/game/look.js +8 -8
- package/dist/commands/game/position.d.ts +1 -0
- package/dist/commands/game/position.js +5 -5
- package/dist/commands/index.d.ts +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/moods.d.ts +2 -0
- package/dist/core/moods.js +11 -2
- package/dist/games/analysis.d.ts +11 -0
- package/dist/games/analysis.js +23 -0
- package/dist/games/archive.d.ts +9 -1
- package/dist/games/archive.js +18 -1
- package/dist/games/current-game.d.ts +23 -1
- package/dist/games/current-game.js +6 -1
- package/dist/games/index.d.ts +3 -3
- package/dist/games/index.js +2 -2
- package/dist/page/app/app.js +148 -29
- package/dist/page/app/keys.js +16 -1
- package/dist/page/app/title.js +6 -3
- package/dist/page/board/board.css +6 -0
- package/dist/page/game/column.js +5 -4
- package/dist/page/game/game.css +150 -1
- package/dist/page/game/move-list.js +48 -10
- package/dist/page/game/variation.js +65 -0
- package/dist/page/guide/en.md +1 -0
- package/dist/page/guide/it.md +1 -0
- package/dist/page/table/window.js +8 -5
- package/dist/page/talk/chat.css +8 -0
- package/dist/page/talk/talk.js +20 -2
- package/dist/page/text.js +20 -0
- package/dist/server/bell/bell.d.ts +5 -2
- package/dist/server/bell/bell.js +36 -12
- package/dist/server/page/game.js +38 -7
- package/dist/server/page/page.js +1 -1
- package/dist/server/page/table.d.ts +12 -2
- package/dist/server/page/table.js +18 -6
- package/dist/server/table/hosts/chats.js +1 -1
- package/dist/server/table/hosts/play.d.ts +3 -1
- package/dist/server/table/hosts/play.js +6 -5
- package/dist/server/table/hosts/seat.d.ts +5 -1
- package/dist/server/table/hosts/seat.js +1 -1
- package/instructions/analysis.md +2 -2
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
package/dist/server/bell/bell.js
CHANGED
|
@@ -23,6 +23,8 @@ export class Bell {
|
|
|
23
23
|
woken = false;
|
|
24
24
|
// A hook has waited for the bell since the chat came: its host runs Chesspal's hooks, and only they tell where a turn ends.
|
|
25
25
|
hooked = false;
|
|
26
|
+
// The chat's last hook went without the bell, or a ring found it stopped in its turn: it is out of its turn, and nobody hears the player.
|
|
27
|
+
unheard = false;
|
|
26
28
|
// Looks again, while a ring waits for a chat in a turn, whether the chat was stopped meanwhile.
|
|
27
29
|
stopWatch;
|
|
28
30
|
news;
|
|
@@ -46,6 +48,10 @@ export class Bell {
|
|
|
46
48
|
get woke() {
|
|
47
49
|
return this.woken;
|
|
48
50
|
}
|
|
51
|
+
/** Whether the chat is out of its turn and hears nobody: its hook went without the bell, or it was stopped; until it calls a tool or a hook waits again. */
|
|
52
|
+
get deaf() {
|
|
53
|
+
return this.unheard;
|
|
54
|
+
}
|
|
49
55
|
/** Whether the chat hears the player: in a turn, or waiting for the bell with a hook. */
|
|
50
56
|
get listening() {
|
|
51
57
|
return this.busy || this.listener !== undefined;
|
|
@@ -54,6 +60,7 @@ export class Bell {
|
|
|
54
60
|
chatCalls() {
|
|
55
61
|
const asleep = !this.listening;
|
|
56
62
|
this.busy = true;
|
|
63
|
+
this.unheard = false;
|
|
57
64
|
this.tellIf(asleep);
|
|
58
65
|
return asleep;
|
|
59
66
|
}
|
|
@@ -63,7 +70,11 @@ export class Bell {
|
|
|
63
70
|
* stays unheard until its first tool, which starts the AI's clock, held till then: the message moves no clock.
|
|
64
71
|
*/
|
|
65
72
|
chatPrompted() {
|
|
73
|
+
// A chat nobody heard is in its turn now, though still unheard.
|
|
66
74
|
if (this.listener === undefined) {
|
|
75
|
+
const deaf = this.unheard;
|
|
76
|
+
this.unheard = false;
|
|
77
|
+
this.tellIf(deaf);
|
|
67
78
|
return;
|
|
68
79
|
}
|
|
69
80
|
this.listener.end("dismissed");
|
|
@@ -101,6 +112,7 @@ export class Bell {
|
|
|
101
112
|
listen(request, response, told = false) {
|
|
102
113
|
response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
103
114
|
this.hooked = true;
|
|
115
|
+
this.unheard = false;
|
|
104
116
|
const missed = this.pending && !told;
|
|
105
117
|
this.pending = false;
|
|
106
118
|
if (missed && this.news()) {
|
|
@@ -117,9 +129,11 @@ export class Bell {
|
|
|
117
129
|
this.changed();
|
|
118
130
|
request.on("close", () => {
|
|
119
131
|
if (this.listener === response) {
|
|
120
|
-
// The hook went without the bell (the chat stopped, cleared or closed): nobody hears the player until the chat calls
|
|
132
|
+
// The hook went without the bell (the chat stopped, cleared or closed, or its host ended the hook): nobody hears the player until the chat calls
|
|
133
|
+
// a tool again, but the chat is out of its turn.
|
|
121
134
|
this.listener = undefined;
|
|
122
|
-
this.
|
|
135
|
+
this.unheard = !this.busy;
|
|
136
|
+
this.tellIf(this.unheard);
|
|
123
137
|
}
|
|
124
138
|
});
|
|
125
139
|
}
|
|
@@ -128,6 +142,7 @@ export class Bell {
|
|
|
128
142
|
this.hooked = false;
|
|
129
143
|
this.busy = false;
|
|
130
144
|
this.woken = false;
|
|
145
|
+
this.unheard = false;
|
|
131
146
|
this.pending = false;
|
|
132
147
|
this.listener?.end("dismissed");
|
|
133
148
|
this.listener = undefined;
|
|
@@ -146,6 +161,7 @@ export class Bell {
|
|
|
146
161
|
if (this.stopped()) {
|
|
147
162
|
this.busy = false;
|
|
148
163
|
this.woken = false;
|
|
164
|
+
this.unheard = true;
|
|
149
165
|
this.changed();
|
|
150
166
|
return;
|
|
151
167
|
}
|
|
@@ -199,15 +215,23 @@ export async function listen(host = "claude") {
|
|
|
199
215
|
for await (const chunk of process.stdin) {
|
|
200
216
|
input += String(chunk);
|
|
201
217
|
}
|
|
218
|
+
// Antigravity's hook leaves a line as it starts: a chat its bell never woke shows whether the hook ran at all.
|
|
219
|
+
if (host === "antigravity") {
|
|
220
|
+
noteHook(chatFolder(hookInput(input)), "waiting");
|
|
221
|
+
}
|
|
202
222
|
const conversation = host === "antigravity" ? hookInput(input).session : undefined;
|
|
203
223
|
const turn = conversation === undefined ? undefined : watchNewTurn(conversation);
|
|
204
224
|
const waited = bellHeard(input).catch((error) => ({ folder: chatFolder(hookInput(input)), rang: false, why: `failed: ${error.message}` }));
|
|
205
225
|
const newTurn = turn?.started.then(() => ({ folder: chatFolder(hookInput(input)), rang: false, why: "the player started a new turn" }));
|
|
206
226
|
const heard = await (newTurn === undefined ? waited : Promise.race([waited, newTurn]));
|
|
207
227
|
turn?.stop();
|
|
208
|
-
// Codex's turn ends with a hook that heard no bell, and the player's next move on the page wakes nobody: the log keeps why.
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
// Codex's turn ends with a hook that heard no bell, and the player's next move on the page wakes nobody: the log keeps why. Antigravity's keeps every
|
|
229
|
+
// end, the bell too, since its chat may not wake even then.
|
|
230
|
+
if (host === "antigravity") {
|
|
231
|
+
noteHook(heard.folder, heard.rang ? "rang" : heard.why);
|
|
232
|
+
}
|
|
233
|
+
else if (host === "codex" && !heard.rang) {
|
|
234
|
+
noteHook(heard.folder, heard.why);
|
|
211
235
|
}
|
|
212
236
|
// It ends as soon as it knows: the chat hears the hook only once it ends.
|
|
213
237
|
const answer = hookAnswer(host, heard.rang);
|
|
@@ -271,15 +295,15 @@ async function forgetGonePages(home, folder, kept, wait) {
|
|
|
271
295
|
}
|
|
272
296
|
}));
|
|
273
297
|
}
|
|
274
|
-
// The log of
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
/** Adds a line to the log of the hooks
|
|
278
|
-
export function
|
|
279
|
-
const path = join(folder,
|
|
298
|
+
// The log of the doorbell's hooks, in the chat's folder: Codex's that heard no bell, Antigravity's as they start and end; its last lines only.
|
|
299
|
+
const hookLog = "listen.log";
|
|
300
|
+
const hookLines = 100;
|
|
301
|
+
/** Adds a line to the log of the doorbell's hooks, keeping its last lines; a log that cannot be written never stops the hook. */
|
|
302
|
+
export function noteHook(folder, why, now = new Date()) {
|
|
303
|
+
const path = join(folder, hookLog);
|
|
280
304
|
try {
|
|
281
305
|
const before = existsSync(path) ? readFileSync(path, "utf8").split("\n").filter((line) => line !== "") : [];
|
|
282
|
-
writeAtomically(path, `${[...before, `${now.toISOString()} ${why}`].slice(-
|
|
306
|
+
writeAtomically(path, `${[...before, `${now.toISOString()} ${why}`].slice(-hookLines).join("\n")}\n`);
|
|
283
307
|
}
|
|
284
308
|
catch {
|
|
285
309
|
// The log only explains; the hook ends the same.
|
package/dist/server/page/game.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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, GameMissing, 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, stopAnalysis, 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 { analystAtWork, chatPlayedAtLength, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
|
|
10
|
+
import { analystAtWork, chatForAnalysis, chatPlayedAtLength, 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) {
|
|
@@ -55,6 +55,7 @@ const routes = {
|
|
|
55
55
|
"POST /api/resign": resignFromPage,
|
|
56
56
|
"POST /api/start": startFromPage,
|
|
57
57
|
"POST /api/study": studyFromPage,
|
|
58
|
+
"POST /api/stop-analysis": stopAnalysisFromPage,
|
|
58
59
|
};
|
|
59
60
|
/**
|
|
60
61
|
* 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
|
|
@@ -98,7 +99,7 @@ async function moveFromPage(request, response, served) {
|
|
|
98
99
|
}
|
|
99
100
|
/**
|
|
100
101
|
* 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
|
|
101
|
-
* first; in analysis with the position the page shows, and the doorbell rung for each (D-101).
|
|
102
|
+
* first; in analysis with the position the page shows and the variation played there (D-116), and the doorbell rung for each (D-101).
|
|
102
103
|
*/
|
|
103
104
|
async function messageFromPage(request, response, served) {
|
|
104
105
|
if (!isJson(request)) {
|
|
@@ -107,11 +108,19 @@ async function messageFromPage(request, response, served) {
|
|
|
107
108
|
}
|
|
108
109
|
const body = jsonOf(await bodyOf(request));
|
|
109
110
|
const text = textOf(body?.text);
|
|
110
|
-
const
|
|
111
|
-
if (text === undefined) {
|
|
111
|
+
const variation = variationOf(body?.variation);
|
|
112
|
+
if (text === undefined || variation === null) {
|
|
112
113
|
send(response, 400, json, JSON.stringify({ error: `Send the message as JSON, ${talkLimits.player} characters at most: {"text": "…"}.` }));
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
116
|
+
let shown;
|
|
117
|
+
try {
|
|
118
|
+
shown = typeof body?.game === "string" && typeof body.at === "number" ? shownPosition(served.context, body.game, body.at, variation) : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
send(response, 400, json, JSON.stringify({ error: error.message }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
115
124
|
let current;
|
|
116
125
|
try {
|
|
117
126
|
current = rerunIfChanged(() => say(gameFolder(served.context.home()), "player", text, served.context.now(), undefined, shown));
|
|
@@ -132,7 +141,8 @@ async function messageFromPage(request, response, served) {
|
|
|
132
141
|
/**
|
|
133
142
|
* 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
|
|
134
143
|
* 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
|
|
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
|
|
144
|
+
* it again. A chat the table holds that must be clear for it, or that it started for the analysis, and is at work, has the game wait with the choices kept
|
|
145
|
+
* until it has ended its answer (D-107).
|
|
136
146
|
*/
|
|
137
147
|
async function newGameFromPage(request, response, served) {
|
|
138
148
|
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.';
|
|
@@ -152,7 +162,7 @@ async function newGameFromPage(request, response, served) {
|
|
|
152
162
|
}
|
|
153
163
|
// 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.
|
|
154
164
|
relaunching = chatToRelaunch(served);
|
|
155
|
-
if (!relaunching && chatToWaitFor(served) && chatPlayedAtLength(served, before)) {
|
|
165
|
+
if (!relaunching && chatToWaitFor(served) && (chatPlayedAtLength(served, before) || chatForAnalysis(served))) {
|
|
156
166
|
waitForChat(served, input.data);
|
|
157
167
|
send(response, 200, json, JSON.stringify({ waiting: true }));
|
|
158
168
|
return;
|
|
@@ -317,6 +327,18 @@ async function studyFromPage(request, response, served) {
|
|
|
317
327
|
}
|
|
318
328
|
send(response, 200, json, JSON.stringify({ kept: true, reviewed }));
|
|
319
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* The player stops the analysis they asked for, while the AI is at it or meant to be: the page goes back to the game over, with its review as far as it got,
|
|
332
|
+
* and the chat is no longer asked to go over it; nothing rings, as there is nothing for the chat to do. Only the game in analysis is stopped.
|
|
333
|
+
*/
|
|
334
|
+
async function stopAnalysisFromPage(request, response, served) {
|
|
335
|
+
const game = isJson(request) ? textOf(jsonOf(await bodyOf(request))?.game) : undefined;
|
|
336
|
+
if (game === undefined) {
|
|
337
|
+
send(response, 400, json, JSON.stringify({ error: 'Send the game in analysis as JSON: {"game": "games/2026-11-02-153012.pgn"}.' }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
send(response, 200, json, JSON.stringify({ stopped: stopAnalysis(gameFolder(served.context.home()), game) }));
|
|
341
|
+
}
|
|
320
342
|
/**
|
|
321
343
|
* 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
|
|
322
344
|
* 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
|
|
@@ -513,3 +535,12 @@ function gameNow(served) {
|
|
|
513
535
|
function aiHeld(served, current) {
|
|
514
536
|
return !served.bell.listening && current.clock !== undefined && endOf(current.game) === undefined && current.game.position.turn === current.ai;
|
|
515
537
|
}
|
|
538
|
+
/** The variation sent with the player's words (D-116): none when there is none, null when it is not one. */
|
|
539
|
+
function variationOf(value) {
|
|
540
|
+
if (value === undefined) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
const { at, moves, cursor } = (typeof value === "object" && value !== null ? value : {});
|
|
544
|
+
const sans = Array.isArray(moves) && moves.every((move) => typeof move === "string") ? moves : undefined;
|
|
545
|
+
return typeof at === "number" && typeof cursor === "number" && sans !== undefined ? { at, moves: sans, cursor } : null;
|
|
546
|
+
}
|
package/dist/server/page/page.js
CHANGED
|
@@ -142,7 +142,7 @@ function answer(request, response, served) {
|
|
|
142
142
|
// The API answers GET alone: a HEAD at the doorbell would wait there as a hook does.
|
|
143
143
|
if (request.method !== "GET" && (request.method !== "HEAD" || path.startsWith("/api/"))) {
|
|
144
144
|
send(response, 405, "text/plain; charset=utf-8", "The page only reads, moves with POST /api/move, writes with POST /api/message, starts a game with POST /api/new and its clocks with POST /api/start, "
|
|
145
|
-
+ "offers or answers a draw with POST /api/draw, resigns with POST /api/resign, brings a game to analyse or opens one again with POST /api/study, deletes a game or its review with POST /api/delete-game and /api/delete-review, and keeps the profiles with POST /api/profile/… and /api/settings.");
|
|
145
|
+
+ "offers or answers a draw with POST /api/draw, resigns with POST /api/resign, brings a game to analyse or opens one again with POST /api/study, stops the analysis asked with POST /api/stop-analysis, deletes a game or its review with POST /api/delete-game and /api/delete-review, and keeps the profiles with POST /api/profile/… and /api/settings.");
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
if (path === "/") {
|
|
@@ -22,11 +22,15 @@ export interface TableChats {
|
|
|
22
22
|
/** Tells `changed` when the hosts' choices change: Antigravity's models, read from the player's account. */
|
|
23
23
|
watch?(changed: () => void): void;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The chat `chesspal play` holds: the model and effort its host started with, none for the host's own, the seat, the play's process, and `analysis` when
|
|
27
|
+
* it started it for the game in analysis on the page.
|
|
28
|
+
*/
|
|
26
29
|
export interface HeldChat {
|
|
27
30
|
readonly model?: string | undefined;
|
|
28
31
|
readonly effort?: string | undefined;
|
|
29
32
|
readonly seat?: number;
|
|
33
|
+
readonly analysis?: true;
|
|
30
34
|
}
|
|
31
35
|
/** A chat opening at the table: one the page asked for, the one started again for a new game, or with another model or host (D-107). */
|
|
32
36
|
export type Opening = "chat" | "again" | "restart";
|
|
@@ -69,7 +73,8 @@ export declare function tableShown(served: Served): {
|
|
|
69
73
|
export declare function answerTable(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
|
|
70
74
|
/**
|
|
71
75
|
* Whether a new game starts the chat at the table again, clean (D-57): `chesspal play` holds it, it waits for the bell, no analyst of it is at work, and it
|
|
72
|
-
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game.
|
|
76
|
+
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game. One
|
|
77
|
+
* `chesspal play` started for the analysis starts again all the same, at rest or still opening, or its first turn would analyse a game no longer there.
|
|
73
78
|
*/
|
|
74
79
|
export declare function chatToRelaunch(served: Served): boolean;
|
|
75
80
|
/**
|
|
@@ -82,6 +87,11 @@ export declare function relaunchChat(served: Served, how?: "again" | "restart",
|
|
|
82
87
|
* it, it is at work, and no analyst of it is; with an analyst at work the player is asked for /clear, as with a chat opened by hand.
|
|
83
88
|
*/
|
|
84
89
|
export declare function chatToWaitFor(served: Served): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Whether the chat `chesspal play` holds was started for the analysis on the page: the one at the table, or, with none at it yet, the one still opening.
|
|
92
|
+
* A chat opened by hand never is.
|
|
93
|
+
*/
|
|
94
|
+
export declare function chatForAnalysis(served: Served): boolean;
|
|
85
95
|
/**
|
|
86
96
|
* Whether the chat at the table has played the game `current` at length (D-57), counted from the first position it was shown of it. A chat the table
|
|
87
97
|
* started again for a game began after it, and new_game takes it for one cleared since: it holds the whole game all the same.
|
|
@@ -62,22 +62,25 @@ export function answerTable(request, response, url, served) {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Whether a new game starts the chat at the table again, clean (D-57): `chesspal play` holds it, it waits for the bell, no analyst of it is at work, and it
|
|
65
|
-
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game.
|
|
65
|
+
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game. One
|
|
66
|
+
* `chesspal play` started for the analysis starts again all the same, at rest or still opening, or its first turn would analyse a game no longer there.
|
|
66
67
|
*/
|
|
67
68
|
export function chatToRelaunch(served) {
|
|
68
69
|
const { positionSeen, positionFirstSeen } = served.chat;
|
|
69
70
|
const played = positionSeen?.started !== undefined && (positionFirstSeen === undefined || !samePosition(positionSeen, positionFirstSeen));
|
|
70
|
-
return played && heldChat(served) !== undefined && chatAtRest(served);
|
|
71
|
+
return chatForAnalysis(served) ? served.attached === undefined || chatAtRest(served) : played && heldChat(served) !== undefined && chatAtRest(served);
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* Asks `chesspal play` to start the chat at the table again: once the new game has started, or with another model or host in its place (D-107). Whether
|
|
74
75
|
* it was asked; the pages then show it opening.
|
|
75
76
|
*/
|
|
76
77
|
export function relaunchChat(served, how = "again", host) {
|
|
77
|
-
|
|
78
|
+
// One started for the analysis is asked again while it is still opening, in the table's folder.
|
|
79
|
+
const opening = served.attached === undefined && how === "again" && chatForAnalysis(served);
|
|
80
|
+
const folder = opening ? served.context.folder : served.attached?.chat.folder;
|
|
78
81
|
// Another host may first ask whether it trusts the table's folder: the pages say so, as for a chat opened.
|
|
79
82
|
const switching = how === "restart" && host !== undefined && host !== hostAtTable(served);
|
|
80
|
-
if (folder === undefined || heldChat(served) === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
|
|
83
|
+
if (folder === undefined || (!opening && heldChat(served) === undefined) || served.table?.chats?.relaunch(folder, host) !== true) {
|
|
81
84
|
return false;
|
|
82
85
|
}
|
|
83
86
|
noteOpening(served, switching ? "chat" : how);
|
|
@@ -90,6 +93,15 @@ export function relaunchChat(served, how = "again", host) {
|
|
|
90
93
|
export function chatToWaitFor(served) {
|
|
91
94
|
return heldChat(served) !== undefined && !chatAtRest(served) && !analystAtWork(served);
|
|
92
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether the chat `chesspal play` holds was started for the analysis on the page: the one at the table, or, with none at it yet, the one still opening.
|
|
98
|
+
* A chat opened by hand never is.
|
|
99
|
+
*/
|
|
100
|
+
export function chatForAnalysis(served) {
|
|
101
|
+
const { attached, context, table } = served;
|
|
102
|
+
const held = table?.chats?.held(attached?.chat.folder ?? context.folder);
|
|
103
|
+
return held?.analysis === true && (attached === undefined || held.seat === attached.chat.seat);
|
|
104
|
+
}
|
|
93
105
|
/**
|
|
94
106
|
* Whether the chat at the table has played the game `current` at length (D-57), counted from the first position it was shown of it. A chat the table
|
|
95
107
|
* started again for a game began after it, and new_game takes it for one cleared since: it holds the whole game all the same.
|
|
@@ -299,10 +311,10 @@ function heldChat(served) {
|
|
|
299
311
|
const held = attached === undefined ? undefined : served.table?.chats?.held(attached.chat.folder);
|
|
300
312
|
return held === undefined || held.seat !== attached?.chat.seat ? undefined : { model: held.model, effort: held.effort };
|
|
301
313
|
}
|
|
302
|
-
// The chat
|
|
314
|
+
// The chat is out of a turn, waiting for the doorbell or deaf to it, and no analyst of it is at work: starting it again loses nothing.
|
|
303
315
|
function chatAtRest(served) {
|
|
304
316
|
const { bell } = served;
|
|
305
|
-
return bell.listening && !bell.inTurn && !analystAtWork(served);
|
|
317
|
+
return (bell.listening || bell.deaf) && !bell.inTurn && !analystAtWork(served);
|
|
306
318
|
}
|
|
307
319
|
/**
|
|
308
320
|
* Whether the chat's analyst is at work, on `game` when named: one whose chat started again since it took its game, or that has written nothing for half an
|
|
@@ -34,7 +34,7 @@ export function terminalChats(context, chesspal = [process.execPath, ...process.
|
|
|
34
34
|
// Only a chat in the table's folder can be the one chesspal play holds.
|
|
35
35
|
held: (folder) => {
|
|
36
36
|
const seat = samePath(folder, tableFolder(context.home())) ? seatHeld(folder, context.now()) : undefined;
|
|
37
|
-
return seat === undefined ? undefined : { model: seat.model, effort: seat.effort, seat: seat.pid };
|
|
37
|
+
return seat === undefined ? undefined : { model: seat.model, effort: seat.effort, seat: seat.pid, ...(seat.analysis === true ? { analysis: true } : {}) };
|
|
38
38
|
},
|
|
39
39
|
relaunch: (folder, host, updated) => samePath(folder, tableFolder(context.home())) && askAgain(folder, context.now(), { ...(host === undefined ? {} : { host }), ...(updated === true ? { updated } : {}) }),
|
|
40
40
|
};
|
|
@@ -4,7 +4,7 @@ import { type HooksNews } from "./hooks.ts";
|
|
|
4
4
|
import { type HostChoice, type HostName } from "./hosts.ts";
|
|
5
5
|
/**
|
|
6
6
|
* What `chesspal play` starts: the host's program, its arguments, folder and environment, whether it needs cmd, as a .cmd shim does on Windows, and the
|
|
7
|
-
* model and effort it is given, which the seat notes for the page (D-107).
|
|
7
|
+
* model and effort it is given, which the seat notes for the page (D-107), as it notes a chat started for the game in analysis.
|
|
8
8
|
*/
|
|
9
9
|
export interface HostLaunch {
|
|
10
10
|
readonly program: string;
|
|
@@ -13,6 +13,8 @@ export interface HostLaunch {
|
|
|
13
13
|
readonly env: NodeJS.ProcessEnv;
|
|
14
14
|
readonly shell: boolean;
|
|
15
15
|
readonly choice: HostChoice;
|
|
16
|
+
/** Started for the game in analysis on the page, with no game going on: a new game from the page starts it again. */
|
|
17
|
+
readonly analysis: boolean;
|
|
16
18
|
/** Chesspal wrote Codex's hooks, the first time or again: Codex runs them only once the player trusts them. */
|
|
17
19
|
readonly hooks?: HooksNews;
|
|
18
20
|
}
|
|
@@ -146,16 +146,17 @@ export function hostLaunch(context, host, env = process.env, exists) {
|
|
|
146
146
|
const choices = tableChoices(folder, host);
|
|
147
147
|
const choice = choiceIn(profileSettings(home, activeProfile(home).id), host, choices);
|
|
148
148
|
const games = gameFolder(home);
|
|
149
|
-
const
|
|
149
|
+
const analysis = analysisAsked(games);
|
|
150
|
+
const args = hostArgs(host, startMessage(languageIn(games, context.language), analysis), choice, folder, choices);
|
|
150
151
|
// An effort the player set for every chat would win over the one chosen on the page: spawn leaves out a variable that is undefined.
|
|
151
152
|
const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
|
|
152
153
|
// The seat this play holds, by which its chat's server tells the table it is the chat held, and not another opened in the same folder.
|
|
153
154
|
const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort, CHESSPAL_SEAT: String(process.pid) };
|
|
154
155
|
const script = npmScript(host, found, context.platform, exists ?? existsSync);
|
|
155
156
|
if (script !== undefined) {
|
|
156
|
-
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, ...hooks };
|
|
157
|
+
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, analysis, ...hooks };
|
|
157
158
|
}
|
|
158
|
-
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, ...hooks };
|
|
159
|
+
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, analysis, ...hooks };
|
|
159
160
|
}
|
|
160
161
|
/** Whether the board page shows a game in analysis, none going on: the chat opened there is for that game, brought to review or to talk over, not for a new one. */
|
|
161
162
|
function analysisAsked(folder) {
|
|
@@ -185,10 +186,10 @@ const closedAtStartWords = {
|
|
|
185
186
|
english: ["Claude Code closed as soon as it opened.", 'If it asks whether you trust this folder, go down to "Yes, I trust this folder" with the arrow keys, then press Enter.'],
|
|
186
187
|
italian: ["Claude Code si è chiuso appena aperto.", "Se ti chiede se ti fidi della cartella, scendi con le frecce su «Yes, I trust this folder», poi premi Invio."],
|
|
187
188
|
};
|
|
188
|
-
// Runs the host as runHost does, the seat noting the model and effort it is given; one not installed, or that could not start, fails saying so.
|
|
189
|
+
// Runs the host as runHost does, the seat noting the model and effort it is given, and whether for the analysis; one not installed, or that could not start, fails saying so.
|
|
189
190
|
async function started(context, host, again) {
|
|
190
191
|
const launch = hostLaunch(context, host);
|
|
191
|
-
takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice });
|
|
192
|
+
takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice, ...(launch.analysis ? { analysis: true } : {}) });
|
|
192
193
|
// Codex asks the player to trust the hooks Chesspal wrote: said here, above its question, as init says it.
|
|
193
194
|
if (launch.hooks !== undefined) {
|
|
194
195
|
const words = hooksWords[launch.hooks];
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The play process in the seat: its process id, its host, and the model and effort it started that host with, none for the host's own; `analysis` when
|
|
3
|
+
* it started it for the game in analysis on the page, which a new game then starts again.
|
|
4
|
+
*/
|
|
2
5
|
export interface Seat {
|
|
3
6
|
readonly pid: number;
|
|
4
7
|
readonly host: string;
|
|
5
8
|
readonly model?: string | undefined;
|
|
6
9
|
readonly effort?: string | undefined;
|
|
10
|
+
readonly analysis?: true;
|
|
7
11
|
}
|
|
8
12
|
/**
|
|
9
13
|
* Takes the seat for this play process, over a request left for one before it; it throws while another one holds it. A seat is held while its play
|
|
@@ -70,7 +70,7 @@ function seatIn(folder) {
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
73
|
-
return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort) };
|
|
73
|
+
return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort), ...(seat.analysis === true ? { analysis: true } : {}) };
|
|
74
74
|
}
|
|
75
75
|
function jsonIn(path) {
|
|
76
76
|
try {
|
package/instructions/analysis.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
- Each line of the analysis is a move and, when they happened: the piece it took; the time it took ("spent") and the time left; who is ahead in material, when that changed; "at risk" after the move, the pieces of either side attacked and undefended, attacked by a cheaper piece, or attacked more times than defended ("outnumbered"), whichever move left them so, written when it changed: a line without it leaves the same pieces at risk as before, "none" when no piece is left at risk; "one legal move", the only move the side to move then had; "mate in one", the mates the side to move then has; "out of book", the first move after which no line of Lichess's openings file reaches the position, in any order of moves: the file names openings, it is not all theory; a draw offered; "ahead", who was estimated ahead after it, with `annotate_game` or by the coach during the game.
|
|
2
2
|
- In a game you played, "you said" and "your plan" are what you said and thought then, right or wrong: where they disagree with the facts, the facts are what was on the board.
|
|
3
|
-
- To see a position closer, `look` with the move, and the game as the analysis names it, gives every piece's attackers and defenders, the pins, the checks and the legal moves; `position` with the same gives the positional facts. Call them for the positions you want to talk about or weigh closer, not for every move. In look, attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now.
|
|
4
|
-
- Never invent a move, a position or a result. The tools give only the positions the game reached: a move
|
|
3
|
+
- To see a position closer, `look` with the move, and the game as the analysis names it, gives every piece's attackers and defenders, the pins, the checks and the legal moves; `position` with the same gives the positional facts. With `line`, a variation from there (`12...Kf8 13.Nxe5`), both give the same of the position after it, or the first of its moves that cannot be played. Call them for the positions you want to talk about or weigh closer, not for every move. In look, attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now.
|
|
4
|
+
- Never invent a move, a position or a result. The tools give only the positions the game reached, and those after a `line` you give them: a move the game did not play is your idea or the player's, say it as such; `look` on its position shows whether it was legal, not what the other side would answer.
|
package/instructions/opponent.md
CHANGED
|
@@ -10,6 +10,6 @@ You choose your moves, explain them briefly and talk with the player.
|
|
|
10
10
|
- You have a face on the board page, and it shows how you feel. Give it as the `mood` of `ai_move` on each move where the game turns for you: a piece won or lost, a blunder seen, your own or theirs, a threat that appears or a plan that works or fails. Let it colour your words while it lasts. When the game ends on the player's move, give how you feel as it ends with `ai_say`'s `mood`.
|
|
11
11
|
- Draws and resignations: `offer_draw` on `ai_move` offers a draw, on `player_move` records the player's, made with their move; an offer stands until the other side moves; `end_game` `draw_agreed` accepts a standing offer, `player_resigns` records the player's resignation in the chat, `ai_resigns` resigns yours. On the board page the player offers, answers and resigns there.
|
|
12
12
|
- When they ask for a game, call `new_game` without choices: the board page's New game window asks the sides, the clock and the level; ask them in the chat only when `new_game` says there is no board page.
|
|
13
|
-
- To go over a game with the player once no game goes on, the one just over or one they bring as a PGN, or one of their latest on Lichess or Chess.com, which `site_games` lists, give it to `analyse_game`: it reads the PGN for you; where Chesspal's answers say to start the analyst agent for it, start that instead, each time those answers say so and never on your own, not even when the player asks for the analysis again. While it works, and after, talk the game over whenever the player asks: `look` and `position` with game and move show you any of its positions. When they show you something the review missed, first correct that move's note with `annotate_game` {game, notes: [{move, badge, note}]}, that move only, then answer. Words from the board page you answer with `ai_say`; words in the chat, in the chat. While a game goes on, go over no game, not even in your head or from a PGN they paste: tell them it waits until this one ends.
|
|
13
|
+
- To go over a game with the player once no game goes on, the one just over or one they bring as a PGN, or one of their latest on Lichess or Chess.com, which `site_games` lists, give it to `analyse_game`: it reads the PGN for you; where Chesspal's answers say to start the analyst agent for it, start that instead, each time those answers say so and never on your own, not even when the player asks for the analysis again. While it works, and after, talk the game over whenever the player asks: `look` and `position` with game and move show you any of its positions, and with `line` the position after a variation. When they show you something the review missed, first correct that move's note with `annotate_game` {game, notes: [{move, badge, note}]}, that move only, then answer. Words from the board page you answer with `ai_say`; words in the chat, in the chat. While a game goes on, go over no game, not even in your head or from a PGN they paste: tell them it waits until this one ends.
|
|
14
14
|
- How to read the facts: attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now; "At risk" lists the pieces attacked and undefended, attacked by a cheaper piece, or attacked more times than defended.
|
|
15
15
|
- Talk with the player in their language, in a few words. The facts and the tools are for you: tell the player what a chess player would say, never the lists or the tools' names. Name pieces and moves in words, as people talk at the board ("your bishop took on f7"); when you write a move, yours or the player's, write it with the pieces' figurines, as the tools show it (♞f6 for Nf6, ♝xf7+ for Bxf7+), for both sides and in every language: never with letters, neither N B R Q K nor the player's own (Cf6), even when they write them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chesspal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|