chesspal 0.3.3 → 0.3.5

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.
Files changed (61) hide show
  1. package/README.md +49 -10
  2. package/dist/commands/analysis/analyse.d.ts +4 -1
  3. package/dist/commands/analysis/analyse.js +46 -12
  4. package/dist/commands/analysis/index.d.ts +1 -0
  5. package/dist/commands/analysis/index.js +2 -1
  6. package/dist/commands/analysis/site-games.d.ts +27 -0
  7. package/dist/commands/analysis/site-games.js +84 -0
  8. package/dist/commands/index.d.ts +1 -1
  9. package/dist/commands/index.js +3 -2
  10. package/dist/commands/profiles/index.d.ts +1 -1
  11. package/dist/commands/profiles/index.js +1 -1
  12. package/dist/commands/profiles/profiles.d.ts +3 -1
  13. package/dist/commands/profiles/profiles.js +2 -1
  14. package/dist/commands/registry/context.d.ts +4 -1
  15. package/dist/commands/registry/registry.d.ts +2 -0
  16. package/dist/core/accounts.d.ts +11 -0
  17. package/dist/core/accounts.js +14 -0
  18. package/dist/core/index.d.ts +1 -0
  19. package/dist/core/index.js +1 -0
  20. package/dist/page/app/app.js +10 -9
  21. package/dist/page/dialogs/analyses.js +7 -17
  22. package/dist/page/dialogs/dialogs.css +102 -0
  23. package/dist/page/dialogs/outcome.js +13 -0
  24. package/dist/page/dialogs/site-games.js +71 -0
  25. package/dist/page/dialogs/studied.js +13 -19
  26. package/dist/page/dialogs/study.js +30 -13
  27. package/dist/page/guide/en.md +1 -0
  28. package/dist/page/guide/it.md +1 -0
  29. package/dist/page/helps/helps.css +26 -18
  30. package/dist/page/helps/helps.js +2 -2
  31. package/dist/page/text.js +32 -2
  32. package/dist/server/mcp/mcp.js +2 -2
  33. package/dist/server/page/analyses.d.ts +5 -0
  34. package/dist/server/page/analyses.js +18 -1
  35. package/dist/server/page/game.js +3 -3
  36. package/dist/server/page/page.js +4 -1
  37. package/dist/sites/chesscom.d.ts +1 -1
  38. package/dist/sites/chesscom.js +1 -1
  39. package/dist/sites/games/chesscom.d.ts +4 -0
  40. package/dist/sites/games/chesscom.js +54 -0
  41. package/dist/sites/games/game.d.ts +34 -0
  42. package/dist/sites/games/game.js +64 -0
  43. package/dist/sites/games/games.d.ts +14 -0
  44. package/dist/sites/games/games.js +22 -0
  45. package/dist/sites/games/index.d.ts +2 -0
  46. package/dist/sites/games/index.js +3 -0
  47. package/dist/sites/games/lichess.d.ts +4 -0
  48. package/dist/sites/games/lichess.js +45 -0
  49. package/dist/sites/index.d.ts +2 -1
  50. package/dist/sites/index.js +5 -3
  51. package/dist/sites/lichess.d.ts +1 -1
  52. package/dist/sites/lichess.js +1 -1
  53. package/dist/sites/request/index.d.ts +1 -0
  54. package/dist/sites/request/index.js +2 -0
  55. package/dist/sites/{request.d.ts → request/request.d.ts} +7 -0
  56. package/dist/sites/request/request.js +37 -0
  57. package/dist/sites/sites.d.ts +1 -1
  58. package/dist/sites/sites.js +1 -1
  59. package/instructions/opponent.md +1 -1
  60. package/package.json +1 -1
  61. package/dist/sites/request.js +0 -15
@@ -245,7 +245,7 @@ async function studyFromPage(request, response, served) {
245
245
  let brought;
246
246
  try {
247
247
  // Without what the chat was told: the analyst's instructions stay for the AI's own analyse_game.
248
- const opened = last ? lastPlayed(served.context) : game === undefined ? kept(served, pgn) : namedGame(served.context, game);
248
+ const opened = last ? lastPlayed(served.context) : game === undefined ? await kept(served, pgn) : namedGame(served.context, game);
249
249
  reviewed = isReviewed(opened.record);
250
250
  brought = !reviewed && !look;
251
251
  // 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.
@@ -377,8 +377,8 @@ function lastPlayed(context) {
377
377
  return { name, record };
378
378
  }
379
379
  /** 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. */
380
- function kept(served, pgn) {
381
- analyseGame.run({ pgn }, served.context);
380
+ async function kept(served, pgn) {
381
+ await analyseGame.run({ pgn }, served.context);
382
382
  return namedGame(served.context, gameInAnalysis(gameFolder(served.context.home())).game);
383
383
  }
384
384
  /** The longest PGN the page brings: a long game with its clocks, and the comments a site adds, which go. */
@@ -9,7 +9,7 @@ import { forgetAnalyst, gameStamp, tableTalkStamp } from "../../games/index.js";
9
9
  import { outdatedHostFiles } from "../../host-files/index.js";
10
10
  import { gameFolder } from "../../profiles/index.js";
11
11
  import { Bell, bellProbe, pagePorts } from "../bell/index.js";
12
- import { analysesOf, answerAnalyses } from "./analyses.js";
12
+ import { analysesOf, answerAnalyses, sendSiteGames } from "./analyses.js";
13
13
  import { answerChat } from "./attach.js";
14
14
  import { attachedSeen, chatSeenOf, pageContext } from "./chat.js";
15
15
  import { askNewGame, keepPosted, tellWatchers } from "./events.js";
@@ -131,6 +131,9 @@ function answer(request, response, served) {
131
131
  else if (path === "/api/analyses") {
132
132
  send(response, 200, json, JSON.stringify(analysesOf(served.context.home())));
133
133
  }
134
+ else if (path === "/api/site-games") {
135
+ sendSiteGames(response, served);
136
+ }
134
137
  else if (path === "/api/events") {
135
138
  keepPosted(request, response, served.watchers);
136
139
  // A page that opens reads the player's ratings again after a day (D-106).
@@ -1,4 +1,4 @@
1
1
  import type { SiteReading } from "../profiles/index.ts";
2
- import { type Fetch } from "./request.ts";
2
+ import { type Fetch } from "./request/index.ts";
3
3
  /** What Chess.com says of a name: undefined when it could not be reached. */
4
4
  export declare function chesscomReading(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteReading | undefined>;
@@ -1,5 +1,5 @@
1
1
  import { counted } from "./choice.js";
2
- import { askSite } from "./request.js";
2
+ import { askSite } from "./request/index.js";
3
3
  /** What Chess.com says of a name: undefined when it could not be reached. */
4
4
  export async function chesscomReading(account, fetch, signal) {
5
5
  const statistics = await askSite(`https://api.chess.com/pub/player/${encodeURIComponent(account.toLowerCase())}/stats`, fetch, signal);
@@ -0,0 +1,4 @@
1
+ import { type Fetch } from "../request/index.ts";
2
+ import { type SiteGamesAnswer } from "./game.ts";
3
+ /** What Chess.com gives of a name's latest games, the latest first. */
4
+ export declare function chesscomGames(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteGamesAnswer>;
@@ -0,0 +1,54 @@
1
+ // The player's latest games on Chess.com (D-110): its archives by month, the last one, and the one before when the last holds too few.
2
+ import { askSite } from "../request/index.js";
3
+ import { gamesASite, latestFirst, siteGameOf } from "./game.js";
4
+ /** What Chess.com gives of a name's latest games, the latest first. */
5
+ export async function chesscomGames(account, fetch, signal) {
6
+ const archives = await askSite(`${address}${encodeURIComponent(account.toLowerCase())}/games/archives`, fetch, signal);
7
+ if (archives === undefined || (archives.status !== 200 && archives.status !== 404)) {
8
+ return "unreached";
9
+ }
10
+ if (archives.status === 404) {
11
+ return "unknown";
12
+ }
13
+ // The months' addresses, the oldest first: the last two are read, the latest first.
14
+ const listed = archives.body?.archives;
15
+ // A 200 without its list of months, as a page of HTML, is no answer.
16
+ if (!Array.isArray(listed)) {
17
+ return "unreached";
18
+ }
19
+ const months = listed.filter((month) => typeof month === "string" && month.startsWith(address));
20
+ const games = [];
21
+ for (const monthAddress of months.slice(-2).reverse()) {
22
+ const month = await monthGames(monthAddress, fetch, signal);
23
+ // A month that cannot be read fails the site only when it gave no game yet.
24
+ if (month === undefined) {
25
+ if (games.length === 0) {
26
+ return "unreached";
27
+ }
28
+ break;
29
+ }
30
+ games.push(...month);
31
+ if (games.length >= gamesASite) {
32
+ break;
33
+ }
34
+ }
35
+ return games.sort(latestFirst).slice(0, gamesASite);
36
+ }
37
+ // Where Chess.com keeps a player's games: the months it lists are asked only there.
38
+ const address = "https://api.chess.com/pub/player/";
39
+ /** The games of standard chess of a month; undefined when it could not be read. */
40
+ async function monthGames(monthAddress, fetch, signal) {
41
+ const month = await askSite(monthAddress, fetch, signal);
42
+ const listed = month?.body?.games;
43
+ if (month?.status !== 200 || !Array.isArray(listed)) {
44
+ return undefined;
45
+ }
46
+ return listed.flatMap((game) => {
47
+ const { url, pgn, time_class: speed, rules, end_time: ended } = (game ?? {});
48
+ if (rules !== "chess" || typeof url !== "string" || typeof pgn !== "string" || typeof speed !== "string" || typeof ended !== "number") {
49
+ return [];
50
+ }
51
+ const read = siteGameOf("chess.com", `chess.com/${url.split("/").slice(-2).join("/")}`, speed, new Date(ended * 1000), pgn);
52
+ return read === undefined ? [] : [read];
53
+ });
54
+ }
@@ -0,0 +1,34 @@
1
+ import type { RatingSite } from "../../profiles/index.ts";
2
+ /** A finished game of the player's on a site, as the list gives it, with its PGN to study. */
3
+ export interface SiteGame {
4
+ readonly site: RatingSite;
5
+ /** What the list calls it: lichess/HqjDTU4A, chess.com/live/186466981288. */
6
+ readonly name: string;
7
+ readonly white: string;
8
+ readonly black: string;
9
+ readonly whiteRating?: number;
10
+ readonly blackRating?: number;
11
+ /** As the Result tag writes it: 1-0, 0-1, 1/2-1/2. */
12
+ readonly result: string;
13
+ /** As the site names it: blitz, rapid, daily… */
14
+ readonly speed: string;
15
+ /** When the game ended, as an ISO time. */
16
+ readonly played: string;
17
+ /** How long it was, in full moves. */
18
+ readonly moves: number;
19
+ readonly pgn: string;
20
+ }
21
+ /** What a site said of a name: its latest games, or that it could not be reached, or has no player of that name. */
22
+ export type SiteGamesAnswer = readonly SiteGame[] | "unreached" | "unknown";
23
+ /** The most games a site gives. */
24
+ export declare const gamesASite = 20;
25
+ /** A site's game from what its JSON gives and its PGN's tags; none for a name the list cannot write, or a game without a result, as one aborted. */
26
+ export declare function siteGameOf(site: RatingSite, name: string, speed: string, played: Date, pgn: string): SiteGame | undefined;
27
+ /** Whether a text is a game's name exactly as the list writes it: lichess/HqjDTU4A, chess.com/live/186466981288. */
28
+ export declare function isSiteGameName(text: string): boolean;
29
+ /** A game's name as the list writes it, from one given with spaces around or the site in other capitals (Lichess/HqjDTU4A); none for another text. */
30
+ export declare function siteGameName(text: string): string | undefined;
31
+ /** Whether a text points at a site's game without being a name the list writes: a link to Lichess or Chess.com, or a name gone wrong. */
32
+ export declare function pointsAtSite(text: string): boolean;
33
+ /** The latest games first. */
34
+ export declare function latestFirst(one: SiteGame, other: SiteGame): number;
@@ -0,0 +1,64 @@
1
+ /** The most games a site gives. */
2
+ export const gamesASite = 20;
3
+ /** A site's game from what its JSON gives and its PGN's tags; none for a name the list cannot write, or a game without a result, as one aborted. */
4
+ export function siteGameOf(site, name, speed, played, pgn) {
5
+ const tags = tagsOf(pgn);
6
+ const result = tags.Result;
7
+ if (!isSiteGameName(name) || result === undefined || !results.has(result) || Number.isNaN(played.getTime())) {
8
+ return undefined;
9
+ }
10
+ const whiteRating = ratingOf(tags.WhiteElo);
11
+ const blackRating = ratingOf(tags.BlackElo);
12
+ return {
13
+ site,
14
+ name,
15
+ white: tags.White ?? "?",
16
+ black: tags.Black ?? "?",
17
+ ...(whiteRating === undefined ? {} : { whiteRating }),
18
+ ...(blackRating === undefined ? {} : { blackRating }),
19
+ result,
20
+ speed,
21
+ played: played.toISOString(),
22
+ moves: fullMovesOf(pgn),
23
+ pgn,
24
+ };
25
+ }
26
+ /** Whether a text is a game's name exactly as the list writes it: lichess/HqjDTU4A, chess.com/live/186466981288. */
27
+ export function isSiteGameName(text) {
28
+ return siteGameName(text) === text;
29
+ }
30
+ /** A game's name as the list writes it, from one given with spaces around or the site in other capitals (Lichess/HqjDTU4A); none for another text. */
31
+ export function siteGameName(text) {
32
+ const [, site, rest = ""] = /^(lichess|chess\.com)\/(\S+)$/i.exec(text.trim()) ?? [];
33
+ if (site?.toLowerCase() === "lichess" && /^[A-Za-z0-9]+$/.test(rest)) {
34
+ return `lichess/${rest}`;
35
+ }
36
+ return site?.toLowerCase() === "chess.com" && /^[a-z]+\/\d+$/i.test(rest) ? `chess.com/${rest.toLowerCase()}` : undefined;
37
+ }
38
+ /** Whether a text points at a site's game without being a name the list writes: a link to Lichess or Chess.com, or a name gone wrong. */
39
+ export function pointsAtSite(text) {
40
+ const trimmed = text.trim();
41
+ return siteGameName(trimmed) === undefined && /^(?:https?:\/\/)?(?:www\.)?(?:lichess\.org|lichess\/|chess\.?com\b)/i.test(trimmed);
42
+ }
43
+ /** The latest games first. */
44
+ export function latestFirst(one, other) {
45
+ return other.played.localeCompare(one.played);
46
+ }
47
+ const results = new Set(["1-0", "0-1", "1/2-1/2"]);
48
+ // The tags of a PGN, [White "Anna"]: the sites do not depend on the chess core, and need only these.
49
+ function tagsOf(pgn) {
50
+ const tags = {};
51
+ for (const [, key, value] of pgn.matchAll(/^\s*\[(\w+)\s+"((?:[^"\\]|\\.)*)"\]\s*$/gm)) {
52
+ tags[key] = value.replace(/\\(.)/g, "$1");
53
+ }
54
+ return tags;
55
+ }
56
+ /** The full moves of a PGN's movetext: its moves counted without the tags, comments, move numbers, annotations and result. */
57
+ function fullMovesOf(pgn) {
58
+ const movetext = pgn.replace(/^\s*\[.*\]\s*$/gm, " ").replace(/\{[^}]*\}|;[^\n]*/g, " ");
59
+ const plies = movetext.split(/\s+/).filter((token) => token !== "" && !/^(?:\d+\.+|\$\d+|1-0|0-1|1\/2-1\/2|\*)$/.test(token)).map((token) => token.replace(/^\d+\.+/, ""));
60
+ return Math.ceil(plies.filter((token) => token !== "").length / 2);
61
+ }
62
+ function ratingOf(tag) {
63
+ return tag !== undefined && /^\d+$/.test(tag) ? Number(tag) : undefined;
64
+ }
@@ -0,0 +1,14 @@
1
+ import type { Accounts, RatingSite } from "../../profiles/index.ts";
2
+ import type { Fetch } from "../request/index.ts";
3
+ import { type SiteGame } from "./game.ts";
4
+ /** The latest games of the sites asked, the latest first; the sites that gave their list, those that could not be reached, and those with no player of that name. */
5
+ export interface SiteGames {
6
+ readonly games: readonly SiteGame[];
7
+ readonly answered: readonly RatingSite[];
8
+ readonly unreached: readonly RatingSite[];
9
+ readonly unknown: readonly RatingSite[];
10
+ }
11
+ /** What Lichess and Chess.com give now of the player's latest games there, each asked only for a name it has: a site that fails never fails the other. */
12
+ export declare function readSiteGames(accounts: Accounts, fetch: Fetch): Promise<SiteGames>;
13
+ /** The site of a game's name as the list writes it. */
14
+ export declare function siteOfGameName(name: string): RatingSite;
@@ -0,0 +1,22 @@
1
+ import { chesscomGames } from "./chesscom.js";
2
+ import { latestFirst } from "./game.js";
3
+ import { lichessGames } from "./lichess.js";
4
+ /** What Lichess and Chess.com give now of the player's latest games there, each asked only for a name it has: a site that fails never fails the other. */
5
+ export async function readSiteGames(accounts, fetch) {
6
+ const signal = AbortSignal.timeout(siteGamesTime);
7
+ const ask = async (site, account, games) => account === undefined ? [] : [[site, await games(account, fetch, signal)]];
8
+ const answers = (await Promise.all([ask("lichess", accounts.lichess, lichessGames), ask("chess.com", accounts.chesscom, chesscomGames)])).flat();
9
+ const sitesThat = (said) => answers.filter(([, answer]) => said(answer)).map(([site]) => site);
10
+ return {
11
+ games: answers.flatMap(([, answer]) => typeof answer === "string" ? [] : answer).sort(latestFirst),
12
+ answered: sitesThat((answer) => typeof answer !== "string"),
13
+ unreached: sitesThat((answer) => answer === "unreached"),
14
+ unknown: sitesThat((answer) => answer === "unknown"),
15
+ };
16
+ }
17
+ /** The site of a game's name as the list writes it. */
18
+ export function siteOfGameName(name) {
19
+ return name.startsWith("lichess/") ? "lichess" : "chess.com";
20
+ }
21
+ // The longest the sites may take to give the games, together, in milliseconds: a month of Chess.com's can be long.
22
+ const siteGamesTime = 10_000;
@@ -0,0 +1,2 @@
1
+ export { isSiteGameName, pointsAtSite, type SiteGame, siteGameName } from "./game.ts";
2
+ export { readSiteGames, type SiteGames, siteOfGameName } from "./games.ts";
@@ -0,0 +1,3 @@
1
+ // The player's latest games on Lichess and Chess.com (D-110), asked from the names in their profile to study one of them: never kept on disk.
2
+ export { isSiteGameName, pointsAtSite, siteGameName } from "./game.js";
3
+ export { readSiteGames, siteOfGameName } from "./games.js";
@@ -0,0 +1,4 @@
1
+ import { type Fetch } from "../request/index.ts";
2
+ import { type SiteGamesAnswer } from "./game.ts";
3
+ /** What Lichess gives of a name's latest games, the latest first. */
4
+ export declare function lichessGames(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteGamesAnswer>;
@@ -0,0 +1,45 @@
1
+ // The player's latest games on Lichess (D-110): finished, of standard chess, with their clocks and without the site's evaluations.
2
+ import { askSiteText } from "../request/index.js";
3
+ import { gamesASite, siteGameOf } from "./game.js";
4
+ /** What Lichess gives of a name's latest games, the latest first. */
5
+ export async function lichessGames(account, fetch, signal) {
6
+ // The speeds of standard chess alone, so that a player of variants still gets their last twenty standard games.
7
+ const query = `max=${gamesASite}&perfType=ultraBullet,bullet,blitz,rapid,classical,correspondence&pgnInJson=true&clocks=true&evals=false&opening=false`;
8
+ const answer = await askSiteText(`https://lichess.org/api/games/user/${encodeURIComponent(account)}?${query}`, "application/x-ndjson", fetch, signal);
9
+ if (answer === undefined || (answer.status !== 200 && answer.status !== 404)) {
10
+ return "unreached";
11
+ }
12
+ if (answer.status === 404) {
13
+ return "unknown";
14
+ }
15
+ const lines = answer.text.split("\n").filter((line) => line.trim() !== "").map(objectOf);
16
+ // An answer with text but no game in it, as a page of HTML, is no answer.
17
+ if (lines.length > 0 && lines.every((line) => line === undefined)) {
18
+ return "unreached";
19
+ }
20
+ return lines.flatMap(gameOf);
21
+ }
22
+ // Games not yet begun, or still going on.
23
+ const unfinished = new Set(["created", "started"]);
24
+ /** The JSON object of a line; none for a line that is not one. */
25
+ function objectOf(line) {
26
+ try {
27
+ const read = JSON.parse(line);
28
+ return typeof read === "object" && read !== null && !Array.isArray(read) ? read : undefined;
29
+ }
30
+ catch {
31
+ return undefined;
32
+ }
33
+ }
34
+ /** The game of one line of Lichess's answer: none for a variant, a game not finished, or a line that is no game. */
35
+ function gameOf(game) {
36
+ const { id, variant, speed, status, lastMoveAt, pgn } = game ?? {};
37
+ if (typeof id !== "string" || variant !== "standard" || typeof status !== "string" || unfinished.has(status)) {
38
+ return [];
39
+ }
40
+ if (typeof speed !== "string" || typeof lastMoveAt !== "number" || typeof pgn !== "string") {
41
+ return [];
42
+ }
43
+ const read = siteGameOf("lichess", `lichess/${id}`, speed, new Date(lastMoveAt), pgn);
44
+ return read === undefined ? [] : [read];
45
+ }
@@ -1,2 +1,3 @@
1
- export { answerTime, askSite, type Fetch } from "./request.ts";
1
+ export { isSiteGameName, pointsAtSite, readSiteGames, type SiteGame, siteGameName, type SiteGames, siteOfGameName } from "./games/index.ts";
2
+ export { answerTime, askSite, type Fetch } from "./request/index.ts";
2
3
  export { readingsStale, readSites } from "./sites.ts";
@@ -1,4 +1,6 @@
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). Its request asks npm too, for a newer Chesspal (D-108).
3
- export { answerTime, askSite } from "./request.js";
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, never
2
+ // asked by a tool of the AI's (D-106); and their latest games there, which a tool lists too, to study one (D-110). Below the commands, beside the profiles,
3
+ // whose types it fills. Its request asks npm too, for a newer Chesspal (D-108).
4
+ export { isSiteGameName, pointsAtSite, readSiteGames, siteGameName, siteOfGameName } from "./games/index.js";
5
+ export { answerTime, askSite } from "./request/index.js";
4
6
  export { readingsStale, readSites } from "./sites.js";
@@ -1,4 +1,4 @@
1
1
  import type { SiteReading } from "../profiles/index.ts";
2
- import { type Fetch } from "./request.ts";
2
+ import { type Fetch } from "./request/index.ts";
3
3
  /** What Lichess says of a name: undefined when it could not be reached. */
4
4
  export declare function lichessReading(account: string, fetch: Fetch, signal: AbortSignal): Promise<SiteReading | undefined>;
@@ -1,5 +1,5 @@
1
1
  import { counted } from "./choice.js";
2
- import { askSite } from "./request.js";
2
+ import { askSite } from "./request/index.js";
3
3
  /** What Lichess says of a name: undefined when it could not be reached. */
4
4
  export async function lichessReading(account, fetch, signal) {
5
5
  const user = await askSite(`${address}/${encodeURIComponent(account)}`, fetch, signal);
@@ -0,0 +1 @@
1
+ export { answerTime, askSite, askSiteText, type Fetch, type SiteAnswer, type SiteText } from "./request.ts";
@@ -0,0 +1,2 @@
1
+ // A question to a site's public API (D-24): JSON or text, named as Chesspal, within the signal it is given.
2
+ export { answerTime, askSite, askSiteText } from "./request.js";
@@ -5,7 +5,14 @@ export interface SiteAnswer {
5
5
  readonly status: number;
6
6
  readonly body?: unknown;
7
7
  }
8
+ /** What a site answered in text: its status and its text, as Lichess's games, one JSON object a line. */
9
+ export interface SiteText {
10
+ readonly status: number;
11
+ readonly text: string;
12
+ }
8
13
  /** The longest the sites may take to answer, together, in milliseconds: the page waits for them. */
9
14
  export declare const answerTime = 5000;
10
15
  /** Asks a site for JSON: what it answered, or undefined when it could not be reached before `signal` gave up. */
11
16
  export declare function askSite(url: string, fetch: Fetch, signal: AbortSignal): Promise<SiteAnswer | undefined>;
17
+ /** Asks a site for text of the type `accept` names: what it answered, or undefined when it could not be reached, or stopped, before `signal` gave up. */
18
+ export declare function askSiteText(url: string, accept: string, fetch: Fetch, signal: AbortSignal): Promise<SiteText | undefined>;
@@ -0,0 +1,37 @@
1
+ // A question to a site's public API (D-24): JSON or text 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: headersFor("application/json"), signal });
9
+ // The whole body read here: one the signal cuts short is a site not reached, not an answer without JSON.
10
+ const body = jsonOf(await response.text());
11
+ return body === undefined ? { status: response.status } : { status: response.status, body };
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ /** Asks a site for text of the type `accept` names: what it answered, or undefined when it could not be reached, or stopped, before `signal` gave up. */
18
+ export async function askSiteText(url, accept, fetch, signal) {
19
+ try {
20
+ const response = await fetch(url, { headers: headersFor(accept), signal });
21
+ return { status: response.status, text: await response.text() };
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ function jsonOf(text) {
28
+ try {
29
+ return JSON.parse(text);
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ function headersFor(accept) {
36
+ return { "Accept": accept, "User-Agent": `Chesspal/${version}` };
37
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Accounts, SiteReadings } from "../profiles/index.ts";
2
- import { type Fetch } from "./request.ts";
2
+ import { type Fetch } from "./request/index.ts";
3
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
4
  export declare function readSites(accounts: Accounts, fetch: Fetch, before: SiteReadings | undefined, now: Date): Promise<SiteReadings>;
5
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. */
@@ -1,6 +1,6 @@
1
1
  import { chesscomReading } from "./chesscom.js";
2
2
  import { lichessReading } from "./lichess.js";
3
- import { answerTime } from "./request.js";
3
+ import { answerTime } from "./request/index.js";
4
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
5
  export async function readSites(accounts, fetch, before, now) {
6
6
  const signal = AbortSignal.timeout(answerTime);
@@ -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, 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. 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.3",
3
+ "version": "0.3.5",
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": {
@@ -1,15 +0,0 @@
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
- }