chesspal 0.2.3 → 0.2.4
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/dist/commands/agents.js +2 -1
- package/dist/commands/ai-say.d.ts +2 -1
- package/dist/commands/analyse-game.d.ts +2 -0
- package/dist/commands/analyse-game.js +7 -4
- package/dist/commands/annotate-game.d.ts +2 -0
- package/dist/commands/annotate-game.js +26 -8
- package/dist/commands/answer-lines.d.ts +8 -4
- package/dist/commands/answer-lines.js +19 -3
- package/dist/commands/coach.js +2 -2
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/delete-review.js +3 -2
- package/dist/commands/game-summary.d.ts +7 -2
- package/dist/commands/game-summary.js +7 -0
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/look.d.ts +9 -5
- package/dist/commands/look.js +19 -9
- package/dist/commands/ponder.js +1 -1
- package/dist/commands/position.js +2 -2
- package/dist/commands/say.d.ts +11 -6
- package/dist/commands/say.js +21 -13
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +31 -6
- package/dist/commands/study.d.ts +11 -1
- package/dist/commands/study.js +31 -2
- package/dist/commands/view.d.ts +17 -5
- package/dist/commands/view.js +67 -10
- package/dist/core/facts.d.ts +14 -2
- package/dist/core/facts.js +32 -7
- package/dist/core/game.d.ts +5 -0
- package/dist/core/game.js +9 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/placement.d.ts +63 -0
- package/dist/core/placement.js +259 -0
- package/dist/core/possibilities.d.ts +38 -0
- package/dist/core/possibilities.js +254 -0
- package/dist/core/review.d.ts +4 -1
- package/dist/core/review.js +17 -6
- package/dist/games/analysis.d.ts +8 -4
- package/dist/games/analysis.js +9 -5
- package/dist/games/current-game.d.ts +16 -1
- package/dist/games/current-game.js +10 -8
- package/dist/games/files.d.ts +2 -0
- package/dist/games/files.js +7 -1
- package/dist/games/index.d.ts +2 -2
- package/dist/games/index.js +1 -1
- package/dist/games/table-talk.d.ts +2 -0
- package/dist/games/table-talk.js +5 -1
- package/dist/games/talk.d.ts +4 -3
- package/dist/games/talk.js +3 -3
- package/dist/page/app/app.js +36 -12
- package/dist/page/app/keys.js +16 -2
- package/dist/page/app/served.js +2 -2
- package/dist/page/board/board.js +2 -1
- package/dist/page/board/marks.js +4 -24
- package/dist/page/board/signs.js +23 -0
- package/dist/page/game/column.js +2 -2
- package/dist/page/game/game.css +12 -0
- package/dist/page/game/move-list.js +2 -2
- package/dist/page/game/plies.js +8 -0
- package/dist/page/helps/helps.css +3 -1
- package/dist/page/helps/helps.js +15 -15
- package/dist/page/layout/layout.css +29 -5
- package/dist/page/legend/legend.css +203 -0
- package/dist/page/legend/legend.js +169 -0
- package/dist/page/legend/view.js +54 -0
- package/dist/page/style.css +2 -0
- package/dist/page/talk/chat.css +90 -0
- package/dist/page/talk/talk.js +27 -5
- package/dist/page/text.js +38 -2
- package/dist/server/mcp.js +8 -6
- package/dist/server/page/game.js +20 -7
- package/dist/server/page/page.js +5 -3
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
package/dist/games/analysis.js
CHANGED
|
@@ -3,23 +3,27 @@
|
|
|
3
3
|
import { readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { writeAtomically } from "./files.js";
|
|
6
|
-
/** Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. */
|
|
7
|
-
export function startAnalysis(folder, game, now, brought = false) {
|
|
8
|
-
|
|
6
|
+
/** Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. The same game keeps its corrections. */
|
|
7
|
+
export function startAnalysis(folder, game, now, brought = false, corrected = []) {
|
|
8
|
+
const before = gameInAnalysis(folder);
|
|
9
|
+
const kept = [...new Set([...(before?.game === game ? before.corrected ?? [] : []), ...corrected])];
|
|
10
|
+
const analysis = { game, at: now.toISOString(), ...(brought ? { brought } : {}), ...(kept.length === 0 ? {} : { corrected: kept }) };
|
|
11
|
+
writeAtomically(join(folder, fileName), `${JSON.stringify(analysis, null, 2)}\n`);
|
|
9
12
|
}
|
|
10
13
|
/** The game in analysis, or none when there is none, or analysis.json cannot be read. */
|
|
11
14
|
export function gameInAnalysis(folder) {
|
|
12
15
|
try {
|
|
13
16
|
const analysis = JSON.parse(readFileSync(join(folder, fileName), "utf8"));
|
|
17
|
+
const corrected = Array.isArray(analysis.corrected) ? analysis.corrected.filter((move) => typeof move === "string") : [];
|
|
14
18
|
return typeof analysis.game === "string" && typeof analysis.at === "string"
|
|
15
|
-
? { game: analysis.game, at: analysis.at, ...(analysis.brought === true ? { brought: true } : {}) }
|
|
19
|
+
? { game: analysis.game, at: analysis.at, ...(analysis.brought === true ? { brought: true } : {}), ...(corrected.length === 0 ? {} : { corrected }) }
|
|
16
20
|
: undefined;
|
|
17
21
|
}
|
|
18
22
|
catch {
|
|
19
23
|
return undefined;
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
|
-
/** Ends the analysis, when a new game starts. */
|
|
26
|
+
/** Ends the analysis, when a new game starts or its review is deleted. */
|
|
23
27
|
export function endAnalysis(folder) {
|
|
24
28
|
rmSync(join(folder, fileName), { force: true });
|
|
25
29
|
}
|
|
@@ -16,13 +16,23 @@ export interface CurrentGame {
|
|
|
16
16
|
}
|
|
17
17
|
/** Who speaks on the board page. */
|
|
18
18
|
export type Speaker = "player" | "ai";
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* A line said on the board page: who said it, its words, how many moves the game had then, and when, as an ISO date; the AI's agent on the player's time
|
|
21
|
+
* (D-72); in analysis, the position the page showed as the player wrote (D-101).
|
|
22
|
+
*/
|
|
20
23
|
export interface TalkLine {
|
|
21
24
|
readonly who: Speaker;
|
|
22
25
|
readonly text: string;
|
|
23
26
|
readonly ply: number;
|
|
24
27
|
readonly at: string;
|
|
25
28
|
readonly agent?: true;
|
|
29
|
+
readonly shown?: Shown;
|
|
30
|
+
}
|
|
31
|
+
/** A position of a game in analysis: the game's name, how many of its moves the page showed, and the last of them numbered, 52...Kf6; none at the start. */
|
|
32
|
+
export interface Shown {
|
|
33
|
+
readonly game: string;
|
|
34
|
+
readonly at: number;
|
|
35
|
+
readonly move?: string;
|
|
26
36
|
}
|
|
27
37
|
/** How a game ended: its result, and why when the record says it or the board decided it. */
|
|
28
38
|
export interface GameEnd {
|
|
@@ -53,4 +63,9 @@ export declare const savedLine: z.ZodObject<{
|
|
|
53
63
|
ply: z.ZodInt;
|
|
54
64
|
at: z.ZodISODateTime;
|
|
55
65
|
agent: z.ZodOptional<z.ZodLiteral<true>>;
|
|
66
|
+
shown: z.ZodOptional<z.ZodObject<{
|
|
67
|
+
game: z.ZodString;
|
|
68
|
+
at: z.ZodInt;
|
|
69
|
+
move: z.ZodOptional<z.ZodString>;
|
|
70
|
+
}, z.core.$strip>>;
|
|
56
71
|
}, z.core.$strip>;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// The player's current game on disk (D-27, D-41): game.json, its PGN and the talk on the board page inside, written whole at every change, and kept in
|
|
2
2
|
// memory while nobody else changes it.
|
|
3
|
-
import { readFileSync, rmSync
|
|
3
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { Game, readPgn, writePgn } from "../core/index.js";
|
|
7
|
-
import { writeAtomically } from "./files.js";
|
|
7
|
+
import { stampOf, writeAtomically } from "./files.js";
|
|
8
8
|
/** The current game of a folder, as it is on disk, or undefined when there is none. A game.json Chesspal cannot read throws: nothing overwrites it. */
|
|
9
9
|
export function loadGame(folder) {
|
|
10
10
|
const path = join(folder, fileName);
|
|
@@ -73,7 +73,14 @@ export function removeGame(folder) {
|
|
|
73
73
|
}
|
|
74
74
|
const fileName = "game.json";
|
|
75
75
|
/** A line of the talk as game.json and talk.json keep it. */
|
|
76
|
-
export const savedLine = z.object({
|
|
76
|
+
export const savedLine = z.object({
|
|
77
|
+
who: z.enum(["player", "ai"]),
|
|
78
|
+
text: z.string(),
|
|
79
|
+
ply: z.int().min(0),
|
|
80
|
+
at: z.iso.datetime(),
|
|
81
|
+
agent: z.literal(true).optional(),
|
|
82
|
+
shown: z.object({ game: z.string(), at: z.int().min(0), move: z.string().optional() }).optional(),
|
|
83
|
+
});
|
|
77
84
|
const savedClock = z.object({
|
|
78
85
|
base: z.int().positive(),
|
|
79
86
|
increment: z.int().min(0),
|
|
@@ -93,11 +100,6 @@ const gameFile = z.object({
|
|
|
93
100
|
});
|
|
94
101
|
// The games read or written in this process, by their file, with the file's stamp then: a call reads a file again only when another process changed it.
|
|
95
102
|
const kept = new Map();
|
|
96
|
-
function stampOf(path) {
|
|
97
|
-
// The file's id changes at every rename: a write within the same tick of the clock, and of the same size, is seen too.
|
|
98
|
-
const stats = statSync(path, { bigint: true, throwIfNoEntry: false });
|
|
99
|
-
return stats === undefined ? undefined : `${stats.ino}:${stats.mtimeNs}:${stats.size}`;
|
|
100
|
-
}
|
|
101
103
|
function parsed(text, path) {
|
|
102
104
|
try {
|
|
103
105
|
const file = gameFile.parse(JSON.parse(text));
|
package/dist/games/files.d.ts
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export declare function writeAtomically(path: string, text: string | Uint8Array): void;
|
|
3
3
|
/** Makes a folder, and those above it, when missing. A failure says what to do about it. */
|
|
4
4
|
export declare function makeFolder(path: string): void;
|
|
5
|
+
/** A stamp of a file that changes whenever it is written, whoever writes it; undefined when there is none. */
|
|
6
|
+
export declare function stampOf(path: string): string | undefined;
|
|
5
7
|
/**
|
|
6
8
|
* Renames a file or a folder. Windows refuses while another program holds a file in it open (an editor, an antivirus, a sync client): Chesspal waits a
|
|
7
9
|
* little and tries again, half a second at most.
|
package/dist/games/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Writing the player's files safely: whole or not at all, with failures that say what to do (D-27).
|
|
2
|
-
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
/** Writes a file whole or not at all: a temporary file beside it, then renamed over it (D-27). A failure says what to do about it. */
|
|
4
4
|
export function writeAtomically(path, text) {
|
|
5
5
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
@@ -23,6 +23,12 @@ export function makeFolder(path) {
|
|
|
23
23
|
throw new Error(`${path} could not be made (${error.message}): check that it is a folder, and can be written.`, { cause: error });
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
/** A stamp of a file that changes whenever it is written, whoever writes it; undefined when there is none. */
|
|
27
|
+
export function stampOf(path) {
|
|
28
|
+
// The file's id changes at every rename: a write within the same tick of the clock, and of the same size, is seen too.
|
|
29
|
+
const stats = statSync(path, { bigint: true, throwIfNoEntry: false });
|
|
30
|
+
return stats === undefined ? undefined : `${stats.ino}:${stats.mtimeNs}:${stats.size}`;
|
|
31
|
+
}
|
|
26
32
|
const attempts = 10;
|
|
27
33
|
/**
|
|
28
34
|
* Renames a file or a folder. Windows refuses while another program holds a file in it open (an editor, an antivirus, a sync client): Chesspal waits a
|
package/dist/games/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export { type Analysis, endAnalysis, gameInAnalysis, startAnalysis } from "./ana
|
|
|
3
3
|
export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.ts";
|
|
4
4
|
export { type ChatStart, cleanSince, lastChatStart, saveChatStart } from "./chat-start.ts";
|
|
5
5
|
export { type CoachHint, type CoachJudgment, type CoachLead, coachShown, coachStarted, coachWaitingSince, type CoachWords, coachWords, forgetCoach, keepHint, keepJudgment, keepLead, startCoach, withCoach } from "./coach.ts";
|
|
6
|
-
export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, type GameEnd, gameStamp, loadGame, removeGame, saveGame, type Speaker, type TalkLine } from "./current-game.ts";
|
|
6
|
+
export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, type GameEnd, gameStamp, loadGame, removeGame, saveGame, type Shown, type Speaker, type TalkLine } from "./current-game.ts";
|
|
7
7
|
export { makeFolder, renameOver, writeAtomically } from "./files.ts";
|
|
8
8
|
export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.ts";
|
|
9
|
-
export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk } from "./table-talk.ts";
|
|
9
|
+
export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk, tableTalkStamp } from "./table-talk.ts";
|
|
10
10
|
export { type Heard, heardLines, type Sayer, type Talk, talkLimits, talkLine, unheardWords, wakesTheChat, type Words, WordsRefused } from "./talk.ts";
|
|
11
11
|
export { keepThought, notePondering, ponderingOn, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";
|
package/dist/games/index.js
CHANGED
|
@@ -7,6 +7,6 @@ export { coachShown, coachStarted, coachWaitingSince, coachWords, forgetCoach, k
|
|
|
7
7
|
export { bothSidesMoved, drawOfferOf, endOf, gameStamp, loadGame, removeGame, saveGame } from "./current-game.js";
|
|
8
8
|
export { makeFolder, renameOver, writeAtomically } from "./files.js";
|
|
9
9
|
export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.js";
|
|
10
|
-
export { endTableTalk, loadTableTalk, saveTableTalk } from "./table-talk.js";
|
|
10
|
+
export { endTableTalk, loadTableTalk, saveTableTalk, tableTalkStamp } from "./table-talk.js";
|
|
11
11
|
export { heardLines, talkLimits, talkLine, unheardWords, wakesTheChat, WordsRefused } from "./talk.js";
|
|
12
12
|
export { keepThought, notePondering, ponderingOn, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.js";
|
|
@@ -8,5 +8,7 @@ export interface TableTalk {
|
|
|
8
8
|
export declare function loadTableTalk(folder: string): TableTalk;
|
|
9
9
|
/** Writes the talk with no game, whole or not at all. */
|
|
10
10
|
export declare function saveTableTalk(folder: string, table: TableTalk): void;
|
|
11
|
+
/** A stamp of talk.json that changes whenever it is written; undefined when there is none. */
|
|
12
|
+
export declare function tableTalkStamp(folder: string): string | undefined;
|
|
11
13
|
/** Forgets the talk with no game, when a game starts. */
|
|
12
14
|
export declare function endTableTalk(folder: string): void;
|
package/dist/games/table-talk.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { savedLine } from "./current-game.js";
|
|
6
|
-
import { writeAtomically } from "./files.js";
|
|
6
|
+
import { stampOf, writeAtomically } from "./files.js";
|
|
7
7
|
/** The talk with no game: empty when there is none, or talk.json cannot be read, since it is only words. */
|
|
8
8
|
export function loadTableTalk(folder) {
|
|
9
9
|
try {
|
|
@@ -17,6 +17,10 @@ export function loadTableTalk(folder) {
|
|
|
17
17
|
export function saveTableTalk(folder, table) {
|
|
18
18
|
writeAtomically(join(folder, fileName), `${JSON.stringify(table, null, 2)}\n`);
|
|
19
19
|
}
|
|
20
|
+
/** A stamp of talk.json that changes whenever it is written; undefined when there is none. */
|
|
21
|
+
export function tableTalkStamp(folder) {
|
|
22
|
+
return stampOf(join(folder, fileName));
|
|
23
|
+
}
|
|
20
24
|
/** Forgets the talk with no game, when a game starts. */
|
|
21
25
|
export function endTableTalk(folder) {
|
|
22
26
|
rmSync(join(folder, fileName), { force: true });
|
package/dist/games/talk.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CurrentGame, type Speaker, type TalkLine } from "./current-game.ts";
|
|
1
|
+
import { type CurrentGame, type Shown, type Speaker, type TalkLine } from "./current-game.ts";
|
|
2
2
|
import type { TableTalk } from "./table-talk.ts";
|
|
3
3
|
/** The most the player may write in a move, in all their lines, and the most a line of the AI's may hold (D-54); with the helps on, as an analyst's note (D-78). */
|
|
4
4
|
export declare const talkLimits: {
|
|
@@ -14,7 +14,7 @@ export type Talk = CurrentGame | TableTalk;
|
|
|
14
14
|
* A new line of the talk, said when the game has `ply` moves: refused when it is empty or too long. The player writes 200 characters a move in all, the
|
|
15
15
|
* words sent with the move included; the AI speaks once a move of theirs, on their time, and its agent's word leaves it an answer to what they write after (D-54, D-72).
|
|
16
16
|
*/
|
|
17
|
-
export declare function talkLine(current: Talk, sayer: Sayer, text: string, at: Date, ply?: number, aiMost?: number): TalkLine;
|
|
17
|
+
export declare function talkLine(current: Talk, sayer: Sayer, text: string, at: Date, ply?: number, aiMost?: number, shown?: Shown): TalkLine;
|
|
18
18
|
/** Words refused, and why, for the page to say it in the player's language: none, the move's characters spent, more than a line or what is left holds. */
|
|
19
19
|
export declare class WordsRefused extends Error {
|
|
20
20
|
readonly reason: "empty" | "spent" | "long" | "longer-than-left";
|
|
@@ -23,10 +23,11 @@ export declare class WordsRefused extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
/** Whether the player's last line wakes the chat: the first of a move, and between games the first since the AI last spoke (D-54, D-74). */
|
|
25
25
|
export declare function wakesTheChat(current: Talk): boolean;
|
|
26
|
-
/** Words of the player's, with the key that tells them apart across games. */
|
|
26
|
+
/** Words of the player's, with the key that tells them apart across games, and in analysis the position the page showed (D-101). */
|
|
27
27
|
export interface Words {
|
|
28
28
|
readonly key: string;
|
|
29
29
|
readonly text: string;
|
|
30
|
+
readonly shown?: Shown;
|
|
30
31
|
}
|
|
31
32
|
/** What the player said that the AI has not answered, but for the words whose keys are in `given`. */
|
|
32
33
|
export declare function unheardWords(current: Talk, given: ReadonlySet<string>): Words[];
|
package/dist/games/talk.js
CHANGED
|
@@ -6,7 +6,7 @@ export const talkLimits = { player: 200, ai: 300, aiHelps: 700 };
|
|
|
6
6
|
* A new line of the talk, said when the game has `ply` moves: refused when it is empty or too long. The player writes 200 characters a move in all, the
|
|
7
7
|
* words sent with the move included; the AI speaks once a move of theirs, on their time, and its agent's word leaves it an answer to what they write after (D-54, D-72).
|
|
8
8
|
*/
|
|
9
|
-
export function talkLine(current, sayer, text, at, ply = isGame(current) ? current.game.record.moves.length : 0, aiMost = talkLimits.ai) {
|
|
9
|
+
export function talkLine(current, sayer, text, at, ply = isGame(current) ? current.game.record.moves.length : 0, aiMost = talkLimits.ai, shown) {
|
|
10
10
|
const who = sayer === "agent" ? "ai" : sayer;
|
|
11
11
|
const words = text.trim();
|
|
12
12
|
const most = who === "player" ? wordsLeft(current) : aiMost;
|
|
@@ -23,7 +23,7 @@ export function talkLine(current, sayer, text, at, ply = isGame(current) ? curre
|
|
|
23
23
|
if (who === "ai" && isGame(current)) {
|
|
24
24
|
refuseAiWords(current, sayer === "agent");
|
|
25
25
|
}
|
|
26
|
-
return { who, text: words, ply, at: at.toISOString(), ...(sayer === "agent" ? { agent: true } : {}) };
|
|
26
|
+
return { who, text: words, ply, at: at.toISOString(), ...(sayer === "agent" ? { agent: true } : {}), ...(shown === undefined ? {} : { shown }) };
|
|
27
27
|
}
|
|
28
28
|
/** Words refused, and why, for the page to say it in the player's language: none, the move's characters spent, more than a line or what is left holds. */
|
|
29
29
|
export class WordsRefused extends Error {
|
|
@@ -41,7 +41,7 @@ export function wakesTheChat(current) {
|
|
|
41
41
|
}
|
|
42
42
|
/** What the player said that the AI has not answered, but for the words whose keys are in `given`. */
|
|
43
43
|
export function unheardWords(current, given) {
|
|
44
|
-
return unanswered(current).filter(({ key }) => !given.has(key)).map(({ key, line }) => ({ key, text: line.text }));
|
|
44
|
+
return unanswered(current).filter(({ key }) => !given.has(key)).map(({ key, line }) => ({ key, text: line.text, ...(line.shown === undefined ? {} : { shown: line.shown }) }));
|
|
45
45
|
}
|
|
46
46
|
/** How far each line of the talk got: the player's by `Heard`, the AI's undefined (D-53). */
|
|
47
47
|
export function heardLines(current, given) {
|
package/dist/page/app/app.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Thinker, Thought } from "../avatars/thought.js";
|
|
|
6
6
|
import { Torretta } from "../avatars/torretta.js";
|
|
7
7
|
import { Board } from "../board/board.js";
|
|
8
8
|
import { explain } from "../board/explain.js";
|
|
9
|
+
import { kingSigns } from "../board/signs.js";
|
|
9
10
|
import { piecesOf } from "../board/squares.js";
|
|
10
11
|
import { NewGameDialog } from "../dialogs/new-game.js";
|
|
11
12
|
import { NameButton, PictureButton, PictureDialog, ProfilesDialog, Wizard } from "../dialogs/profiles.js";
|
|
@@ -18,13 +19,15 @@ import { GameColumn } from "../game/column.js";
|
|
|
18
19
|
import { howEnded, useDeclined, useJustEnded, useSawEnd } from "../game/ending.js";
|
|
19
20
|
import { Steps } from "../game/move-list.js";
|
|
20
21
|
import { useOpeningBook } from "../game/opening-book.js";
|
|
21
|
-
import { aiSpoke, aiWords, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
|
|
22
|
+
import { aiSpoke, aiWords, analysisTalk, judgmentsOf, playerSpent, playerWords, pliesOf, withBook, withJudgments, withoutReviews } from "../game/plies.js";
|
|
22
23
|
import { BareButton, HelpsCard, HintLine, MapButton } from "../helps/helps.js";
|
|
23
24
|
import { helpsOf } from "../helps/view.js";
|
|
24
25
|
import { FoldTab } from "../layout/fold-tab.js";
|
|
25
26
|
import { nextFold, rememberFold, startFolds } from "../layout/folds.js";
|
|
26
27
|
import { Header } from "../layout/header.js";
|
|
27
28
|
import { standingsOf } from "../lead/view.js";
|
|
29
|
+
import { MarksLegend } from "../legend/legend.js";
|
|
30
|
+
import { legendOf } from "../legend/view.js";
|
|
28
31
|
import { ChosenSet } from "../pieces/pieces.js";
|
|
29
32
|
import { ClockFace, PlayerCard } from "../players/card.js";
|
|
30
33
|
import { signature } from "../players/model-name.js";
|
|
@@ -34,7 +37,7 @@ import { endSoundOf, playSound, soundOf, wakeSounds } from "../sounds/sounds.js"
|
|
|
34
37
|
import { AiNote, AiWords, Compose, PointSquare, TalkFaces } from "../talk/talk.js";
|
|
35
38
|
import { textFor } from "../text.js";
|
|
36
39
|
import { anchorName, useAnchor } from "./anchor.js";
|
|
37
|
-
import { useBrowseKeys, useFoldKeys, useMapKey, useWheelSteps } from "./keys.js";
|
|
40
|
+
import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
|
|
38
41
|
import { phaseOf, verdictOf } from "./phase.js";
|
|
39
42
|
import { post, sendMessage, sendMove, useServed } from "./served.js";
|
|
40
43
|
/**
|
|
@@ -135,6 +138,9 @@ export function App({ start }) {
|
|
|
135
138
|
const [mapping, setMapping] = useState(false);
|
|
136
139
|
// The board without its drawings while their button is held (Stefano).
|
|
137
140
|
const [bare, setBare] = useState(false);
|
|
141
|
+
// The legend of the marks on the board, open until the player closes it (D-99).
|
|
142
|
+
const [legendOpen, setLegendOpen] = useState(false);
|
|
143
|
+
useLegendKey(() => setLegendOpen((open) => !open));
|
|
138
144
|
const past = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing) }), [live, viewing]);
|
|
139
145
|
const next = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing + 1) }).lastMove, [live, viewing]);
|
|
140
146
|
const shown = useMemo(() => past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past]);
|
|
@@ -253,6 +259,22 @@ export function App({ start }) {
|
|
|
253
259
|
const review = reviewed?.review;
|
|
254
260
|
// An open hint draws on the board as it is, in place of the last move's drawings.
|
|
255
261
|
const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
|
|
262
|
+
const drawnByCoach = hint !== undefined && drawn === hint;
|
|
263
|
+
// The game over, when this is its last position: the kings' signs, on the board and in the legend.
|
|
264
|
+
const boardEnd = ended && viewing === undefined && pending === undefined ? { how, winner, player, fresh: justEnded } : undefined;
|
|
265
|
+
// The legend has the marks the board shows: none of the helps' or the drawings while the board is bare (D-99).
|
|
266
|
+
const shownBare = bare && helps !== undefined;
|
|
267
|
+
const legendRows = legendOf({
|
|
268
|
+
...(review?.badge === undefined || shown.lastMove === undefined ? {} : { badge: { badge: review.badge, square: shown.lastMove.to } }),
|
|
269
|
+
...(helps === undefined || shownBare ? {} : { helps }),
|
|
270
|
+
...(drawn === undefined || shownBare ? {} : { drawn }),
|
|
271
|
+
control: mapping && helps !== undefined,
|
|
272
|
+
...(boardEnd === undefined ? {} : { ends: kingSigns(piecesOf(shown.position.fen), shown.position.turn, boardEnd) }),
|
|
273
|
+
});
|
|
274
|
+
// In a game the coach draws on the player's moves, the AI on its own.
|
|
275
|
+
const drawer = drawnByCoach || (going && reviewed?.side === player) ? text.legend.drawnByCoach : text.legend.drawnBy(analyst);
|
|
276
|
+
const legendAt = (under) => (_jsx(MarksLegend, { rows: legendRows, open: legendOpen, onOpen: setLegendOpen, drawn: { words: drawnByCoach ? text.helpsCard.drawn : text.drawn, by: drawer }, onPoint: setPointed, under: under, text: text }));
|
|
277
|
+
const legend = legendAt(false);
|
|
256
278
|
const summary = studied === undefined ? undefined : gameReviewOf(live.record);
|
|
257
279
|
const closing = studied === undefined ? undefined : closingOf(live.record);
|
|
258
280
|
// In analysis the AI's card says who played that game, as its PGN keeps it (D-82), not who plays now.
|
|
@@ -270,7 +292,7 @@ export function App({ start }) {
|
|
|
270
292
|
const clockWaits = clock?.waiting === true && !ended && studied === undefined && viewing === undefined && live.position.turn === ai;
|
|
271
293
|
const clockOf = (side) => clock === undefined ? undefined : (_jsx(ClockFace, { clock: clock, side: side, toMove: !ended && live.position.turn === side, held: pending !== undefined || (asleep && side === ai), onFlag: reload, text: text }));
|
|
272
294
|
// Where to wake the chat, under the AI's card (over the board when the talk is folded); the button that starts the clocks, under the player's (Zoe).
|
|
273
|
-
const asleepNote = asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null;
|
|
295
|
+
const asleepNote = asleep ? _jsx("p", { class: "asleep", role: "status", children: sides === undefined ? text.asleep(name) : text.asleepInAnalysis(name) }) : null;
|
|
274
296
|
const startClocks = clockWaits
|
|
275
297
|
? (_jsxs("button", { type: "button", class: "command primary start-clocks", onClick: () => act("/api/start", {}), children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M8 5.5v13l10.5-6.5z" }) }), text.startClocks] }))
|
|
276
298
|
: null;
|
|
@@ -322,7 +344,8 @@ export function App({ start }) {
|
|
|
322
344
|
setRefusal({ text: words, square: from, piece: pieces.get(from), explanation: explain(illegal, to, king) });
|
|
323
345
|
}
|
|
324
346
|
};
|
|
325
|
-
|
|
347
|
+
// In analysis the words go with the game and the move shown (D-101).
|
|
348
|
+
const sendWords = (shown) => {
|
|
326
349
|
const words = draft;
|
|
327
350
|
// /clear and the other commands are for Claude's app: written here they would reach the AI as words (Sebastiano's playtest).
|
|
328
351
|
if (isCommand(words)) {
|
|
@@ -330,7 +353,7 @@ export function App({ start }) {
|
|
|
330
353
|
return;
|
|
331
354
|
}
|
|
332
355
|
setUnsent(undefined);
|
|
333
|
-
void sendMessage(words, text, name).then((refused) => {
|
|
356
|
+
void sendMessage(words, text, name, shown).then((refused) => {
|
|
334
357
|
setUnsent(refused?.text);
|
|
335
358
|
clearSent(refused?.text, words);
|
|
336
359
|
});
|
|
@@ -344,9 +367,7 @@ export function App({ start }) {
|
|
|
344
367
|
setDialog(undefined);
|
|
345
368
|
act("/api/new", { ai_colour: colour === "random" ? "random" : colour === "white" ? "black" : "white", level: chosen, ...(control === undefined ? {} : { clock: control }) });
|
|
346
369
|
};
|
|
347
|
-
// In analysis the AI's note
|
|
348
|
-
const top = flipped ? player : ai;
|
|
349
|
-
const bottom = flipped ? ai : player;
|
|
370
|
+
// In analysis the AI's note goes with the other side's card and the talk with the player's, turned round with the board as in a game (D-101, Zoe).
|
|
350
371
|
const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place,
|
|
351
372
|
// The player changes their picture in analysis too, from their own side's card.
|
|
352
373
|
picture: sides[side].who === "player" ? _jsx(PictureButton, { profile: profile, onOpen: () => setDialog("picture"), text: text }) : pictureOf(sides[side], profile, 48, mood), name: sides[side].name, signed: sides[side].who === "ai" && playedBy !== undefined ? signature(playedBy.model, playedBy.effort) : undefined, side: side, taken: material.taken[side], ahead: ahead(side), badges: _jsx(BadgeCounts, { plies: plies, side: side, names: text.badges }), text: text }));
|
|
@@ -360,8 +381,11 @@ export function App({ start }) {
|
|
|
360
381
|
};
|
|
361
382
|
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(PictureDialog, { open: dialog === "picture", profile: profile, onClose: () => setDialog(undefined), text: text }), _jsx(ProfilesDialog, { open: dialog === "profiles", profiles: served.profiles, active: profile, locked: going ? text.profile.whilePlaying : undefined, dataFolder: served.dataFolder, onClose: () => setDialog(undefined), text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), you: profile.name, accounts: profile.accounts, ai: name, text: text }), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true ? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) }) : null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
|
|
362
383
|
? (_jsx(TalkFaces, { name: name, aiPicture: thinker(40), you: profile.name, picture: picture, ...(idle ? {} : { ai, player }), aiMoves: going && !playerToMove, playerMoves: playerToMove, unread: unread || asleep, text: text }))
|
|
363
|
-
: (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined
|
|
364
|
-
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[
|
|
384
|
+
: (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, 40, mood), ai: ai, player: player, aiMoves: false, playerMoves: false, unread: asleep, text: text })), _jsx(FoldTab, { column: "talk", folded: folds.talk === "folded", label: thinking === undefined || folds.talk !== "folded" ? text.talk : `${text.talk}: ${text.thinking[thinking]}`, unread: (unread || asleep) && folds.talk === "folded", ...(thinking === undefined || folds.talk !== "folded" ? {} : { mark: _jsx(Thought, { thinking: thinking, words: text.thinking }) }), onFold: fold }), _jsx(FoldTab, { column: "game", folded: folds.game === "folded", label: text.game, onFold: fold }), _jsx("span", { class: "visually-hidden", "aria-live": "polite", children: unread ? text.wrote(name) : "" }), _jsx("div", { class: sides === undefined ? "talk" : "talk studied", id: "talk", children: sides !== undefined && studied !== undefined
|
|
385
|
+
? (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": sides[ai].name, children: [studiedCard(ai, "ai"), asleepNote, _jsx(AiNote, { ...(reviewed === undefined ? {} : { ply: reviewed }), ...(shownOpening === undefined ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined ? { closing } : {}), ...(at === 0 && summary !== undefined ? { summary } : {}), ...(summary === undefined ? { reviewing: true } : {}), name: analyst, by: annotator, mood: mood, ...(settings.voice ? { voice: text.language } : {}), text: text })] }), _jsxs("section", { class: "you-block", "aria-label": sides[player].name, children: [_jsx("div", { class: "chat", children: _jsx(Compose, { lines: analysisTalk(talk, studied.name), newestFirst: false, spent: 0, draft: draft, onDraft: (words) => {
|
|
386
|
+
setDraft(words);
|
|
387
|
+
setUnsent(undefined);
|
|
388
|
+
}, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, text: text }) }), studiedCard(player, "you")] })] }))
|
|
365
389
|
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: name, signed: signed, side: idle ? undefined : ai, ...(idle ? {} : { level: text.start.levels[level] }), taken: material.taken[ai], ahead: ahead(ai), clock: clockOf(ai), text: text }), asleepNote, _jsx(AiWords, { words: aiWords(plies, ai, talk), newestFirst: !flipped, ...(settings.voice ? { voice: text.language } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
|
|
366
390
|
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
367
391
|
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
|
@@ -371,9 +395,9 @@ export function App({ start }) {
|
|
|
371
395
|
// Between games the board goes off (D-77): a game the page saw end once its effects are over, its final position less off than the table set.
|
|
372
396
|
class: between && !justEnded ? "board-wrap off" : "board-wrap", onWheel: wheel,
|
|
373
397
|
// A press on the board while the player looks back brings the game as it is: the next one plays.
|
|
374
|
-
onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end:
|
|
398
|
+
onPointerDownCapture: viewing === undefined || studied !== undefined ? undefined : (event) => event.button === 0 && goTo(total), children: [_jsx(Board, { fen: shown.position.fen, bottom: flipped ? ai : player, lastMove: shown.lastMove, pointed: pointed, ...(helps === undefined ? {} : { helps }), ...(mapping && helps !== undefined ? { control: helps.strategy.control } : {}), bare: bare && helps !== undefined, ...(review?.badge === undefined ? {} : { badge: { badge: review.badge, name: text.badges[review.badge] } }), ...(drawn === undefined ? {} : { drawn }), nextMove: next, jumped: jumped, check: shown.position.check, end: boardEnd, player: playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined || viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing === undefined ? refusal : undefined, promotion: promoting === undefined || viewing !== undefined || !playerToMove ? undefined : { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : play(promoting.from, promoting.to, piece), onSelect: select, onMove: play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleep || clockWaits)
|
|
375
399
|
? (_jsxs("div", { class: "over-board", children: [asleep ? _jsx("p", { class: "asleep", role: "status", children: text.asleep(name) }) : null, startClocks] }))
|
|
376
|
-
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [_jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, text: text })), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: analyst } : { by: analyst, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
|
|
400
|
+
: null] }), studied === undefined ? null : (_jsx("div", { class: "board-steps", children: _jsx(Steps, { shown: at, total: total, onStep: step, text: text }) })), helps === undefined ? null : (_jsxs("div", { class: "board-map", children: [legendAt(true), _jsx(BareButton, { bare: bare, onBare: setBare, text: text }), _jsx(MapButton, { mapping: mapping, onMap: setMapping, text: text }), hint === undefined || viewing !== undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: openTheHint, text: text })] })), _jsx(GameColumn, { plies: idle ? [] : plies, result: ended ? result : undefined, how: how, lit: justEnded, started: idle ? undefined : played?.started, move: live.position.moveNumber, toMove: live.position.turn, opening: opening, helps: helps === undefined ? undefined : (_jsx(HelpsCard, { helps: helps, judged: judgments, hint: viewing === undefined ? hint : undefined, hintOpen: hintOpen, onHint: openTheHint, onPoint: setPointed, onMap: setMapping, mapping: mapping, onBare: setBare, bare: bare, legend: legend, text: text })), ...(helps === undefined && legendRows.length > 0 ? { legend } : {}), ...(standings === undefined ? {} : { standings }), ...(studied === undefined ? {} : { analysis: summary === undefined ? { by: analyst } : { by: analyst, summary } }), shownPlies: at, onShow: goTo, onStep: step, ...(studied === undefined ? {} : { onDownload: () => download(studied.pgn, pgnName(live.record.tags, "analysis")) }), ...(total === 0 ? {} : { onSaveMoves: () => download(writePgn(movesOnly(live.record)), pgnName(live.record.tags)) }), ...(between && game !== null ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text })] })] }) }));
|
|
377
401
|
}
|
|
378
402
|
/** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, or the plain face of someone else. */
|
|
379
403
|
function pictureOf({ who }, profile, size, mood) {
|
package/dist/page/app/keys.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The page's keys and wheel: [ and ] fold the side columns, M holds the helps' map (D-78), the arrows, Home and End step
|
|
2
|
-
// and x turn the board, and the wheel over the board steps too.
|
|
1
|
+
// The page's keys and wheel: [ and ] fold the side columns, M holds the helps' map (D-78), ? opens the legend (D-99), the arrows, Home and End step
|
|
2
|
+
// through the moves and f and x turn the board, and the wheel over the board steps too.
|
|
3
3
|
import { useEffect, useRef } from "preact/hooks";
|
|
4
4
|
import { flipKeys, stepKeys } from "../game/browse.js";
|
|
5
5
|
import { foldKeys, gameUnderBoard, oneColumn } from "../layout/folds.js";
|
|
@@ -52,6 +52,20 @@ export function useMapKey(helping, map) {
|
|
|
52
52
|
};
|
|
53
53
|
}, [helping]);
|
|
54
54
|
}
|
|
55
|
+
/** ? opens the legend of the marks on the board and closes it, while the page shows its ? (D-99). */
|
|
56
|
+
export function useLegendKey(toggle) {
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
const press = (event) => {
|
|
59
|
+
const shown = [...document.querySelectorAll(".legend-button")].some((button) => button.getClientRects().length > 0);
|
|
60
|
+
if (shown && forThePage(event) && event.key === "?") {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
toggle();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
window.addEventListener("keydown", press);
|
|
66
|
+
return () => window.removeEventListener("keydown", press);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
55
69
|
/** The keys of Lichess and chess.com: the arrows, Home and End step through the moves, f and x turn the board round. */
|
|
56
70
|
export function useBrowseKeys(step, flip) {
|
|
57
71
|
useEffect(() => {
|
package/dist/page/app/served.js
CHANGED
|
@@ -68,6 +68,6 @@ export function sendMove(move, words, text, name) {
|
|
|
68
68
|
return post("/api/move", message === "" ? { move } : { move, message }, text, name);
|
|
69
69
|
}
|
|
70
70
|
/** Sends the player's words alone: the reason they were refused, or undefined. */
|
|
71
|
-
export function sendMessage(words, text, name) {
|
|
72
|
-
return post("/api/message", { text: words.trim() }, text, name);
|
|
71
|
+
export function sendMessage(words, text, name, shown) {
|
|
72
|
+
return post("/api/message", { text: words.trim(), ...shown }, text, name);
|
|
73
73
|
}
|
package/dist/page/board/board.js
CHANGED
|
@@ -6,9 +6,10 @@ import { BadgeIcon } from "../game/badges.js";
|
|
|
6
6
|
import { pinOn, riskOn, skewerOn } from "../helps/view.js";
|
|
7
7
|
import { Piece } from "../pieces/pieces.js";
|
|
8
8
|
import { bubblePlace, keptSquares } from "./explain.js";
|
|
9
|
-
import { Arrows, Badge, Bubble, ControlCount, controlOf,
|
|
9
|
+
import { Arrows, Badge, Bubble, ControlCount, controlOf, Lifted, Notch, PinsAndSkewers, PromotionChoice, Pushpin, Skewer } from "./marks.js";
|
|
10
10
|
import { backOf, motionOf, noMotion, playedOn, rearranged } from "./motion.js";
|
|
11
11
|
import { useBoardPointer } from "./pointer.js";
|
|
12
|
+
import { kingSigns, noSigns } from "./signs.js";
|
|
12
13
|
import { files, kingToMove, piecesOf } from "./squares.js";
|
|
13
14
|
/**
|
|
14
15
|
* The board of a position, seen from one side, with the last move's squares marked. While the player may move, their pieces answer the pointer: a press
|
package/dist/page/board/marks.js
CHANGED
|
@@ -5,7 +5,10 @@ import { Piece } from "../pieces/pieces.js";
|
|
|
5
5
|
export function Notch({ tone }) {
|
|
6
6
|
return (_jsx("span", { class: tone === undefined ? "help-notch" : `help-notch ${tone}`, "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 10 10", children: [_jsx("path", { d: "M0.5 0.5V9.5H9.5Z" }), tone === undefined ? null : _jsx("path", { class: "edge", d: "M0.5 0.5V9.5H9.5Z" }), _jsx("circle", { cx: "2.9", cy: "7.1", r: "1.1" })] }) }));
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* The pushpin stuck in a pinned piece from the top left corner, its needle toward the piece, in the tone of its corner at risk (D-91); in ink in no side's;
|
|
10
|
+
* in the card's line, in its colour.
|
|
11
|
+
*/
|
|
9
12
|
export function Pushpin({ tone }) {
|
|
10
13
|
return _jsx(Stuck, { kind: "help-pin", shape: pushpin, tone: tone });
|
|
11
14
|
}
|
|
@@ -122,29 +125,6 @@ export function Lifted({ drag, letter }) {
|
|
|
122
125
|
const classes = ["lifted", up && !drag.back ? "up" : "", drag.touch ? "touch" : "", drag.back ? "back" : ""].filter((name) => name !== "").join(" ");
|
|
123
126
|
return (_jsx("div", { class: classes, style: { "--x": `${drag.at[0]}px`, "--y": `${drag.at[1]}px` }, children: _jsx(Piece, { letter: letter }) }));
|
|
124
127
|
}
|
|
125
|
-
/** The kings' signs at the end, by their squares: the loser's king lies down when mated or resigning; both kings of a draw stand, the stalemated one stuck. */
|
|
126
|
-
export function kingSigns(pieces, toMove, end) {
|
|
127
|
-
const kingOf = (side) => [...pieces].find(([, letter]) => letter === (side === "white" ? "K" : "k"))?.[0];
|
|
128
|
-
const signs = new Map();
|
|
129
|
-
const sign = (side, badge, classes) => {
|
|
130
|
-
const square = kingOf(side);
|
|
131
|
-
if (square !== undefined) {
|
|
132
|
-
signs.set(square, { badge, classes });
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
if (end.winner === undefined) {
|
|
136
|
-
sign("white", "half", end.how === "stalemate" && toMove === "white" ? "drawn stalled" : "drawn");
|
|
137
|
-
sign("black", "half", end.how === "stalemate" && toMove === "black" ? "drawn stalled" : "drawn");
|
|
138
|
-
return signs;
|
|
139
|
-
}
|
|
140
|
-
const loser = end.winner === "white" ? "black" : "white";
|
|
141
|
-
sign(end.winner, "crown", "winner");
|
|
142
|
-
if (end.how === "checkmate" || end.how === "resignation") {
|
|
143
|
-
sign(loser, end.how === "checkmate" ? "mate" : "flag", "fallen");
|
|
144
|
-
}
|
|
145
|
-
return signs;
|
|
146
|
-
}
|
|
147
|
-
export const noSigns = new Map();
|
|
148
128
|
/** Arrows off one square, the AI's on a white edge (D-78): every edge under every arrow, so one edge goes round them all where they meet. */
|
|
149
129
|
function ArrowGroup({ shapes, edged }) {
|
|
150
130
|
return (_jsxs("g", { children: [edged
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The kings' signs at the end, by their squares: the loser's king lies down when mated or resigning; both kings of a draw stand, the stalemated one stuck. */
|
|
2
|
+
export function kingSigns(pieces, toMove, end) {
|
|
3
|
+
const kingOf = (side) => [...pieces].find(([, letter]) => letter === (side === "white" ? "K" : "k"))?.[0];
|
|
4
|
+
const signs = new Map();
|
|
5
|
+
const sign = (side, badge, classes) => {
|
|
6
|
+
const square = kingOf(side);
|
|
7
|
+
if (square !== undefined) {
|
|
8
|
+
signs.set(square, { badge, classes });
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
if (end.winner === undefined) {
|
|
12
|
+
sign("white", "half", end.how === "stalemate" && toMove === "white" ? "drawn stalled" : "drawn");
|
|
13
|
+
sign("black", "half", end.how === "stalemate" && toMove === "black" ? "drawn stalled" : "drawn");
|
|
14
|
+
return signs;
|
|
15
|
+
}
|
|
16
|
+
const loser = end.winner === "white" ? "black" : "white";
|
|
17
|
+
sign(end.winner, "crown", "winner");
|
|
18
|
+
if (end.how === "checkmate" || end.how === "resignation") {
|
|
19
|
+
sign(loser, end.how === "checkmate" ? "mate" : "flag", "fallen");
|
|
20
|
+
}
|
|
21
|
+
return signs;
|
|
22
|
+
}
|
|
23
|
+
export const noSigns = new Map();
|
package/dist/page/game/column.js
CHANGED
|
@@ -10,11 +10,11 @@ import { ResultTile } from "./result-tile.js";
|
|
|
10
10
|
* The column, which the player folds with its tab; under the board, where three columns would leave the board small, always open. A game in analysis
|
|
11
11
|
* (D-69) says above the moves that the AI is going over it, until its word on the whole game comes, which the talk shows.
|
|
12
12
|
*/
|
|
13
|
-
export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
|
|
13
|
+
export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, legend, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
|
|
14
14
|
const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
|
|
15
15
|
return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
|
|
16
16
|
? (_jsxs(_Fragment, { children: [_jsx("p", { class: "card empty", children: text.noGame }), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseLast, onAnalyse: onAnalyse })] }))
|
|
17
|
-
: (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps, analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })) }));
|
|
17
|
+
: (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps, analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, tools: legend, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })) }));
|
|
18
18
|
}
|
|
19
19
|
/** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
|
|
20
20
|
function SaveButton({ label, hint, onSave }) {
|
package/dist/page/game/game.css
CHANGED
|
@@ -352,6 +352,18 @@
|
|
|
352
352
|
padding: 2px 4px 0;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
/* The title, and on its right the legend of the marks when no helps' card holds it (D-99) */
|
|
356
|
+
.moves-head {
|
|
357
|
+
display: flex;
|
|
358
|
+
align-items: center;
|
|
359
|
+
justify-content: space-between;
|
|
360
|
+
gap: 8px;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.moves-head .hold-button {
|
|
364
|
+
padding: 3px 6px;
|
|
365
|
+
}
|
|
366
|
+
|
|
355
367
|
/* Positioned, so that the moves' hidden names scroll with the list instead of stretching the page */
|
|
356
368
|
.moves {
|
|
357
369
|
position: relative;
|
|
@@ -7,7 +7,7 @@ import { FoldButton, useCardFold } from "./card-fold.js";
|
|
|
7
7
|
* The moves numbered in pairs, the one the board shows marked (`shown`, how many moves it shows), and the result once the game is over; a click on a move
|
|
8
8
|
* shows it. The list keeps its last move in sight, and the one shown while the player looks back.
|
|
9
9
|
*/
|
|
10
|
-
export function MoveList({ plies, result, shown, onShow, text }) {
|
|
10
|
+
export function MoveList({ plies, result, shown, onShow, tools, text }) {
|
|
11
11
|
const list = useRef(null);
|
|
12
12
|
const [folded, toggleFold] = useCardFold("moves");
|
|
13
13
|
const rows = rowsOf(plies);
|
|
@@ -30,7 +30,7 @@ export function MoveList({ plies, result, shown, onShow, text }) {
|
|
|
30
30
|
const move = (ply) => _jsx(Move, { ply: ply, current: ply !== undefined && ply === current, onShow: () => onShow(plies.indexOf(ply) + 1), text: text });
|
|
31
31
|
// The card's title with its icon, a score sheet, as the other cards of the column, which folds the card to it (Stefano); only the list under it
|
|
32
32
|
// scrolls, and it stays in the page while folded, so that it keeps its move in sight.
|
|
33
|
-
return (_jsxs("section", { class: `card moves-card${folded ? " folded" : ""}`, "aria-label": text.moves, children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 3H15L19 7V21H6ZM15 3V7H19M9 11H16M9 14.5H16M9 18H13" }) }), text.moves] }) }), _jsxs("div", { ref: list, class: "moves", hidden: folded, children: [_jsx("ol", { children: rows.map(({ number, white, black }) => (_jsxs("li", { children: [_jsxs("span", { class: "number", children: [number, "."] }), move(white), move(black)] }, number))) }), result === undefined ? null : _jsx("p", { class: "result", children: result })] })] }));
|
|
33
|
+
return (_jsxs("section", { class: `card moves-card${folded ? " folded" : ""}`, "aria-label": text.moves, children: [_jsxs("div", { class: "moves-head", children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 3H15L19 7V21H6ZM15 3V7H19M9 11H16M9 14.5H16M9 18H13" }) }), text.moves] }) }), tools] }), _jsxs("div", { ref: list, class: "moves", hidden: folded, children: [_jsx("ol", { children: rows.map(({ number, white, black }) => (_jsxs("li", { children: [_jsxs("span", { class: "number", children: [number, "."] }), move(white), move(black)] }, number))) }), result === undefined ? null : _jsx("p", { class: "result", children: result })] })] }));
|
|
34
34
|
}
|
|
35
35
|
/** The buttons that step through the moves: to the start, back, forward, and to the game as it is; each one off where it could not go. */
|
|
36
36
|
export function Steps({ shown, total, onStep, text }) {
|
package/dist/page/game/plies.js
CHANGED
|
@@ -58,6 +58,14 @@ export function judgmentsOf(plies, player, { judgments }) {
|
|
|
58
58
|
}
|
|
59
59
|
/** How long a line of the player's may be: the server's limit (D-54), which a test keeps equal. */
|
|
60
60
|
export const talkLimits = { line: 200 };
|
|
61
|
+
/**
|
|
62
|
+
* The talk over a game in analysis (D-101): from the player's first line on it, the lines said since, but for the player's on another game; the last ten,
|
|
63
|
+
* which is all half the column holds (Zoe).
|
|
64
|
+
*/
|
|
65
|
+
export function analysisTalk(talk, game) {
|
|
66
|
+
const first = talk.findIndex((line) => line.shown?.game === game);
|
|
67
|
+
return first === -1 ? [] : talk.slice(first).filter((line) => line.shown === undefined || line.shown.game === game).slice(-10);
|
|
68
|
+
}
|
|
61
69
|
/** How many times the AI has spoken in the game, its comments on its moves and its lines on the page: the page sees it speak when the count grows. */
|
|
62
70
|
export function aiSpoke(plies, ai, talk) {
|
|
63
71
|
return plies.filter((ply) => ply.side === ai && ply.comment !== undefined).length + talk.filter((line) => line.who === "ai").length;
|