chesspal 0.2.4 → 0.3.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 +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 +11 -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 +49 -20
- 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 +96 -43
- 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 +85 -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/table.css +123 -0
- package/dist/page/table/window.js +130 -0
- package/dist/page/talk/chat.css +23 -5
- package/dist/page/talk/talk.css +38 -26
- package/dist/page/talk/talk.js +6 -4
- package/dist/page/talk/voice.js +35 -9
- package/dist/page/text.js +131 -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 +65 -0
- package/dist/server/page/chat.js +87 -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 +51 -7
- package/dist/server/page/page.js +74 -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 +58 -0
- package/dist/server/page/table.js +210 -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 +34 -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 +29 -0
- package/dist/server/table/hosts/play.js +180 -0
- package/dist/server/table/hosts/seat.d.ts +33 -0
- package/dist/server/table/hosts/seat.js +80 -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 +231 -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,94 @@
|
|
|
1
|
+
// The table (D-104): the board page served in a process of its own, on a folder of the data folder that is one for every profile, which a chat attaches
|
|
2
|
+
// to; it closes by itself once no browser has had the page open, and no chat been attached, for a while.
|
|
3
|
+
import { request } from "node:http";
|
|
4
|
+
import { tableFolder } from "../../home/index.js";
|
|
5
|
+
import { setUpHosts } from "../../host-files/index.js";
|
|
6
|
+
import { pageAt, pagePorts, samePath } from "../bell.js";
|
|
7
|
+
import { startPage } from "../page/page.js";
|
|
8
|
+
/** Five minutes with no browser and no chat, looked at every fifteen seconds (D-104). */
|
|
9
|
+
export const tableTimes = { idle: 5 * 60_000, every: 15_000 };
|
|
10
|
+
/**
|
|
11
|
+
* Starts the table on the context's data folder, its folder set up as init does, its page opening chats through `chats`: undefined when another table
|
|
12
|
+
* started at the same time took a lower port, since a table's ports are its lock. It closes once no page has been open, no chat attached and no request
|
|
13
|
+
* come, for `times.idle`.
|
|
14
|
+
*/
|
|
15
|
+
export async function startTable(context, times = tableTimes, chats) {
|
|
16
|
+
const folder = tableFolder(context.home());
|
|
17
|
+
setUpHosts(folder, context.platform);
|
|
18
|
+
let quit = () => undefined;
|
|
19
|
+
const quitting = new Promise((done) => {
|
|
20
|
+
quit = done;
|
|
21
|
+
});
|
|
22
|
+
let lastUsed = context.now().getTime();
|
|
23
|
+
const used = () => {
|
|
24
|
+
lastUsed = context.now().getTime();
|
|
25
|
+
};
|
|
26
|
+
const page = await startPage({ ...context, folder }, { table: { used, quit, chats } });
|
|
27
|
+
if (await lostTo(page.url, folder)) {
|
|
28
|
+
await page.close();
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const watch = closeWhenIdle(page, () => context.now().getTime() - lastUsed, used, times, quit);
|
|
32
|
+
const closed = quitting.then(async () => {
|
|
33
|
+
clearInterval(watch);
|
|
34
|
+
await page.close();
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
url: page.url,
|
|
38
|
+
closed,
|
|
39
|
+
close: () => {
|
|
40
|
+
quit();
|
|
41
|
+
return closed;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** The table of a folder that answers on its ports, the lowest of them; its ports are asked together. */
|
|
46
|
+
export async function tableAt(folder) {
|
|
47
|
+
const ports = pagePorts(folder);
|
|
48
|
+
const said = await Promise.all(ports.map(pageAt));
|
|
49
|
+
const index = said.findIndex((one) => one?.table === true && samePath(one.folder, folder));
|
|
50
|
+
const port = ports[index];
|
|
51
|
+
return port === undefined ? undefined : { url: `http://127.0.0.1:${port}/`, port, version: said[index]?.version, chat: said[index]?.chat };
|
|
52
|
+
}
|
|
53
|
+
/** Asks the table at a port to quit; it answers before it goes, and a table that cannot be reached, or does not answer within a second or so, is let be. */
|
|
54
|
+
export function askToQuit(port) {
|
|
55
|
+
return new Promise((done) => {
|
|
56
|
+
const asked = request({ host: "127.0.0.1", port, path: "/api/table/quit", method: "POST", headers: { "Content-Type": "application/json" }, agent: false }, (response) => {
|
|
57
|
+
response.resume();
|
|
58
|
+
response.on("end", () => done());
|
|
59
|
+
});
|
|
60
|
+
asked.setTimeout(quitWait, () => asked.destroy());
|
|
61
|
+
asked.on("error", () => done());
|
|
62
|
+
asked.end("{}");
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
// A table answers at once: one stuck must not hold the chat's first tool, nor `chesspal`.
|
|
66
|
+
const quitWait = 1_500;
|
|
67
|
+
/** Whether a version is older than another, part by part: 0.2.4 is older than 0.10.0. */
|
|
68
|
+
export function isOlder(one, other) {
|
|
69
|
+
const parts = (version) => version.split(/[.+-]/).slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
|
|
70
|
+
const [mine, theirs] = [parts(one), parts(other)];
|
|
71
|
+
const differs = mine.findIndex((part, index) => part !== theirs[index]);
|
|
72
|
+
return differs !== -1 && mine[differs] < theirs[differs];
|
|
73
|
+
}
|
|
74
|
+
/** Whether a table of the same folder answers on a lower port than this one's: the other won, and this one goes. */
|
|
75
|
+
async function lostTo(url, folder) {
|
|
76
|
+
const port = Number(new URL(url).port);
|
|
77
|
+
const lower = pagePorts(folder).filter((other) => other < port);
|
|
78
|
+
const found = await Promise.all(lower.map(pageAt));
|
|
79
|
+
return found.some((said) => said?.table === true && samePath(said.folder, folder));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Looks every `times.every` whether a browser has the page open or a chat is attached, which uses the table, and quits once it has been unused for
|
|
83
|
+
* `times.idle`: the page that `chesspal` opens counts from its first request. The timer keeps the process alive till then.
|
|
84
|
+
*/
|
|
85
|
+
function closeWhenIdle(page, unusedFor, used, times, quit) {
|
|
86
|
+
return setInterval(() => {
|
|
87
|
+
if (page.inUse()) {
|
|
88
|
+
used();
|
|
89
|
+
}
|
|
90
|
+
else if (unusedFor() >= times.idle) {
|
|
91
|
+
quit();
|
|
92
|
+
}
|
|
93
|
+
}, times.every);
|
|
94
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SiteReading } from "../profiles/index.ts";
|
|
2
|
+
import { type Fetch } from "./request.ts";
|
|
3
|
+
/** What Chess.com says of a name: undefined when it could not be reached. */
|
|
4
|
+
export declare function chesscomReading(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteReading | undefined>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { counted } from "./choice.js";
|
|
2
|
+
import { askSite } from "./request.js";
|
|
3
|
+
/** What Chess.com says of a name: undefined when it could not be reached. */
|
|
4
|
+
export async function chesscomReading(account, fetch, signal) {
|
|
5
|
+
const statistics = await askSite(`https://api.chess.com/pub/player/${encodeURIComponent(account.toLowerCase())}/stats`, fetch, signal);
|
|
6
|
+
if (statistics === undefined || (statistics.status !== 200 && statistics.status !== 404)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (statistics.status === 404) {
|
|
10
|
+
return { account, found: false };
|
|
11
|
+
}
|
|
12
|
+
const speeds = statistics.body;
|
|
13
|
+
const chosen = counted((speed) => {
|
|
14
|
+
const last = speeds?.[`chess_${speed}`]?.last;
|
|
15
|
+
return typeof last?.rating === "number" && typeof last.rd === "number"
|
|
16
|
+
? { value: last.rating, deviation: last.rd, established: last.rd < provisional, ...(typeof last.date === "number" ? { played: dayOf(last.date) } : {}) }
|
|
17
|
+
: undefined;
|
|
18
|
+
});
|
|
19
|
+
return chosen === undefined ? { account, found: true } : { account, found: true, rating: { ...chosen, site: "chess.com" } };
|
|
20
|
+
}
|
|
21
|
+
// Chess.com gives no provisional mark: a rating is provisional while its deviation (RD) is as wide as Lichess's provisional ones.
|
|
22
|
+
const provisional = 110;
|
|
23
|
+
function dayOf(seconds) {
|
|
24
|
+
return new Date(seconds * 1000).toISOString().slice(0, 10);
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Speed } from "../profiles/index.ts";
|
|
2
|
+
/** A site's rating at one speed, as it gives it: established once it is no longer provisional. */
|
|
3
|
+
export interface Candidate {
|
|
4
|
+
readonly value: number;
|
|
5
|
+
readonly deviation: number;
|
|
6
|
+
readonly established: boolean;
|
|
7
|
+
readonly played?: string;
|
|
8
|
+
}
|
|
9
|
+
/** The rating that counts, with its speed. */
|
|
10
|
+
export interface Played {
|
|
11
|
+
readonly speed: Speed;
|
|
12
|
+
readonly value: number;
|
|
13
|
+
readonly deviation: number;
|
|
14
|
+
readonly played?: string;
|
|
15
|
+
}
|
|
16
|
+
/** The first established rating in the order the speeds count: none when every one is missing or provisional. */
|
|
17
|
+
export declare function counted(at: (speed: Speed) => Candidate | undefined): Played | undefined;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Which of a site's ratings counts (D-106): the rapid, and when it is missing or provisional the classical, then the blitz.
|
|
2
|
+
import { speeds } from "../profiles/index.js";
|
|
3
|
+
/** The first established rating in the order the speeds count: none when every one is missing or provisional. */
|
|
4
|
+
export function counted(at) {
|
|
5
|
+
for (const speed of speeds) {
|
|
6
|
+
const candidate = at(speed);
|
|
7
|
+
if (candidate?.established === true) {
|
|
8
|
+
const { value, deviation, played } = candidate;
|
|
9
|
+
return { speed, value: Math.round(value), deviation: Math.round(deviation), ...(played === undefined ? {} : { played }) };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The sites the player plays on (D-24, D-105): their public ratings on Lichess and chess.com, read from the names in their profile, without a key. Below
|
|
2
|
+
// the commands, beside the profiles, whose types it fills; never asked by a tool of the AI's (D-106).
|
|
3
|
+
export {} from "./request.js";
|
|
4
|
+
export { readingsStale, readSites } from "./sites.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SiteReading } from "../profiles/index.ts";
|
|
2
|
+
import { type Fetch } from "./request.ts";
|
|
3
|
+
/** What Lichess says of a name: undefined when it could not be reached. */
|
|
4
|
+
export declare function lichessReading(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteReading | undefined>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { counted } from "./choice.js";
|
|
2
|
+
import { askSite } from "./request.js";
|
|
3
|
+
/** What Lichess says of a name: undefined when it could not be reached. */
|
|
4
|
+
export async function lichessReading(account, fetch, signal) {
|
|
5
|
+
const user = await askSite(`${address}/${encodeURIComponent(account)}`, fetch, signal);
|
|
6
|
+
if (user === undefined || (user.status !== 200 && user.status !== 404)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
const profile = user.body;
|
|
10
|
+
// A closed account is no player any more.
|
|
11
|
+
if (user.status === 404 || profile?.disabled === true) {
|
|
12
|
+
return { account, found: false };
|
|
13
|
+
}
|
|
14
|
+
const chosen = counted((speed) => {
|
|
15
|
+
const speedRating = profile?.perfs?.[speed];
|
|
16
|
+
return typeof speedRating?.rating === "number" && typeof speedRating.rd === "number" && typeof speedRating.games === "number"
|
|
17
|
+
? { value: speedRating.rating, deviation: speedRating.rd, established: speedRating.prov !== true && speedRating.games > 0 }
|
|
18
|
+
: undefined;
|
|
19
|
+
});
|
|
20
|
+
if (chosen === undefined) {
|
|
21
|
+
return { account, found: true };
|
|
22
|
+
}
|
|
23
|
+
const played = await lastPlayed(account, chosen.speed, fetch, signal);
|
|
24
|
+
return { account, found: true, rating: { ...chosen, site: "lichess", ...(played === undefined ? {} : { played }) } };
|
|
25
|
+
}
|
|
26
|
+
const address = "https://lichess.org/api/user";
|
|
27
|
+
// The day of the last game at that speed, from the speed's statistics: the profile gives only when the player was last seen.
|
|
28
|
+
async function lastPlayed(account, speed, fetch, signal) {
|
|
29
|
+
const statistics = await askSite(`${address}/${encodeURIComponent(account)}/perf/${speed}`, fetch, signal);
|
|
30
|
+
const last = statistics?.body?.stat?.playStreak?.lastDate;
|
|
31
|
+
return typeof last === "string" && /^\d{4}-\d{2}-\d{2}/.test(last) ? last.slice(0, 10) : undefined;
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** How Chesspal fetches: the global fetch, or the tests' own. */
|
|
2
|
+
export type Fetch = typeof globalThis.fetch;
|
|
3
|
+
/** What a site answered: its status and its JSON, none when it sent none. */
|
|
4
|
+
export interface SiteAnswer {
|
|
5
|
+
readonly status: number;
|
|
6
|
+
readonly body?: unknown;
|
|
7
|
+
}
|
|
8
|
+
/** The longest the sites may take to answer, together, in milliseconds: the page waits for them. */
|
|
9
|
+
export declare const answerTime = 5000;
|
|
10
|
+
/** Asks a site for JSON: what it answered, or undefined when it could not be reached before `signal` gave up. */
|
|
11
|
+
export declare function askSite(url: string, fetch: Fetch, signal: AbortSignal): Promise<SiteAnswer | undefined>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A question to a site's public API (D-24): JSON over HTTPS, without a key, named as Chesspal and nothing of the player's besides the name asked.
|
|
2
|
+
import { version } from "../version.js";
|
|
3
|
+
/** The longest the sites may take to answer, together, in milliseconds: the page waits for them. */
|
|
4
|
+
export const answerTime = 5000;
|
|
5
|
+
/** Asks a site for JSON: what it answered, or undefined when it could not be reached before `signal` gave up. */
|
|
6
|
+
export async function askSite(url, fetch, signal) {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(url, { headers: { "Accept": "application/json", "User-Agent": `Chesspal/${version}` }, signal });
|
|
9
|
+
const body = await response.json().catch(() => undefined);
|
|
10
|
+
return body === undefined ? { status: response.status } : { status: response.status, body };
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Accounts, SiteReadings } from "../profiles/index.ts";
|
|
2
|
+
import { type Fetch } from "./request.ts";
|
|
3
|
+
/** What Lichess and Chess.com say now of the player's names there, at `now`; a site that cannot be reached keeps what it said before of the same name. */
|
|
4
|
+
export declare function readSites(accounts: Accounts, fetch: Fetch, before: SiteReadings | undefined, now: Date): Promise<SiteReadings>;
|
|
5
|
+
/** Whether the readings are of other names than the profile's now, or a site did not answer a while ago: they are read again then, whatever their age. */
|
|
6
|
+
export declare function readingsStale(readings: SiteReadings | undefined, accounts: Accounts | undefined, now: Date): boolean;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { chesscomReading } from "./chesscom.js";
|
|
2
|
+
import { lichessReading } from "./lichess.js";
|
|
3
|
+
import { answerTime } from "./request.js";
|
|
4
|
+
/** What Lichess and Chess.com say now of the player's names there, at `now`; a site that cannot be reached keeps what it said before of the same name. */
|
|
5
|
+
export async function readSites(accounts, fetch, before, now) {
|
|
6
|
+
const signal = AbortSignal.timeout(answerTime);
|
|
7
|
+
const [lichess, chesscom] = await Promise.all([
|
|
8
|
+
readSite(accounts.lichess, (account) => lichessReading(account, fetch, signal), before?.lichess),
|
|
9
|
+
readSite(accounts.chesscom, (account) => chesscomReading(account, fetch, signal), before?.chesscom),
|
|
10
|
+
]);
|
|
11
|
+
return { read: now.toISOString(), ...(lichess === undefined ? {} : { lichess }), ...(chesscom === undefined ? {} : { chesscom }) };
|
|
12
|
+
}
|
|
13
|
+
/** Whether the readings are of other names than the profile's now, or a site did not answer a while ago: they are read again then, whatever their age. */
|
|
14
|
+
export function readingsStale(readings, accounts, now) {
|
|
15
|
+
const retry = readings !== undefined && now.getTime() - Date.parse(readings.read) >= retryTime;
|
|
16
|
+
const stale = (reading, account) => reading === undefined ? account !== undefined : account === undefined || !sameName(reading.account, account) || (reading.unreached === true && retry);
|
|
17
|
+
return stale(readings?.lichess, accounts?.lichess) || stale(readings?.chesscom, accounts?.chesscom);
|
|
18
|
+
}
|
|
19
|
+
// A site that did not answer is asked again ten minutes later, not at every event of the page.
|
|
20
|
+
const retryTime = 10 * 60 * 1000;
|
|
21
|
+
async function readSite(account, read, before) {
|
|
22
|
+
return account === undefined ? undefined : settled(account, await read(account), before);
|
|
23
|
+
}
|
|
24
|
+
function settled(account, read, before) {
|
|
25
|
+
if (read !== undefined) {
|
|
26
|
+
return read;
|
|
27
|
+
}
|
|
28
|
+
return before !== undefined && sameName(before.account, account) ? { ...before, account, unreached: true } : { account, unreached: true };
|
|
29
|
+
}
|
|
30
|
+
// The sites take a name in capitals or not.
|
|
31
|
+
function sameName(one, other) {
|
|
32
|
+
return one.toLowerCase() === other.toLowerCase();
|
|
33
|
+
}
|
package/instructions/analysis.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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; "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.
|
|
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
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
4
|
- Never invent a move, a position or a result. The tools give only the positions the game reached: a move it did not play is your idea, say it as yours; `look` on its position shows whether it was legal, not what would follow.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
You are the player's chess analyst: a game they played against an AI opponent is over, and you review it on the board page, where they go through it move by move. You work in the background; the AI that started you talks with the player.
|
|
2
2
|
|
|
3
3
|
- Your voice: when the game was against you (`analyse_game` says "You played Black" or "White"), every note, the summary and the closing are you talking to the player across the board: your moves in the first person ("I take your bishop", "my knight"), theirs in the second ("your bishop had no defender"); never "Black", "White", "the AI" or "the player" for the two of you, and never "you" for a move of yours. Warm and plain, a player going over the game with them, never a manual. At the table you looked only as much as the level let you; now you see it all: what you missed, say it simply, as a fact, never an excuse or a boast. In `analyse_game`'s lines, "You played", "you said" and "your plan" are yours.
|
|
4
|
-
- Call `analyse_game` with the game your prompt names as game, or with no game for the game just over: it gives the facts of every move, and the game's name: give it as game to every tool after, since the board page may open another game meanwhile; the judgment is yours. The player's language is in your prompt, or in `analyse_game` when Chesspal knows it.
|
|
4
|
+
- Call `analyse_game` with the game your prompt names as game, or with no game for the game just over: it gives the facts of every move, and the game's name: give it as game to every tool after, since the board page may open another game meanwhile; the judgment is yours. Call only `analyse_game`, `annotate_game`, `look` and `position`: the lines that tell the chat to call `instructions` and `status` are not for you. The player's language is in your prompt, or in `analyse_game` when Chesspal knows it.
|
|
5
5
|
- The badges and notes already on the moves ("you marked", "you noted") are the coach's, given at the table, or a review from before: look at each again now that you see the whole game, keep it or give the move a new badge and note, and spend your time on the moves nobody judged.
|
|
6
6
|
- Keep your review with `annotate_game`, in few calls: a badge on each move you would point out, with a short note when it needs one; a short note on each move of the opening book, what it is for; your word on the whole game as summary, and a last word on how it ended as closing. Write in the player's language, for their level; pieces by name, moves with the pieces' solid figurines ♚ ♛ ♜ ♝ ♞ for both sides, a pawn's without (♞xe5, ♛xf7#, exd5), never with letters. Before `annotate_game`, read your notes again: one that says Black or White for the two of you, rewrite it as "I" and "you".
|
|
7
7
|
- With `annotate_game`'s `ahead`, give who is ahead after every move, for the page's bar and its chart of the game: in pawns from White's side as chess.com shows it (1.5, -3, 0), or a mate you see ("#3", "#-3"). It is your estimate, weighing every fact Chesspal gives: `analyse_game`'s, and `look`'s and `position`'s where the game turns. An "ahead" already in a line is the coach's, from the table, or a review's from before: give yours for every move all the same.
|
package/instructions/coach.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
You are the player's chess coach while they play an AI opponent with the helps on: you see every fact of the game, and your words go to the board page alone, where the player reads them while they think their next move; the opponent never sees them.
|
|
2
2
|
|
|
3
3
|
- You are that opponent too, in another hat: the same voice, warm and plain, a player beside them at the board, never a manual. Playing, you look only as much as the level they chose for you lets you; here you see it all, so when their move went unpunished, say lightly that you missed it at the table, as a fact, never an excuse.
|
|
4
|
-
- Call `coach {}` first: it gives the game, the player's move to judge with the position they played it in, and the position they are to move in now.
|
|
4
|
+
- Call `coach {}` first: it gives the game, the player's move to judge with the position they played it in, and the position they are to move in now. Call only `coach`: the lines that tell the chat to call `instructions` and `status` are not for you.
|
|
5
|
+
- 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.
|
|
5
6
|
- While the game goes on, say who is ahead in the position they are to move in, with `ahead`, for the page's bar: in pawns from White's side as chess.com shows it (1.5, -3, 0), or a mate you see in moves of the side that mates ("#3" White's, "#-3" Black's). It is your estimate, weighing every fact `coach {}` gives.
|
|
6
7
|
- Judge their move only when it stands out (a mistake, a miss, a fine find), never a routine one: a badge as chess.com names them, and a note of a sentence or two on why.
|
|
7
8
|
- Give a hint for their turn only when there is something to find. Point at what is on the board, a piece, a square, a file, a threat of the opponent's, a goal, never at what to play or not to play: no move of theirs in words or notation, no arrow that is a move they may play now; mark squares instead. Chesspal refuses a hint that gives their move.
|
package/instructions/opponent.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
You choose your moves, explain them briefly and talk with the player.
|
|
2
2
|
|
|
3
|
-
-
|
|
3
|
+
- `status` says where the game stands and what to do next. The game is played on the board page, in the player's browser, and the chat is for talking: when `status` says nobody has it open, give them its address once. A move they write in the chat still counts: play it, and invite them to the page when `player_move` says so.
|
|
4
4
|
- Every move goes through the tools: the player's with `player_move`, which answers with the position for your reply; yours with `ai_move`, with your plan. Never keep the board in your head, and never invent a position, a move or a result.
|
|
5
|
-
- When your level gives them (`status` and `new_game` say it), `look` gives the whole view of the position when the one you have is not enough; `position` gives the positional facts (pawns, files, outposts, king safety, phase) when you set or change your plan.
|
|
5
|
+
- When your level gives them (`status` and `new_game` say it), `look` gives the whole view of the position when the one you have is not enough; `position` gives the positional facts (pawns, files, outposts, king safety, phase) when you set or change your plan. Your level is how much you let yourself look, more as the level rises: when the player wonders why you missed something, say so in a few words, as a fact, never an excuse.
|
|
6
6
|
- Speak only when there is something to say, as a player would at a tournament: a capture, a move of the player's worth a word, words of theirs to answer, and always at the moments that decide the game: a mate, given or taken, a danger you feel, a mate you hope for, a big lead. Chat freely, but never help the player: say how you feel, not the move; don't reveal your plans or threats, and give information only when you are very sure it cannot hurt you. Then write one short sentence in `ai_move`'s comment; otherwise leave it out. `ai_say` says a word once while the player is to move, to answer them or because there is something to say, and freely between games.
|
|
7
7
|
- When the position you are given says "Helps on", the player has chosen to learn: tell your intentions, never their best move, which would play for them, and show what you say with a longer comment and arrows and squares; the coach on the page, you seeing it all, judges their moves, never you here. "Helps off now" brings back the tournament.
|
|
8
8
|
- When `ai_move` tells you to start an agent that thinks on the player's time, start it as it says; its end needs no word from you: write nothing in the chat then. `player_move` and `status` give you what it thought of the player's move. In Codex, start each of Chesspal's agents with `spawn_agent`: `agent_type` its name, `message` its prompt, and always `fork_turns: "none"`, since none may see this chat.
|
|
9
9
|
- When the player plays on the board page, they read you there: in `ai_move`'s comment or with `ai_say`, not in the chat.
|
|
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.
|
|
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
|
-
-
|
|
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, 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,
|
|
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, 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.
|
|
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/instructions/ponder.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
You are the player's chess opponent, thinking on their time while they choose their move: you prepare your answers to their likely replies, so that your next move comes sooner and better thought. You never play a move, and the chat never sees your words.
|
|
2
2
|
|
|
3
3
|
- Call `ponder {}` first: it gives the position, its number, your level, your plan, your mood and what you said with your move. Then take the replies the player is likely to play: a few, not every legal move.
|
|
4
|
+
- Call only `ponder`: the lines that tell the chat to call `instructions` and `status` are not for you.
|
|
4
5
|
- For each, `ponder {position, reply}` shows the position after it, as you will see it at your move; choose your answer as you would play it, with your plan, and keep it with `ponder {position, reply, move, why}`, the reason in one sentence. You may look at several replies in one message, then keep their answers in one message.
|
|
5
6
|
- When you have no more replies to think on, call `ponder {position, done: true}` alone, last, and again while it says it is still their move.
|
|
6
7
|
- Whenever `ponder` says stop, stop at once: end with one word.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This folder is the player's chess table, and you are their opponent; Chesspal keeps the rules, the board, the clock and the games. In every chat, before you write anything, even to a greeting, call `instructions` and `status`; again after the conversation is summarized. Every move goes through Chesspal's tools: never keep the board in your head or invent a position.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chesspal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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": {
|