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
package/dist/page/talk/voice.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export function speak(words, language) {
|
|
1
|
+
/** Reads the words aloud in the voice's language, the moves in words, cutting short what was being read. */
|
|
2
|
+
export function speak(words, voice) {
|
|
4
3
|
if (typeof speechSynthesis === "undefined") {
|
|
5
4
|
return;
|
|
6
5
|
}
|
|
7
|
-
const said = words.replace(/\*\*/g, "").trim();
|
|
6
|
+
const said = spokenMoves(words.replace(/\*\*/g, ""), voice.saidMove).trim();
|
|
8
7
|
speechSynthesis.cancel();
|
|
9
8
|
clearTimeout(pending);
|
|
10
9
|
if (said === "") {
|
|
@@ -13,10 +12,10 @@ export function speak(words, language) {
|
|
|
13
12
|
// Chrome drops words spoken right after a cancel, and lists its voices late the first time: the words wait a moment.
|
|
14
13
|
pending = setTimeout(() => {
|
|
15
14
|
const utterance = new SpeechSynthesisUtterance(said);
|
|
16
|
-
const
|
|
17
|
-
utterance.lang =
|
|
18
|
-
if (
|
|
19
|
-
utterance.voice =
|
|
15
|
+
const reader = readerFor(voice.language);
|
|
16
|
+
utterance.lang = reader?.lang ?? voice.language;
|
|
17
|
+
if (reader !== undefined) {
|
|
18
|
+
utterance.voice = reader;
|
|
20
19
|
}
|
|
21
20
|
speechSynthesis.resume();
|
|
22
21
|
speechSynthesis.speak(utterance);
|
|
@@ -29,9 +28,36 @@ export function hush() {
|
|
|
29
28
|
speechSynthesis.cancel();
|
|
30
29
|
}
|
|
31
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The words with each move the AI wrote said in words, since a voice reads ♟g4 as "chess pawn g4": ♞xf6+, Nxf6+, exd5, O-O, e8=♛. A square alone, e4,
|
|
33
|
+
* stays as it is; a pawn's move becomes words only with its figurine, a capture, a promotion or a check, and without the file it takes from.
|
|
34
|
+
*/
|
|
35
|
+
export function spokenMoves(words, say) {
|
|
36
|
+
return words.replace(movePattern, (move, ...found) => {
|
|
37
|
+
const { castles, figurine, letter, from, takes, square, promotion, check } = found.at(-1);
|
|
38
|
+
const checked = check === undefined ? {} : { check: check === "+" ? "check" : "mate" };
|
|
39
|
+
if (castles !== undefined) {
|
|
40
|
+
return say({ castles: castles.length > 3 ? "long" : "short", ...checked });
|
|
41
|
+
}
|
|
42
|
+
if (square === undefined || (figurine === undefined && letter === undefined && takes === undefined && promotion === undefined && check === undefined)) {
|
|
43
|
+
return move;
|
|
44
|
+
}
|
|
45
|
+
const piece = figurine === undefined ? (letter ?? "P") : pieceOf(figurine);
|
|
46
|
+
const promoted = promotion === undefined ? {} : { promotion: pieceOf(promotion) };
|
|
47
|
+
// A pawn's file, exd5, goes unsaid: "pawn takes d5".
|
|
48
|
+
const said = from === undefined || from === "" || piece === "P" ? {} : { from };
|
|
49
|
+
return say({ piece, ...said, takes: takes !== undefined, square, ...promoted, ...checked });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
32
52
|
let pending;
|
|
53
|
+
// A move in SAN, with the pieces' letters or figurines, as the AI writes it; the parts named, castling apart.
|
|
54
|
+
const movePattern = /(?<![\w♔-♟])(?:(?<castles>O-O(?:-O)?|0-0(?:-0)?)|(?:(?<figurine>[♔-♟])[\uFE0E\uFE0F]?|(?<letter>[KQRBN]))?(?<from>[a-h]?[1-8]?)(?<takes>[x×])?(?<square>[a-h][1-8])(?:=?(?<promotion>[QRBN]|[♔-♟]))?[\uFE0E\uFE0F]?)(?<check>\+\+|[+#])?(?![\w♔-♟])/gu;
|
|
55
|
+
// A piece's letter, or its figurine: White's run ♔♕♖♗♘♙ and Black's ♚♛♜♝♞♟, in SAN's order.
|
|
56
|
+
function pieceOf(sign) {
|
|
57
|
+
return ("KQRBN".includes(sign) ? sign : "KQRBNP".charAt(((sign.codePointAt(0) ?? 0) - 0x2654) % 6));
|
|
58
|
+
}
|
|
33
59
|
/** A voice for the language: a local one first, the browser's default among them first. */
|
|
34
|
-
function
|
|
60
|
+
function readerFor(language) {
|
|
35
61
|
const voices = speechSynthesis.getVoices().filter((voice) => voice.lang.toLowerCase().startsWith(language.toLowerCase()));
|
|
36
62
|
const local = voices.filter((voice) => voice.localService);
|
|
37
63
|
return local.find((voice) => voice.default) ?? local[0] ?? voices[0];
|
package/dist/page/text.js
CHANGED
|
@@ -5,12 +5,31 @@ export function textFor(languages) {
|
|
|
5
5
|
}
|
|
6
6
|
const englishNames = { K: "king", Q: "queen", R: "rook", B: "bishop", N: "knight", P: "pawn" };
|
|
7
7
|
const italianNames = { K: "re", Q: "donna", R: "torre", B: "alfiere", N: "cavallo", P: "pedone" };
|
|
8
|
+
const englishMoveWords = {
|
|
9
|
+
names: englishNames,
|
|
10
|
+
castles: { short: "castles short", long: "castles long" },
|
|
11
|
+
from: "from",
|
|
12
|
+
to: "to",
|
|
13
|
+
takes: "takes",
|
|
14
|
+
promotes: "promotes to",
|
|
15
|
+
check: { check: "check", mate: "checkmate" },
|
|
16
|
+
};
|
|
17
|
+
const italianMoveWords = {
|
|
18
|
+
names: italianNames,
|
|
19
|
+
castles: { short: "arrocco corto", long: "arrocco lungo" },
|
|
20
|
+
from: "da",
|
|
21
|
+
to: "in",
|
|
22
|
+
takes: "prende",
|
|
23
|
+
promotes: "promuove a",
|
|
24
|
+
check: { check: "scacco", mate: "scacco matto" },
|
|
25
|
+
};
|
|
8
26
|
const english = {
|
|
9
27
|
language: "en",
|
|
10
28
|
flip: "Flip",
|
|
11
29
|
theme: "Light or dark theme",
|
|
12
30
|
sounds: "Sounds",
|
|
13
31
|
voice: "Read the AI's words aloud",
|
|
32
|
+
saidMove: (move) => moveInWords(englishMoveWords, move),
|
|
14
33
|
newGame: "New game",
|
|
15
34
|
offerDraw: "Offer a draw",
|
|
16
35
|
offerDrawWhen: "Offer a draw: on your turn, once each side has moved",
|
|
@@ -108,6 +127,7 @@ const english = {
|
|
|
108
127
|
talk: "The talk",
|
|
109
128
|
move: "Move",
|
|
110
129
|
draw: "draw",
|
|
130
|
+
aborted: "Game aborted",
|
|
111
131
|
checkmate: "checkmate",
|
|
112
132
|
ended: {
|
|
113
133
|
checkmate: "checkmate",
|
|
@@ -132,6 +152,28 @@ const english = {
|
|
|
132
152
|
thinking: { move: "Thinking about its move", ponder: "Thinking while you play" },
|
|
133
153
|
asleep: (name) => `${name} is not listening: write a message, such as "hi", in the window where you opened ${name}, not here. Its clock waits until it answers.`,
|
|
134
154
|
asleepInAnalysis: (name) => `${name} is not listening: write to it in the window where you opened it, not here.`,
|
|
155
|
+
table: {
|
|
156
|
+
title: "Who are you playing?",
|
|
157
|
+
choose: "Choose the chat to play with: it opens in a window of its own, to keep open.",
|
|
158
|
+
restartHint: "Choose the model and effort, then press Claude Code or Codex: the chat starts again, clean, in the same window, once it has ended its answer. The game stays as it is.",
|
|
159
|
+
byHand: (name) => `You opened this chat yourself: change ${name}'s model with /model in its window, or close it and open one from here, with the choices below.`,
|
|
160
|
+
noChat: "No chat at the table: choose one",
|
|
161
|
+
change: "Change the chat or its model",
|
|
162
|
+
install: (name) => `To play with ${name}, install Claude Code or Codex, then open this page again.`,
|
|
163
|
+
model: "Model",
|
|
164
|
+
effort: "Effort",
|
|
165
|
+
asHost: (host) => `as set in ${host}`,
|
|
166
|
+
opening: "Opening the chat… In the terminal, if it asks whether you trust the folder, choose Yes with the arrow keys and press Enter.",
|
|
167
|
+
again: (name) => `${name} starts again with a clean chat for the new game…`,
|
|
168
|
+
restart: "The chat starts again, clean…",
|
|
169
|
+
waiting: (name) => `${name} starts again once it has ended its answer…`,
|
|
170
|
+
restartChanged: "Starts again, clean, with the new choices",
|
|
171
|
+
restartNow: "Start again now: it cuts the answer it is giving",
|
|
172
|
+
cancel: "Do not start again",
|
|
173
|
+
failed: "The terminal did not open: type the command below in a terminal.",
|
|
174
|
+
notRestarted: "The chat did not start again: close its window, then open another from here.",
|
|
175
|
+
typeIt: "Or type in a terminal:",
|
|
176
|
+
},
|
|
135
177
|
commandHere: (name) => `/clear and the other commands go in the window where you opened ${name}, not here.`,
|
|
136
178
|
refused: {
|
|
137
179
|
"no-game": () => "There is no game going on: press + to start one, or ask for one in the chat.",
|
|
@@ -187,14 +229,15 @@ const english = {
|
|
|
187
229
|
learning: ["I've just learned", "I know the rules, little more"],
|
|
188
230
|
casual: ["I play now and then", "With friends or on my phone"],
|
|
189
231
|
regular: ["I play often", "Online or at a club"],
|
|
190
|
-
tournament: ["I play tournaments", "
|
|
232
|
+
tournament: ["I play tournaments", "At a club, with a rating"],
|
|
191
233
|
},
|
|
192
|
-
rating: "Do you have a rating?",
|
|
193
234
|
optional: "optional",
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
235
|
+
privacy: "We only read your public rating. No password.",
|
|
236
|
+
speeds: { rapid: "rapid", classical: "classical", blitz: "blitz" },
|
|
237
|
+
ratingTitle: (speed, played) => (played === undefined ? speed : `${speed} · last game ${played}`),
|
|
238
|
+
notFound: (site) => `${site} has no player of this name`,
|
|
239
|
+
unreached: (site) => `${site} isn't answering: we'll try again later`,
|
|
240
|
+
pending: "Reading your rating…",
|
|
198
241
|
start: "Start playing",
|
|
199
242
|
create: "Done",
|
|
200
243
|
newProfile: "Add a player",
|
|
@@ -202,12 +245,17 @@ const english = {
|
|
|
202
245
|
eachOwn: "Each player has their own games and settings.",
|
|
203
246
|
changePlayer: "Change player",
|
|
204
247
|
back: "Back",
|
|
205
|
-
|
|
206
|
-
|
|
248
|
+
yourProfile: "Your profile",
|
|
249
|
+
editProfile: "Edit your profile",
|
|
250
|
+
yourPicture: "Your picture: edit your profile",
|
|
207
251
|
avatars: { "short-hair": "Short hair", bun: "Bun", glasses: "Glasses", beanie: "Beanie", beard: "Beard", curls: "Curls", moustache: "Moustache", "silver-bob": "Silver bob", cat: "Cat", owl: "Owl" },
|
|
208
252
|
whilePlaying: "Finish the game, or resign, before changing player.",
|
|
253
|
+
accounts: "Do you play on Lichess, Chess.com or in FIDE tournaments?",
|
|
209
254
|
username: "your username",
|
|
210
255
|
accountTitle: "With your name there, a game you load from there knows which side is yours.",
|
|
256
|
+
fidePlaceholder: "your rating",
|
|
257
|
+
fideTitle: "Your FIDE rating, as FIDE lists it: Chesspal cannot read it by itself.",
|
|
258
|
+
fideRange: (lowest, highest) => `A FIDE rating goes from ${lowest} to ${highest}`,
|
|
211
259
|
badImage: "That picture won't open: try another photo (the ones from a phone usually work).",
|
|
212
260
|
},
|
|
213
261
|
start: {
|
|
@@ -343,6 +391,20 @@ const english = {
|
|
|
343
391
|
control: "How many of White's pieces and of Black's reach the square",
|
|
344
392
|
ends: { crown: "Won", mate: "Checkmated", flag: "Resigned", half: "Draw, half a point each" },
|
|
345
393
|
},
|
|
394
|
+
guide: {
|
|
395
|
+
title: "Guide",
|
|
396
|
+
contents: "Contents",
|
|
397
|
+
onGitHub: "Chesspal on GitHub",
|
|
398
|
+
onNpm: "Chesspal on npm",
|
|
399
|
+
support: "Support Chesspal",
|
|
400
|
+
credits: "Credits",
|
|
401
|
+
creditLines: [
|
|
402
|
+
"The rules: [chess.js](https://github.com/jhlywa/chess.js), under the BSD 2-Clause license.",
|
|
403
|
+
"The openings' names: Lichess's [chess-openings](https://github.com/lichess-org/chess-openings), under CC0.",
|
|
404
|
+
"The pieces: Nitida, Torretta and Capablanca are Chesspal's own, the last after the diagrams of Capablanca's *Chess Fundamentals* (1921), under CC BY 4.0: [their credits](https://github.com/stefanocaronia/chesspal/blob/main/src/page/pieces/CREDITS.md).",
|
|
405
|
+
"The sounds: Chesspal's own.",
|
|
406
|
+
],
|
|
407
|
+
},
|
|
346
408
|
};
|
|
347
409
|
const italian = {
|
|
348
410
|
language: "it",
|
|
@@ -350,6 +412,7 @@ const italian = {
|
|
|
350
412
|
theme: "Tema chiaro o scuro",
|
|
351
413
|
sounds: "Suoni",
|
|
352
414
|
voice: "Leggi ad alta voce le parole dell'AI",
|
|
415
|
+
saidMove: (move) => moveInWords(italianMoveWords, move),
|
|
353
416
|
newGame: "Nuova partita",
|
|
354
417
|
offerDraw: "Proponi patta",
|
|
355
418
|
offerDrawWhen: "Proponi patta: nel tuo turno, dopo una mossa a testa",
|
|
@@ -447,6 +510,7 @@ const italian = {
|
|
|
447
510
|
talk: "Conversazione",
|
|
448
511
|
move: "Mossa",
|
|
449
512
|
draw: "patta",
|
|
513
|
+
aborted: "Partita annullata",
|
|
450
514
|
checkmate: "scacco matto",
|
|
451
515
|
ended: {
|
|
452
516
|
checkmate: "scacco matto",
|
|
@@ -471,6 +535,28 @@ const italian = {
|
|
|
471
535
|
thinking: { move: "Sta pensando alla sua mossa", ponder: "Pensa mentre giochi tu" },
|
|
472
536
|
asleep: (name) => `${name} non ti sta ascoltando: scrivi un messaggio, per esempio "ciao", nella finestra dove hai aperto ${name}, non qui. Il suo orologio aspetta che risponda.`,
|
|
473
537
|
asleepInAnalysis: (name) => `${name} non ti sta ascoltando: scrivigli nella finestra dove l'hai aperto, non qui.`,
|
|
538
|
+
table: {
|
|
539
|
+
title: "Contro chi giochi?",
|
|
540
|
+
choose: "Scegli la chat con cui giocare: parte in una finestra a parte, da tenere aperta.",
|
|
541
|
+
restartHint: "Scegli modello e ragionamento, poi premi Claude Code o Codex: la chat riparte pulita nella stessa finestra, appena ha finito di rispondere. La partita resta com'è.",
|
|
542
|
+
byHand: (name) => `Questa chat l'hai aperta tu: cambia il modello di ${name} con /model nella sua finestra, oppure chiudila e aprine una da qui, con le scelte qui sotto.`,
|
|
543
|
+
noChat: "Nessuna chat al tavolo: scegline una",
|
|
544
|
+
change: "Cambia la chat o il modello",
|
|
545
|
+
install: (name) => `Per giocare con ${name}, installa Claude Code o Codex, poi apri di nuovo questa pagina.`,
|
|
546
|
+
model: "Modello",
|
|
547
|
+
effort: "Ragionamento",
|
|
548
|
+
asHost: (host) => `come è impostato in ${host}`,
|
|
549
|
+
opening: "Apro la chat… Nel terminale, se ti chiede se ti fidi della cartella, scegli Yes con le frecce e premi Invio.",
|
|
550
|
+
again: (name) => `${name} riparte con una chat pulita per la nuova partita…`,
|
|
551
|
+
restart: "La chat riparte pulita…",
|
|
552
|
+
waiting: (name) => `${name} riparte appena ha finito di rispondere…`,
|
|
553
|
+
restartChanged: "Riparte pulita con le nuove scelte",
|
|
554
|
+
restartNow: "Riparti adesso: interrompe la risposta in corso",
|
|
555
|
+
cancel: "Non ripartire",
|
|
556
|
+
failed: "Il terminale non si è aperto: scrivi il comando qui sotto in un terminale.",
|
|
557
|
+
notRestarted: "La chat non è ripartita: chiudi la sua finestra, poi aprine un'altra da qui.",
|
|
558
|
+
typeIt: "Oppure scrivi in un terminale:",
|
|
559
|
+
},
|
|
474
560
|
commandHere: (name) => `/clear e gli altri comandi vanno scritti nella finestra dove hai aperto ${name}, non qui.`,
|
|
475
561
|
refused: {
|
|
476
562
|
"no-game": () => "Nessuna partita in corso: premi + per iniziarne una, o chiedila nella chat.",
|
|
@@ -492,7 +578,7 @@ const italian = {
|
|
|
492
578
|
piece: (letter, side) => `${italianNames[letter]} ${side === "white" ? (letter === "Q" || letter === "R" ? "bianca" : "bianco") : (letter === "Q" || letter === "R" ? "nera" : "nero")}`,
|
|
493
579
|
taken: (pieces, ahead) => `Pezzi presi: ${pieces.length === 0 ? "nessuno" : pieces.join(", ")}${ahead > 0 ? `; avanti di ${ahead}` : ""}`,
|
|
494
580
|
writeTo: (name) => `Scrivi a ${name}…`,
|
|
495
|
-
askAbout: (name) => `Chiedi a ${name} di questa partita…`,
|
|
581
|
+
askAbout: (name) => `Chiedi ${name === "AI" ? "all'AI" : `a ${name}`} di questa partita…`,
|
|
496
582
|
goTo: (move) => `Vai a ${move}`,
|
|
497
583
|
readBy: (name) => `Letto da ${name}`,
|
|
498
584
|
wrote: (name) => `${name} ha scritto`,
|
|
@@ -527,14 +613,15 @@ const italian = {
|
|
|
527
613
|
learning: ["Ho appena imparato", "Conosco le regole, poco di più"],
|
|
528
614
|
casual: ["Gioco ogni tanto", "Con amici o sul telefono"],
|
|
529
615
|
regular: ["Gioco spesso", "Online o al circolo"],
|
|
530
|
-
tournament: ["Gioco a torneo", "
|
|
616
|
+
tournament: ["Gioco a torneo", "Al circolo, con punteggio"],
|
|
531
617
|
},
|
|
532
|
-
rating: "Hai un punteggio?",
|
|
533
618
|
optional: "facoltativo",
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
619
|
+
privacy: "Leggiamo solo il tuo punteggio pubblico. Niente password.",
|
|
620
|
+
speeds: { rapid: "rapid", classical: "classica", blitz: "blitz" },
|
|
621
|
+
ratingTitle: (speed, played) => (played === undefined ? speed : `${speed} · ultima partita il ${played.split("-").reverse().join("/")}`),
|
|
622
|
+
notFound: (site) => `Su ${site} non trovo questo nome`,
|
|
623
|
+
unreached: (site) => `${site} non risponde: riproviamo più tardi`,
|
|
624
|
+
pending: "Leggo il tuo punteggio…",
|
|
538
625
|
start: "Inizia a giocare",
|
|
539
626
|
create: "Fatto",
|
|
540
627
|
newProfile: "Aggiungi un giocatore",
|
|
@@ -542,12 +629,17 @@ const italian = {
|
|
|
542
629
|
eachOwn: "Ognuno ha le sue partite e le sue impostazioni.",
|
|
543
630
|
changePlayer: "Cambia giocatore",
|
|
544
631
|
back: "Indietro",
|
|
545
|
-
|
|
546
|
-
|
|
632
|
+
yourProfile: "Il tuo profilo",
|
|
633
|
+
editProfile: "Modifica il profilo",
|
|
634
|
+
yourPicture: "La tua immagine: modifica il profilo",
|
|
547
635
|
avatars: { "short-hair": "Capelli corti", bun: "Chignon", glasses: "Occhiali", beanie: "Berretto", beard: "Barba", curls: "Ricci", moustache: "Baffi", "silver-bob": "Caschetto grigio", cat: "Gatto", owl: "Gufo" },
|
|
548
636
|
whilePlaying: "Finisci la partita, o abbandona, prima di cambiare giocatore.",
|
|
637
|
+
accounts: "Giochi su Lichess, Chess.com o nei tornei FIDE?",
|
|
549
638
|
username: "il tuo nome utente",
|
|
550
639
|
accountTitle: "Col tuo nome lì, una partita che carichi da lì sa qual è la tua parte.",
|
|
640
|
+
fidePlaceholder: "il tuo punteggio",
|
|
641
|
+
fideTitle: "Il tuo punteggio FIDE, come lo trovi sul sito della FIDE: Chesspal non può leggerlo da sé.",
|
|
642
|
+
fideRange: (lowest, highest) => `Un punteggio FIDE va da ${lowest} a ${highest}`,
|
|
551
643
|
badImage: "Questa immagine non si apre: prova con un'altra foto (quelle del telefono di solito vanno bene).",
|
|
552
644
|
},
|
|
553
645
|
start: {
|
|
@@ -683,6 +775,20 @@ const italian = {
|
|
|
683
775
|
control: "Quanti pezzi del Bianco e del Nero raggiungono la casa",
|
|
684
776
|
ends: { crown: "Ha vinto", mate: "Scacco matto", flag: "Ha abbandonato", half: "Patta, mezzo punto a testa" },
|
|
685
777
|
},
|
|
778
|
+
guide: {
|
|
779
|
+
title: "Guida",
|
|
780
|
+
contents: "Indice",
|
|
781
|
+
onGitHub: "Chesspal su GitHub",
|
|
782
|
+
onNpm: "Chesspal su npm",
|
|
783
|
+
support: "Sostieni Chesspal",
|
|
784
|
+
credits: "Crediti",
|
|
785
|
+
creditLines: [
|
|
786
|
+
"Le regole: [chess.js](https://github.com/jhlywa/chess.js), con licenza BSD 2-Clause.",
|
|
787
|
+
"I nomi delle aperture: [chess-openings](https://github.com/lichess-org/chess-openings) di Lichess, con licenza CC0.",
|
|
788
|
+
"I pezzi: Nitida, Torretta e Capablanca sono di Chesspal, l'ultimo dai diagrammi di *Chess Fundamentals* di Capablanca (1921), con licenza CC BY 4.0: [i loro crediti](https://github.com/stefanocaronia/chesspal/blob/main/src/page/pieces/CREDITS.md).",
|
|
789
|
+
"I suoni: di Chesspal.",
|
|
790
|
+
],
|
|
791
|
+
},
|
|
686
792
|
};
|
|
687
793
|
// How each piece moves, said when it was taken where it cannot go (Anatolij's words).
|
|
688
794
|
const englishMoves = {
|
|
@@ -745,6 +851,14 @@ function italianCulprit(by) {
|
|
|
745
851
|
function italianWith(preposition, letter) {
|
|
746
852
|
return italianThe[letter].replace(/^il /, `${preposition}l `).replace(/^la /, `${preposition}lla `).replace(/^l'/, `${preposition}ll'`);
|
|
747
853
|
}
|
|
854
|
+
/** A move in words: "knight from b takes d7, check", "cavallo in f3", "arrocco corto". */
|
|
855
|
+
function moveInWords(words, move) {
|
|
856
|
+
const promotion = "castles" in move || move.promotion === undefined ? undefined : `${words.promotes} ${words.names[move.promotion]}`;
|
|
857
|
+
const played = "castles" in move
|
|
858
|
+
? words.castles[move.castles]
|
|
859
|
+
: [words.names[move.piece], ...(move.from === undefined ? [] : [words.from, move.from]), move.takes ? words.takes : words.to, move.square].join(" ");
|
|
860
|
+
return [played, promotion, move.check === undefined ? undefined : words.check[move.check]].filter((part) => part !== undefined).join(", ");
|
|
861
|
+
}
|
|
748
862
|
/** The words with their first letter capital, to open a sentence. */
|
|
749
863
|
function capital(words) {
|
|
750
864
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
package/dist/page/theme.css
CHANGED
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
--help-on: #fff;
|
|
34
34
|
--help-line: rgba(17, 22, 29, 0.78);
|
|
35
35
|
--help-halo: rgba(255, 255, 255, 0.85);
|
|
36
|
+
/* The control map's veils (Zoe's variant A): grey over the board, sand where White alone reaches, slate where Black alone does */
|
|
37
|
+
--map-veil: rgba(160, 166, 176, 0.5);
|
|
38
|
+
--map-white: rgba(242, 233, 210, 0.78);
|
|
39
|
+
--map-black: rgba(56, 66, 84, 0.5);
|
|
36
40
|
--torretta-body: #6d9fd1;
|
|
37
41
|
--torretta-shade: #4b7cb0;
|
|
38
42
|
--torretta-screen: #16263a;
|
|
@@ -102,6 +106,9 @@
|
|
|
102
106
|
--badge-blunder: #d93434;
|
|
103
107
|
--shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 8px 22px rgba(0, 0, 0, 0.25);
|
|
104
108
|
--board-shadow: var(--shadow);
|
|
109
|
+
--map-veil: rgba(112, 120, 132, 0.66);
|
|
110
|
+
--map-white: rgba(226, 214, 188, 0.64);
|
|
111
|
+
--map-black: rgba(28, 35, 48, 0.7);
|
|
105
112
|
color-scheme: dark;
|
|
106
113
|
}
|
|
107
114
|
|
package/dist/profiles/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { type ImageType, imageTypeOf, imageTypes, ownImage, ownImageStamp, removeOwnImage, saveOwnImage, sentImageLimit } from "./avatar.ts";
|
|
2
|
-
export { type Accounts, activateProfile, activeProfile, createProfile, type DrawnAvatar, drawnAvatars, ensureProfile, gameFolder, gameFolders, type Level, levels, listProfiles, nameLimit, type PlayerProfile, type ProfileFields, profileFolder,
|
|
2
|
+
export { type Accounts, activateProfile, activeProfile, createProfile, type DrawnAvatar, drawnAvatars, ensureProfile, fideHighest, fideLowest, gameFolder, gameFolders, type Level, levels, listProfiles, nameLimit, type PlayerProfile, type ProfileFields, profileFolder, updateProfile, } from "./profiles.ts";
|
|
3
|
+
export { type Rating, type RatingSite, ratingSites, saveReadings, type SiteReading, type SiteReadings, type Speed, speeds } from "./ratings.ts";
|
|
3
4
|
export { helpsIn, languageIn, type PageSettings, profileSettings, saveProfileSettings, settingsOf } from "./settings.ts";
|
package/dist/profiles/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The player's profiles (D-23): who plays, their games, their picture and their page settings, each in profiles/<id>/ of the data folder (D-71). Below the
|
|
2
2
|
// commands, beside the games.
|
|
3
3
|
export { imageTypeOf, imageTypes, ownImage, ownImageStamp, removeOwnImage, saveOwnImage, sentImageLimit } from "./avatar.js";
|
|
4
|
-
export { activateProfile, activeProfile, createProfile, drawnAvatars, ensureProfile, gameFolder, gameFolders, levels, listProfiles, nameLimit, profileFolder,
|
|
4
|
+
export { activateProfile, activeProfile, createProfile, drawnAvatars, ensureProfile, fideHighest, fideLowest, gameFolder, gameFolders, levels, listProfiles, nameLimit, profileFolder, updateProfile, } from "./profiles.js";
|
|
5
|
+
export { ratingSites, saveReadings, speeds } from "./ratings.js";
|
|
5
6
|
export { helpsIn, languageIn, profileSettings, saveProfileSettings, settingsOf } from "./settings.js";
|
|
@@ -1,30 +1,21 @@
|
|
|
1
|
+
import { type Rating, type SiteReadings } from "./ratings.ts";
|
|
1
2
|
/** How the player says they play, from who has just learned to who plays tournaments. */
|
|
2
3
|
export declare const levels: readonly ["learning", "casual", "regular", "tournament"];
|
|
3
4
|
export type Level = (typeof levels)[number];
|
|
4
|
-
/** Where a rating comes from. */
|
|
5
|
-
export declare const ratingSites: readonly ["lichess", "chess.com", "fide"];
|
|
6
|
-
export type RatingSite = (typeof ratingSites)[number];
|
|
7
|
-
/** The lowest and highest rating a profile takes. */
|
|
8
|
-
export declare const ratingLimits: {
|
|
9
|
-
readonly lowest: 100;
|
|
10
|
-
readonly highest: 3500;
|
|
11
|
-
};
|
|
12
5
|
/** The longest name a profile keeps. */
|
|
13
6
|
export declare const nameLimit = 40;
|
|
14
7
|
/** The avatars Zoe drew, for a player without an image of their own (D-45). */
|
|
15
8
|
export declare const drawnAvatars: readonly ["short-hair", "bun", "glasses", "beanie", "beard", "curls", "moustache", "silver-bob", "cat", "owl"];
|
|
16
9
|
export type DrawnAvatar = (typeof drawnAvatars)[number];
|
|
17
|
-
/**
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
/** What the player tells of themselves: their name, how they play, their rating, the avatar they chose. */
|
|
10
|
+
/** The FIDE ratings Chesspal takes: whole numbers from FIDE's lowest, 1400 since 2024, to 3000. */
|
|
11
|
+
export declare const fideLowest = 1400;
|
|
12
|
+
export declare const fideHighest = 3000;
|
|
13
|
+
/** What the player tells of themselves: their name, how they play, the avatar they chose, their FIDE rating, typed since FIDE has no API (D-106). */
|
|
23
14
|
export interface ProfileFields {
|
|
24
15
|
readonly name: string;
|
|
25
16
|
readonly level?: Level;
|
|
26
|
-
readonly rating?: Rating;
|
|
27
17
|
readonly avatar?: DrawnAvatar;
|
|
18
|
+
readonly fide?: number;
|
|
28
19
|
}
|
|
29
20
|
/** The player's names on the sites they play on. */
|
|
30
21
|
export interface Accounts {
|
|
@@ -38,6 +29,10 @@ export interface PlayerProfile extends ProfileFields {
|
|
|
38
29
|
readonly new?: true;
|
|
39
30
|
/** The player's names on Lichess and chess.com, written in profile.md by hand or from the page, by which the games they load are theirs (D-93). */
|
|
40
31
|
readonly accounts?: Accounts;
|
|
32
|
+
/** What Lichess and chess.com said of those names, the last time Chesspal asked (D-105). */
|
|
33
|
+
readonly sites?: SiteReadings;
|
|
34
|
+
/** The rating that counts, of the names the profile has now: of two, the one the sites are surer of (D-106). */
|
|
35
|
+
readonly rating?: Rating;
|
|
41
36
|
}
|
|
42
37
|
/** What may change in a profile: what the player tells of themselves, and their names on the sites, each one written whole. */
|
|
43
38
|
export type ProfileChanges = Partial<ProfileFields> & {
|
|
@@ -4,16 +4,16 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:f
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { makeFolder, renameOver, writeAtomically } from "../games/index.js";
|
|
6
6
|
import { readFrontMatter, writeFrontMatter } from "./front-matter.js";
|
|
7
|
+
import { savedReadings } from "./ratings.js";
|
|
7
8
|
/** How the player says they play, from who has just learned to who plays tournaments. */
|
|
8
9
|
export const levels = ["learning", "casual", "regular", "tournament"];
|
|
9
|
-
/** Where a rating comes from. */
|
|
10
|
-
export const ratingSites = ["lichess", "chess.com", "fide"];
|
|
11
|
-
/** The lowest and highest rating a profile takes. */
|
|
12
|
-
export const ratingLimits = { lowest: 100, highest: 3500 };
|
|
13
10
|
/** The longest name a profile keeps. */
|
|
14
11
|
export const nameLimit = 40;
|
|
15
12
|
/** The avatars Zoe drew, for a player without an image of their own (D-45). */
|
|
16
13
|
export const drawnAvatars = ["short-hair", "bun", "glasses", "beanie", "beard", "curls", "moustache", "silver-bob", "cat", "owl"];
|
|
14
|
+
/** The FIDE ratings Chesspal takes: whole numbers from FIDE's lowest, 1400 since 2024, to 3000. */
|
|
15
|
+
export const fideLowest = 1400;
|
|
16
|
+
export const fideHighest = 3000;
|
|
17
17
|
/** The profiles in the data folder, by their folders' names. */
|
|
18
18
|
export function listProfiles(root) {
|
|
19
19
|
return profileIds(root).map((id) => readProfile(root, id));
|
|
@@ -47,9 +47,9 @@ export function profileFolder(root, id) {
|
|
|
47
47
|
export function createProfile(root, fields, use = true) {
|
|
48
48
|
const id = activeId(root);
|
|
49
49
|
if (id !== undefined && readProfile(root, id).new === true) {
|
|
50
|
-
const { level,
|
|
50
|
+
const { level, avatar, fide } = fields;
|
|
51
51
|
const named = renamed(root, id, fields.name);
|
|
52
|
-
writeProfile(root, named, { name: fields.name, level,
|
|
52
|
+
writeProfile(root, named, { name: fields.name, level, avatar, fide, new: false });
|
|
53
53
|
return readProfile(root, named);
|
|
54
54
|
}
|
|
55
55
|
let made = freeId(root, slugOf(fields.name));
|
|
@@ -172,22 +172,38 @@ function writeActive(root, id) {
|
|
|
172
172
|
}
|
|
173
173
|
function readProfile(root, id) {
|
|
174
174
|
const { fields } = readFrontMatter(profileText(root, id));
|
|
175
|
-
const value = Number(fields.rating);
|
|
176
|
-
const site = oneOf(ratingSites, fields.site);
|
|
177
175
|
const level = oneOf(levels, fields.level);
|
|
178
176
|
const avatar = oneOf(drawnAvatars, fields.avatar);
|
|
179
|
-
const
|
|
177
|
+
const fide = fideIn(fields.fide);
|
|
178
|
+
const { accounts } = accountsIn(fields);
|
|
179
|
+
const sites = savedReadings(profileFolder(root, id));
|
|
180
|
+
const rating = sites === undefined ? undefined : surest([ofAccount(sites.lichess, accounts?.lichess), ofAccount(sites.chesscom, accounts?.chesscom)]);
|
|
180
181
|
// A name written by hand is cut as the page and the CLI cut it: the AI reads it.
|
|
181
182
|
return {
|
|
182
183
|
id,
|
|
183
184
|
name: fields.name?.trim().slice(0, nameLimit).trim() || id,
|
|
184
185
|
...(level === undefined ? {} : { level }),
|
|
185
|
-
...(rated ? { rating: { value, site } } : {}),
|
|
186
186
|
...(avatar === undefined ? {} : { avatar }),
|
|
187
|
+
...(fide === undefined ? {} : { fide }),
|
|
187
188
|
...(fields.new === "true" ? { new: true } : {}),
|
|
188
|
-
...
|
|
189
|
+
...(accounts === undefined ? {} : { accounts }),
|
|
190
|
+
...(sites === undefined ? {} : { sites }),
|
|
191
|
+
...(rating === undefined ? {} : { rating }),
|
|
189
192
|
};
|
|
190
193
|
}
|
|
194
|
+
/** A site's rating, when it was read for the name the profile has there now. */
|
|
195
|
+
function ofAccount(reading, account) {
|
|
196
|
+
return account !== undefined && reading?.account.toLowerCase() === account.toLowerCase() ? reading.rating : undefined;
|
|
197
|
+
}
|
|
198
|
+
/** Of the ratings, the one its site is surest of: the lowest RD (D-106). */
|
|
199
|
+
function surest(ratings) {
|
|
200
|
+
return ratings.reduce((best, one) => (one !== undefined && (best === undefined || one.deviation < best.deviation) ? one : best), undefined);
|
|
201
|
+
}
|
|
202
|
+
/** The FIDE rating of profile.md's fide line: none when it is not a whole number in FIDE's range, as one written by hand may not be. */
|
|
203
|
+
function fideIn(text) {
|
|
204
|
+
const fide = Number(text?.trim());
|
|
205
|
+
return Number.isInteger(fide) && fide >= fideLowest && fide <= fideHighest ? fide : undefined;
|
|
206
|
+
}
|
|
191
207
|
/** The player's names on the sites, from profile.md's lichess and chesscom lines; none without either. */
|
|
192
208
|
function accountsIn(fields) {
|
|
193
209
|
const named = (site) => fields[site]?.trim().slice(0, nameLimit).trim() || undefined;
|
|
@@ -208,13 +224,12 @@ function writeProfile(root, id, changes) {
|
|
|
208
224
|
if ("level" in changes) {
|
|
209
225
|
written.level = changes.level;
|
|
210
226
|
}
|
|
211
|
-
if ("rating" in changes) {
|
|
212
|
-
written.rating = changes.rating?.value;
|
|
213
|
-
written.site = changes.rating?.site;
|
|
214
|
-
}
|
|
215
227
|
if ("avatar" in changes) {
|
|
216
228
|
written.avatar = changes.avatar;
|
|
217
229
|
}
|
|
230
|
+
if ("fide" in changes) {
|
|
231
|
+
written.fide = changes.fide;
|
|
232
|
+
}
|
|
218
233
|
if ("accounts" in changes) {
|
|
219
234
|
written.lichess = changes.accounts?.lichess;
|
|
220
235
|
written.chesscom = changes.accounts?.chesscom;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** The sites a rating is read from, by the player's names there (D-105). */
|
|
2
|
+
export declare const ratingSites: readonly ["lichess", "chess.com"];
|
|
3
|
+
export type RatingSite = (typeof ratingSites)[number];
|
|
4
|
+
/** The speeds whose rating counts, in the order they count: the games with the AI are like a slow rapid (D-106). */
|
|
5
|
+
export declare const speeds: readonly ["rapid", "classical", "blitz"];
|
|
6
|
+
export type Speed = (typeof speeds)[number];
|
|
7
|
+
/** A rating a site gives: the number as it is, never converted, its speed, how sure the site is of it (its rating deviation, RD), and the day of the last game at that speed. */
|
|
8
|
+
export interface Rating {
|
|
9
|
+
readonly value: number;
|
|
10
|
+
readonly site: RatingSite;
|
|
11
|
+
readonly speed: Speed;
|
|
12
|
+
readonly deviation: number;
|
|
13
|
+
/** YYYY-MM-DD. */
|
|
14
|
+
readonly played?: string;
|
|
15
|
+
}
|
|
16
|
+
/** What a site said of the player's name there, the last time Chesspal asked. */
|
|
17
|
+
export interface SiteReading {
|
|
18
|
+
readonly account: string;
|
|
19
|
+
/** False when the site has no player of that name; unknown while it has never answered. */
|
|
20
|
+
readonly found?: boolean;
|
|
21
|
+
/** The rating that counts there: none when the player has no established rating at a speed that counts. */
|
|
22
|
+
readonly rating?: Rating;
|
|
23
|
+
/** The last try did not reach the site: what it said before stays. */
|
|
24
|
+
readonly unreached?: true;
|
|
25
|
+
}
|
|
26
|
+
/** What the sites said, and when Chesspal last asked them. */
|
|
27
|
+
export interface SiteReadings {
|
|
28
|
+
readonly read: string;
|
|
29
|
+
readonly lichess?: SiteReading;
|
|
30
|
+
readonly chesscom?: SiteReading;
|
|
31
|
+
}
|
|
32
|
+
/** The readings kept in a profile's folder; undefined when it has none, or they cannot be read. */
|
|
33
|
+
export declare function savedReadings(folder: string): SiteReadings | undefined;
|
|
34
|
+
/** Keeps the readings in a profile's folder, over the ones before; none removes them. */
|
|
35
|
+
export declare function saveReadings(folder: string, readings: SiteReadings | undefined): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// What Lichess and Chess.com said of the player's names there (D-105, D-106): ratings.json in the profile's folder, written by Chesspal each time it reads
|
|
2
|
+
// them, never by hand; the rating that counts is chosen from it as the profile is read.
|
|
3
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { writeAtomically } from "../games/index.js";
|
|
6
|
+
/** The sites a rating is read from, by the player's names there (D-105). */
|
|
7
|
+
export const ratingSites = ["lichess", "chess.com"];
|
|
8
|
+
/** The speeds whose rating counts, in the order they count: the games with the AI are like a slow rapid (D-106). */
|
|
9
|
+
export const speeds = ["rapid", "classical", "blitz"];
|
|
10
|
+
/** The readings kept in a profile's folder; undefined when it has none, or they cannot be read. */
|
|
11
|
+
export function savedReadings(folder) {
|
|
12
|
+
try {
|
|
13
|
+
return readingsOf(JSON.parse(readFileSync(join(folder, fileName), "utf8")));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Keeps the readings in a profile's folder, over the ones before; none removes them. */
|
|
20
|
+
export function saveReadings(folder, readings) {
|
|
21
|
+
if (readings === undefined) {
|
|
22
|
+
rmSync(join(folder, fileName), { force: true });
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
writeAtomically(join(folder, fileName), `${JSON.stringify(readings, null, 2)}\n`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const fileName = "ratings.json";
|
|
29
|
+
// Readings as the file has them, each part checked: a file edited by hand never gives the AI a rating that is not one.
|
|
30
|
+
function readingsOf(value) {
|
|
31
|
+
const file = value;
|
|
32
|
+
if (typeof file?.read !== "string" || Number.isNaN(Date.parse(file.read))) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const lichess = readingOf(file.lichess, "lichess");
|
|
36
|
+
const chesscom = readingOf(file.chesscom, "chess.com");
|
|
37
|
+
return { read: file.read, ...(lichess === undefined ? {} : { lichess }), ...(chesscom === undefined ? {} : { chesscom }) };
|
|
38
|
+
}
|
|
39
|
+
function readingOf(value, site) {
|
|
40
|
+
const reading = value;
|
|
41
|
+
if (typeof reading?.account !== "string" || reading.account === "") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const rating = ratingOf(reading.rating, site);
|
|
45
|
+
return {
|
|
46
|
+
account: reading.account,
|
|
47
|
+
...(typeof reading.found === "boolean" ? { found: reading.found } : {}),
|
|
48
|
+
...(rating === undefined ? {} : { rating }),
|
|
49
|
+
...(reading.unreached === true ? { unreached: true } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function ratingOf(value, site) {
|
|
53
|
+
const rating = value;
|
|
54
|
+
const speed = speeds.find((one) => one === rating?.speed);
|
|
55
|
+
const plausible = (number) => typeof number === "number" && Number.isInteger(number) && number > 0 && number < 4000;
|
|
56
|
+
if (rating?.site !== site || speed === undefined || !plausible(rating.value) || !plausible(rating.deviation)) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
const played = typeof rating.played === "string" && /^\d{4}-\d{2}-\d{2}$/.test(rating.played) ? rating.played : undefined;
|
|
60
|
+
return { value: rating.value, site, speed, deviation: rating.deviation, ...(played === undefined ? {} : { played }) };
|
|
61
|
+
}
|