chesspal 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/commands/analysis/analyse.js +7 -2
- package/dist/commands/analysis/annotate.js +6 -2
- package/dist/commands/answers/agents.d.ts +2 -0
- package/dist/commands/answers/agents.js +5 -0
- package/dist/commands/answers/index.d.ts +2 -2
- package/dist/commands/answers/index.js +2 -2
- package/dist/commands/answers/lines.js +3 -1
- package/dist/commands/answers/say.d.ts +7 -1
- package/dist/commands/answers/say.js +25 -7
- package/dist/commands/answers/study.d.ts +27 -5
- package/dist/commands/answers/study.js +121 -10
- package/dist/commands/game/look.d.ts +1 -0
- package/dist/commands/game/look.js +8 -8
- package/dist/commands/game/position.d.ts +1 -0
- package/dist/commands/game/position.js +5 -5
- package/dist/commands/index.d.ts +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/moods.d.ts +2 -0
- package/dist/core/moods.js +11 -2
- package/dist/games/analysis.d.ts +11 -0
- package/dist/games/analysis.js +23 -0
- package/dist/games/archive.d.ts +9 -1
- package/dist/games/archive.js +18 -1
- package/dist/games/current-game.d.ts +23 -1
- package/dist/games/current-game.js +6 -1
- package/dist/games/index.d.ts +3 -3
- package/dist/games/index.js +2 -2
- package/dist/page/app/app.js +38 -6
- package/dist/page/game/column.js +3 -2
- package/dist/page/game/game.css +34 -5
- package/dist/page/game/move-list.js +3 -3
- package/dist/page/table/window.js +8 -5
- package/dist/page/talk/chat.css +8 -0
- package/dist/page/talk/talk.js +20 -2
- package/dist/page/text.js +20 -2
- package/dist/server/bell/bell.d.ts +5 -2
- package/dist/server/bell/bell.js +36 -12
- package/dist/server/page/game.js +34 -4
- package/dist/server/page/page.js +1 -1
- package/dist/server/page/table.js +2 -2
- package/instructions/analysis.md +2 -2
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
package/dist/server/bell/bell.js
CHANGED
|
@@ -23,6 +23,8 @@ export class Bell {
|
|
|
23
23
|
woken = false;
|
|
24
24
|
// A hook has waited for the bell since the chat came: its host runs Chesspal's hooks, and only they tell where a turn ends.
|
|
25
25
|
hooked = false;
|
|
26
|
+
// The chat's last hook went without the bell, or a ring found it stopped in its turn: it is out of its turn, and nobody hears the player.
|
|
27
|
+
unheard = false;
|
|
26
28
|
// Looks again, while a ring waits for a chat in a turn, whether the chat was stopped meanwhile.
|
|
27
29
|
stopWatch;
|
|
28
30
|
news;
|
|
@@ -46,6 +48,10 @@ export class Bell {
|
|
|
46
48
|
get woke() {
|
|
47
49
|
return this.woken;
|
|
48
50
|
}
|
|
51
|
+
/** Whether the chat is out of its turn and hears nobody: its hook went without the bell, or it was stopped; until it calls a tool or a hook waits again. */
|
|
52
|
+
get deaf() {
|
|
53
|
+
return this.unheard;
|
|
54
|
+
}
|
|
49
55
|
/** Whether the chat hears the player: in a turn, or waiting for the bell with a hook. */
|
|
50
56
|
get listening() {
|
|
51
57
|
return this.busy || this.listener !== undefined;
|
|
@@ -54,6 +60,7 @@ export class Bell {
|
|
|
54
60
|
chatCalls() {
|
|
55
61
|
const asleep = !this.listening;
|
|
56
62
|
this.busy = true;
|
|
63
|
+
this.unheard = false;
|
|
57
64
|
this.tellIf(asleep);
|
|
58
65
|
return asleep;
|
|
59
66
|
}
|
|
@@ -63,7 +70,11 @@ export class Bell {
|
|
|
63
70
|
* stays unheard until its first tool, which starts the AI's clock, held till then: the message moves no clock.
|
|
64
71
|
*/
|
|
65
72
|
chatPrompted() {
|
|
73
|
+
// A chat nobody heard is in its turn now, though still unheard.
|
|
66
74
|
if (this.listener === undefined) {
|
|
75
|
+
const deaf = this.unheard;
|
|
76
|
+
this.unheard = false;
|
|
77
|
+
this.tellIf(deaf);
|
|
67
78
|
return;
|
|
68
79
|
}
|
|
69
80
|
this.listener.end("dismissed");
|
|
@@ -101,6 +112,7 @@ export class Bell {
|
|
|
101
112
|
listen(request, response, told = false) {
|
|
102
113
|
response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" });
|
|
103
114
|
this.hooked = true;
|
|
115
|
+
this.unheard = false;
|
|
104
116
|
const missed = this.pending && !told;
|
|
105
117
|
this.pending = false;
|
|
106
118
|
if (missed && this.news()) {
|
|
@@ -117,9 +129,11 @@ export class Bell {
|
|
|
117
129
|
this.changed();
|
|
118
130
|
request.on("close", () => {
|
|
119
131
|
if (this.listener === response) {
|
|
120
|
-
// The hook went without the bell (the chat stopped, cleared or closed): nobody hears the player until the chat calls
|
|
132
|
+
// The hook went without the bell (the chat stopped, cleared or closed, or its host ended the hook): nobody hears the player until the chat calls
|
|
133
|
+
// a tool again, but the chat is out of its turn.
|
|
121
134
|
this.listener = undefined;
|
|
122
|
-
this.
|
|
135
|
+
this.unheard = !this.busy;
|
|
136
|
+
this.tellIf(this.unheard);
|
|
123
137
|
}
|
|
124
138
|
});
|
|
125
139
|
}
|
|
@@ -128,6 +142,7 @@ export class Bell {
|
|
|
128
142
|
this.hooked = false;
|
|
129
143
|
this.busy = false;
|
|
130
144
|
this.woken = false;
|
|
145
|
+
this.unheard = false;
|
|
131
146
|
this.pending = false;
|
|
132
147
|
this.listener?.end("dismissed");
|
|
133
148
|
this.listener = undefined;
|
|
@@ -146,6 +161,7 @@ export class Bell {
|
|
|
146
161
|
if (this.stopped()) {
|
|
147
162
|
this.busy = false;
|
|
148
163
|
this.woken = false;
|
|
164
|
+
this.unheard = true;
|
|
149
165
|
this.changed();
|
|
150
166
|
return;
|
|
151
167
|
}
|
|
@@ -199,15 +215,23 @@ export async function listen(host = "claude") {
|
|
|
199
215
|
for await (const chunk of process.stdin) {
|
|
200
216
|
input += String(chunk);
|
|
201
217
|
}
|
|
218
|
+
// Antigravity's hook leaves a line as it starts: a chat its bell never woke shows whether the hook ran at all.
|
|
219
|
+
if (host === "antigravity") {
|
|
220
|
+
noteHook(chatFolder(hookInput(input)), "waiting");
|
|
221
|
+
}
|
|
202
222
|
const conversation = host === "antigravity" ? hookInput(input).session : undefined;
|
|
203
223
|
const turn = conversation === undefined ? undefined : watchNewTurn(conversation);
|
|
204
224
|
const waited = bellHeard(input).catch((error) => ({ folder: chatFolder(hookInput(input)), rang: false, why: `failed: ${error.message}` }));
|
|
205
225
|
const newTurn = turn?.started.then(() => ({ folder: chatFolder(hookInput(input)), rang: false, why: "the player started a new turn" }));
|
|
206
226
|
const heard = await (newTurn === undefined ? waited : Promise.race([waited, newTurn]));
|
|
207
227
|
turn?.stop();
|
|
208
|
-
// Codex's turn ends with a hook that heard no bell, and the player's next move on the page wakes nobody: the log keeps why.
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
// Codex's turn ends with a hook that heard no bell, and the player's next move on the page wakes nobody: the log keeps why. Antigravity's keeps every
|
|
229
|
+
// end, the bell too, since its chat may not wake even then.
|
|
230
|
+
if (host === "antigravity") {
|
|
231
|
+
noteHook(heard.folder, heard.rang ? "rang" : heard.why);
|
|
232
|
+
}
|
|
233
|
+
else if (host === "codex" && !heard.rang) {
|
|
234
|
+
noteHook(heard.folder, heard.why);
|
|
211
235
|
}
|
|
212
236
|
// It ends as soon as it knows: the chat hears the hook only once it ends.
|
|
213
237
|
const answer = hookAnswer(host, heard.rang);
|
|
@@ -271,15 +295,15 @@ async function forgetGonePages(home, folder, kept, wait) {
|
|
|
271
295
|
}
|
|
272
296
|
}));
|
|
273
297
|
}
|
|
274
|
-
// The log of
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
/** Adds a line to the log of the hooks
|
|
278
|
-
export function
|
|
279
|
-
const path = join(folder,
|
|
298
|
+
// The log of the doorbell's hooks, in the chat's folder: Codex's that heard no bell, Antigravity's as they start and end; its last lines only.
|
|
299
|
+
const hookLog = "listen.log";
|
|
300
|
+
const hookLines = 100;
|
|
301
|
+
/** Adds a line to the log of the doorbell's hooks, keeping its last lines; a log that cannot be written never stops the hook. */
|
|
302
|
+
export function noteHook(folder, why, now = new Date()) {
|
|
303
|
+
const path = join(folder, hookLog);
|
|
280
304
|
try {
|
|
281
305
|
const before = existsSync(path) ? readFileSync(path, "utf8").split("\n").filter((line) => line !== "") : [];
|
|
282
|
-
writeAtomically(path, `${[...before, `${now.toISOString()} ${why}`].slice(-
|
|
306
|
+
writeAtomically(path, `${[...before, `${now.toISOString()} ${why}`].slice(-hookLines).join("\n")}\n`);
|
|
283
307
|
}
|
|
284
308
|
catch {
|
|
285
309
|
// The log only explains; the hook ends the same.
|
package/dist/server/page/game.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { analyseGame, answerDraw, bookPlies, ChatNotClear, DrawRefused, endGame, gameAt, gameToStudy, namedGame, newGame, offerDraw, playMove, samePosition, say, shownPosition, startNewGame, } from "../../commands/index.js";
|
|
2
2
|
import { clockStarted, isReviewed, writePgn } from "../../core/index.js";
|
|
3
|
-
import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, GameMissing, heardLines, loadGame, loadTableTalk, playerPgn, ponderingUntil, rerunIfChanged, saveGame, startAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
|
|
3
|
+
import { aiModelOf, archiveName, coachShown, coachWords, drawOfferOf, endOf, gameInAnalysis, GameMissing, heardLines, loadGame, loadTableTalk, playerPgn, ponderingUntil, rerunIfChanged, saveGame, startAnalysis, stopAnalysis, talkLimits, thoughtKey, wakesTheChat, WordsRefused, } from "../../games/index.js";
|
|
4
4
|
import { gameFolder, helpsIn } from "../../profiles/index.js";
|
|
5
5
|
import { version } from "../../version.js";
|
|
6
6
|
import { asTheChatSees } from "./chat.js";
|
|
@@ -55,6 +55,7 @@ const routes = {
|
|
|
55
55
|
"POST /api/resign": resignFromPage,
|
|
56
56
|
"POST /api/start": startFromPage,
|
|
57
57
|
"POST /api/study": studyFromPage,
|
|
58
|
+
"POST /api/stop-analysis": stopAnalysisFromPage,
|
|
58
59
|
};
|
|
59
60
|
/**
|
|
60
61
|
* The player's move from the page, with their words when they wrote some: played as player_move plays it (D-47) and saved with the words, refused in the
|
|
@@ -98,7 +99,7 @@ async function moveFromPage(request, response, served) {
|
|
|
98
99
|
}
|
|
99
100
|
/**
|
|
100
101
|
* The player's words from the page, without a move: saved with the game as player_say saves them, refused in the player's words, the doorbell rung for the
|
|
101
|
-
* first; in analysis with the position the page shows, and the doorbell rung for each (D-101).
|
|
102
|
+
* first; in analysis with the position the page shows and the variation played there (D-116), and the doorbell rung for each (D-101).
|
|
102
103
|
*/
|
|
103
104
|
async function messageFromPage(request, response, served) {
|
|
104
105
|
if (!isJson(request)) {
|
|
@@ -107,11 +108,19 @@ async function messageFromPage(request, response, served) {
|
|
|
107
108
|
}
|
|
108
109
|
const body = jsonOf(await bodyOf(request));
|
|
109
110
|
const text = textOf(body?.text);
|
|
110
|
-
const
|
|
111
|
-
if (text === undefined) {
|
|
111
|
+
const variation = variationOf(body?.variation);
|
|
112
|
+
if (text === undefined || variation === null) {
|
|
112
113
|
send(response, 400, json, JSON.stringify({ error: `Send the message as JSON, ${talkLimits.player} characters at most: {"text": "…"}.` }));
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
116
|
+
let shown;
|
|
117
|
+
try {
|
|
118
|
+
shown = typeof body?.game === "string" && typeof body.at === "number" ? shownPosition(served.context, body.game, body.at, variation) : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
send(response, 400, json, JSON.stringify({ error: error.message }));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
115
124
|
let current;
|
|
116
125
|
try {
|
|
117
126
|
current = rerunIfChanged(() => say(gameFolder(served.context.home()), "player", text, served.context.now(), undefined, shown));
|
|
@@ -318,6 +327,18 @@ async function studyFromPage(request, response, served) {
|
|
|
318
327
|
}
|
|
319
328
|
send(response, 200, json, JSON.stringify({ kept: true, reviewed }));
|
|
320
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* The player stops the analysis they asked for, while the AI is at it or meant to be: the page goes back to the game over, with its review as far as it got,
|
|
332
|
+
* and the chat is no longer asked to go over it; nothing rings, as there is nothing for the chat to do. Only the game in analysis is stopped.
|
|
333
|
+
*/
|
|
334
|
+
async function stopAnalysisFromPage(request, response, served) {
|
|
335
|
+
const game = isJson(request) ? textOf(jsonOf(await bodyOf(request))?.game) : undefined;
|
|
336
|
+
if (game === undefined) {
|
|
337
|
+
send(response, 400, json, JSON.stringify({ error: 'Send the game in analysis as JSON: {"game": "games/2026-11-02-153012.pgn"}.' }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
send(response, 200, json, JSON.stringify({ stopped: stopAnalysis(gameFolder(served.context.home()), game) }));
|
|
341
|
+
}
|
|
321
342
|
/**
|
|
322
343
|
* The game in progress or just ended, as the player may see it: its PGN without the AI's plans while it goes on, the AI's side, when it started, and the
|
|
323
344
|
* talk on the page, each line of the player's with how far it got (D-53), and the draw offer that stands (D-65); the AI's name and model (D-82); and the
|
|
@@ -514,3 +535,12 @@ function gameNow(served) {
|
|
|
514
535
|
function aiHeld(served, current) {
|
|
515
536
|
return !served.bell.listening && current.clock !== undefined && endOf(current.game) === undefined && current.game.position.turn === current.ai;
|
|
516
537
|
}
|
|
538
|
+
/** The variation sent with the player's words (D-116): none when there is none, null when it is not one. */
|
|
539
|
+
function variationOf(value) {
|
|
540
|
+
if (value === undefined) {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
const { at, moves, cursor } = (typeof value === "object" && value !== null ? value : {});
|
|
544
|
+
const sans = Array.isArray(moves) && moves.every((move) => typeof move === "string") ? moves : undefined;
|
|
545
|
+
return typeof at === "number" && typeof cursor === "number" && sans !== undefined ? { at, moves: sans, cursor } : null;
|
|
546
|
+
}
|
package/dist/server/page/page.js
CHANGED
|
@@ -142,7 +142,7 @@ function answer(request, response, served) {
|
|
|
142
142
|
// The API answers GET alone: a HEAD at the doorbell would wait there as a hook does.
|
|
143
143
|
if (request.method !== "GET" && (request.method !== "HEAD" || path.startsWith("/api/"))) {
|
|
144
144
|
send(response, 405, "text/plain; charset=utf-8", "The page only reads, moves with POST /api/move, writes with POST /api/message, starts a game with POST /api/new and its clocks with POST /api/start, "
|
|
145
|
-
+ "offers or answers a draw with POST /api/draw, resigns with POST /api/resign, brings a game to analyse or opens one again with POST /api/study, deletes a game or its review with POST /api/delete-game and /api/delete-review, and keeps the profiles with POST /api/profile/… and /api/settings.");
|
|
145
|
+
+ "offers or answers a draw with POST /api/draw, resigns with POST /api/resign, brings a game to analyse or opens one again with POST /api/study, stops the analysis asked with POST /api/stop-analysis, deletes a game or its review with POST /api/delete-game and /api/delete-review, and keeps the profiles with POST /api/profile/… and /api/settings.");
|
|
146
146
|
return;
|
|
147
147
|
}
|
|
148
148
|
if (path === "/") {
|
|
@@ -311,10 +311,10 @@ function heldChat(served) {
|
|
|
311
311
|
const held = attached === undefined ? undefined : served.table?.chats?.held(attached.chat.folder);
|
|
312
312
|
return held === undefined || held.seat !== attached?.chat.seat ? undefined : { model: held.model, effort: held.effort };
|
|
313
313
|
}
|
|
314
|
-
// The chat
|
|
314
|
+
// The chat is out of a turn, waiting for the doorbell or deaf to it, and no analyst of it is at work: starting it again loses nothing.
|
|
315
315
|
function chatAtRest(served) {
|
|
316
316
|
const { bell } = served;
|
|
317
|
-
return bell.listening && !bell.inTurn && !analystAtWork(served);
|
|
317
|
+
return (bell.listening || bell.deaf) && !bell.inTurn && !analystAtWork(served);
|
|
318
318
|
}
|
|
319
319
|
/**
|
|
320
320
|
* Whether the chat's analyst is at work, on `game` when named: one whose chat started again since it took its game, or that has written nothing for half an
|
package/instructions/analysis.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
- Each line of the analysis is a move and, when they happened: the piece it took; the time it took ("spent") and the time left; who is ahead in material, when that changed; "at risk" after the move, the pieces of either side attacked and undefended, attacked by a cheaper piece, or attacked more times than defended ("outnumbered"), whichever move left them so, written when it changed: a line without it leaves the same pieces at risk as before, "none" when no piece is left at risk; "one legal move", the only move the side to move then had; "mate in one", the mates the side to move then has; "out of book", the first move after which no line of Lichess's openings file reaches the position, in any order of moves: the file names openings, it is not all theory; a draw offered; "ahead", who was estimated ahead after it, with `annotate_game` or by the coach during the game.
|
|
2
2
|
- In a game you played, "you said" and "your plan" are what you said and thought then, right or wrong: where they disagree with the facts, the facts are what was on the board.
|
|
3
|
-
- To see a position closer, `look` with the move, and the game as the analysis names it, gives every piece's attackers and defenders, the pins, the checks and the legal moves; `position` with the same gives the positional facts. Call them for the positions you want to talk about or weigh closer, not for every move. In look, attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now.
|
|
4
|
-
- Never invent a move, a position or a result. The tools give only the positions the game reached: a move
|
|
3
|
+
- To see a position closer, `look` with the move, and the game as the analysis names it, gives every piece's attackers and defenders, the pins, the checks and the legal moves; `position` with the same gives the positional facts. With `line`, a variation from there (`12...Kf8 13.Nxe5`), both give the same of the position after it, or the first of its moves that cannot be played. Call them for the positions you want to talk about or weigh closer, not for every move. In look, attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now.
|
|
4
|
+
- Never invent a move, a position or a result. The tools give only the positions the game reached, and those after a `line` you give them: a move the game did not play is your idea or the player's, say it as such; `look` on its position shows whether it was legal, not what the other side would answer.
|
package/instructions/opponent.md
CHANGED
|
@@ -10,6 +10,6 @@ You choose your moves, explain them briefly and talk with the player.
|
|
|
10
10
|
- You have a face on the board page, and it shows how you feel. Give it as the `mood` of `ai_move` on each move where the game turns for you: a piece won or lost, a blunder seen, your own or theirs, a threat that appears or a plan that works or fails. Let it colour your words while it lasts. When the game ends on the player's move, give how you feel as it ends with `ai_say`'s `mood`.
|
|
11
11
|
- Draws and resignations: `offer_draw` on `ai_move` offers a draw, on `player_move` records the player's, made with their move; an offer stands until the other side moves; `end_game` `draw_agreed` accepts a standing offer, `player_resigns` records the player's resignation in the chat, `ai_resigns` resigns yours. On the board page the player offers, answers and resigns there.
|
|
12
12
|
- When they ask for a game, call `new_game` without choices: the board page's New game window asks the sides, the clock and the level; ask them in the chat only when `new_game` says there is no board page.
|
|
13
|
-
- To go over a game with the player once no game goes on, the one just over or one they bring as a PGN, or one of their latest on Lichess or Chess.com, which `site_games` lists, give it to `analyse_game`: it reads the PGN for you; where Chesspal's answers say to start the analyst agent for it, start that instead, each time those answers say so and never on your own, not even when the player asks for the analysis again. While it works, and after, talk the game over whenever the player asks: `look` and `position` with game and move show you any of its positions. When they show you something the review missed, first correct that move's note with `annotate_game` {game, notes: [{move, badge, note}]}, that move only, then answer. Words from the board page you answer with `ai_say`; words in the chat, in the chat. While a game goes on, go over no game, not even in your head or from a PGN they paste: tell them it waits until this one ends.
|
|
13
|
+
- To go over a game with the player once no game goes on, the one just over or one they bring as a PGN, or one of their latest on Lichess or Chess.com, which `site_games` lists, give it to `analyse_game`: it reads the PGN for you; where Chesspal's answers say to start the analyst agent for it, start that instead, each time those answers say so and never on your own, not even when the player asks for the analysis again. While it works, and after, talk the game over whenever the player asks: `look` and `position` with game and move show you any of its positions, and with `line` the position after a variation. When they show you something the review missed, first correct that move's note with `annotate_game` {game, notes: [{move, badge, note}]}, that move only, then answer. Words from the board page you answer with `ai_say`; words in the chat, in the chat. While a game goes on, go over no game, not even in your head or from a PGN they paste: tell them it waits until this one ends.
|
|
14
14
|
- How to read the facts: attackers and defenders count only the pieces that reach the square directly, and a piece in brackets stands behind them; "Loose" means neither attacked nor defended, not attacked now; "At risk" lists the pieces attacked and undefended, attacked by a cheaper piece, or attacked more times than defended.
|
|
15
15
|
- Talk with the player in their language, in a few words. The facts and the tools are for you: tell the player what a chess player would say, never the lists or the tools' names. Name pieces and moves in words, as people talk at the board ("your bishop took on f7"); when you write a move, yours or the player's, write it with the pieces' figurines, as the tools show it (♞f6 for Nf6, ♝xf7+ for Bxf7+), for both sides and in every language: never with letters, neither N B R Q K nor the player's own (Cf6), even when they write them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chesspal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|