chesspal 0.5.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 +148 -29
- package/dist/page/app/keys.js +16 -1
- package/dist/page/app/title.js +6 -3
- package/dist/page/board/board.css +6 -0
- package/dist/page/game/column.js +5 -4
- package/dist/page/game/game.css +150 -1
- package/dist/page/game/move-list.js +48 -10
- package/dist/page/game/variation.js +65 -0
- package/dist/page/guide/en.md +1 -0
- package/dist/page/guide/it.md +1 -0
- 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 -0
- package/dist/server/bell/bell.d.ts +5 -2
- package/dist/server/bell/bell.js +36 -12
- package/dist/server/page/game.js +38 -7
- package/dist/server/page/page.js +1 -1
- package/dist/server/page/table.d.ts +12 -2
- package/dist/server/page/table.js +18 -6
- package/dist/server/table/hosts/chats.js +1 -1
- package/dist/server/table/hosts/play.d.ts +3 -1
- package/dist/server/table/hosts/play.js +6 -5
- package/dist/server/table/hosts/seat.d.ts +5 -1
- package/dist/server/table/hosts/seat.js +1 -1
- package/instructions/analysis.md +2 -2
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
package/dist/games/analysis.d.ts
CHANGED
|
@@ -38,5 +38,16 @@ export declare function startAnalysis(folder: string, game: string, now: Date, {
|
|
|
38
38
|
export declare function forgetAnalyst(folder: string): void;
|
|
39
39
|
/** The game in analysis, or none when there is none, or analysis.json cannot be read. */
|
|
40
40
|
export declare function gameInAnalysis(folder: string): Analysis | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Stops the analysis of the game in analysis at the player's word on the board page: the page goes back to the game over, status no longer names it, and
|
|
43
|
+
* the review written so far stays on the game. analysis.json keeps the game stopped, with the analyst that held it, until the next analysis starts: their
|
|
44
|
+
* later writes are refused. False when another game, or none, is in analysis.
|
|
45
|
+
*/
|
|
46
|
+
export declare function stopAnalysis(folder: string, game: string): boolean;
|
|
47
|
+
/** The game whose analysis the player stopped on the board page, with the analyst that held it then; none once another analysis starts. */
|
|
48
|
+
export declare function stoppedAnalysis(folder: string): {
|
|
49
|
+
game: string;
|
|
50
|
+
analyst?: AnalystRun;
|
|
51
|
+
} | undefined;
|
|
41
52
|
/** Ends the analysis, when a new game starts or its review is deleted. */
|
|
42
53
|
export declare function endAnalysis(folder: string): void;
|
package/dist/games/analysis.js
CHANGED
|
@@ -50,6 +50,29 @@ export function gameInAnalysis(folder) {
|
|
|
50
50
|
return undefined;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Stops the analysis of the game in analysis at the player's word on the board page: the page goes back to the game over, status no longer names it, and
|
|
55
|
+
* the review written so far stays on the game. analysis.json keeps the game stopped, with the analyst that held it, until the next analysis starts: their
|
|
56
|
+
* later writes are refused. False when another game, or none, is in analysis.
|
|
57
|
+
*/
|
|
58
|
+
export function stopAnalysis(folder, game) {
|
|
59
|
+
const analysis = gameInAnalysis(folder);
|
|
60
|
+
if (analysis?.game !== game) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
writeAtomically(join(folder, fileName), `${JSON.stringify({ stopped: game, ...withAnalyst(analysis.analyst) }, null, 2)}\n`);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/** The game whose analysis the player stopped on the board page, with the analyst that held it then; none once another analysis starts. */
|
|
67
|
+
export function stoppedAnalysis(folder) {
|
|
68
|
+
try {
|
|
69
|
+
const stopped = JSON.parse(readFileSync(join(folder, fileName), "utf8"));
|
|
70
|
+
return typeof stopped.stopped === "string" ? { game: stopped.stopped, ...withAnalyst(isAnalystRun(stopped.analyst) ? stopped.analyst : undefined) } : undefined;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
53
76
|
/** Ends the analysis, when a new game starts or its review is deleted. */
|
|
54
77
|
export function endAnalysis(folder) {
|
|
55
78
|
rmSync(join(folder, fileName), { force: true });
|
package/dist/games/archive.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GameRecord } from "../core/index.ts";
|
|
1
|
+
import { type GameRecord, type Mood } from "../core/index.ts";
|
|
2
2
|
import { type CurrentGame } from "./current-game.ts";
|
|
3
3
|
/** Where the game goes in the archive, in the player's folder: named by when it started, in the player's time, games/2026-11-02-153012.pgn. */
|
|
4
4
|
export declare function archiveName(current: CurrentGame): string;
|
|
@@ -22,6 +22,14 @@ export declare function archiveEnded(folder: string, current: CurrentGame, openi
|
|
|
22
22
|
archive?: string;
|
|
23
23
|
warning?: string;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* Gives the mood the AI ends with to a game over already in the archive, on its last move, beside the review the analyst and Chesspal may have written there
|
|
27
|
+
* since; archives the game when it is not there.
|
|
28
|
+
*/
|
|
29
|
+
export declare function archiveMood(folder: string, current: CurrentGame, mood: Mood, opening: OpeningTags): {
|
|
30
|
+
archive?: string;
|
|
31
|
+
warning?: string;
|
|
32
|
+
};
|
|
25
33
|
/**
|
|
26
34
|
* Writes the coach's judgments into a game over already in the archive, beside what the analyst may have written there since (Federico);
|
|
27
35
|
* archives the game when it is not there.
|
package/dist/games/archive.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The finished games, as PGN in games/ (D-9, D-41), with the AI's plans for the analyst (D-21); and the games played elsewhere, kept to study in study/ (D-68).
|
|
2
2
|
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { movesOnly, pgnDate, readPgn, readPgnTags, readPgnUnplayed, unsigned, withAiHost, withAiModel, writePgn } from "../core/index.js";
|
|
4
|
+
import { movesOnly, pgnDate, readPgn, readPgnTags, readPgnUnplayed, recordWithMood, unsigned, withAiHost, withAiModel, writePgn } from "../core/index.js";
|
|
5
5
|
import { aiHostOf, aiModelOf } from "./ai-model.js";
|
|
6
6
|
import { coachWords, withCoach } from "./coach.js";
|
|
7
7
|
import { endOf, loadGame } from "./current-game.js";
|
|
@@ -65,6 +65,23 @@ export function archiveEnded(folder, current, opening) {
|
|
|
65
65
|
return { warning: `${error.message} The game is saved in game.json: it goes to the archive when a new game starts.` };
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Gives the mood the AI ends with to a game over already in the archive, on its last move, beside the review the analyst and Chesspal may have written there
|
|
70
|
+
* since; archives the game when it is not there.
|
|
71
|
+
*/
|
|
72
|
+
export function archiveMood(folder, current, mood, opening) {
|
|
73
|
+
const name = archivedName(folder, current);
|
|
74
|
+
if (name === undefined) {
|
|
75
|
+
return archiveEnded(folder, current, opening);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
writeAtomically(join(folder, name), writePgn(recordWithMood(archivedGame(folder, name), mood)));
|
|
79
|
+
return { archive: name };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return { warning: `Chesspal could not write the mood into ${name}: ${error.message}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
68
85
|
/**
|
|
69
86
|
* Writes the coach's judgments into a game over already in the archive, beside what the analyst may have written there since (Federico);
|
|
70
87
|
* archives the game when it is not there.
|
|
@@ -28,11 +28,26 @@ export interface TalkLine {
|
|
|
28
28
|
readonly agent?: true;
|
|
29
29
|
readonly shown?: Shown;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* 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. With
|
|
33
|
+
* the variation the player played there (D-116), on the board or set aside.
|
|
34
|
+
*/
|
|
32
35
|
export interface Shown {
|
|
33
36
|
readonly game: string;
|
|
34
37
|
readonly at: number;
|
|
35
38
|
readonly move?: string;
|
|
39
|
+
readonly variation?: ShownVariation;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A variation of the page's (D-116): how many of the game's moves come before it, its moves in SAN, how many of them the board showed (0 with the board back
|
|
43
|
+
* on the game), the line as PGN writes it, 12...Kf8 13.Nxe5 Bxe2, and the move shown numbered, 13.Nxe5.
|
|
44
|
+
*/
|
|
45
|
+
export interface ShownVariation {
|
|
46
|
+
readonly at: number;
|
|
47
|
+
readonly moves: readonly string[];
|
|
48
|
+
readonly cursor: number;
|
|
49
|
+
readonly text: string;
|
|
50
|
+
readonly move?: string;
|
|
36
51
|
}
|
|
37
52
|
/** How a game ended: its result, and why when the record says it or the board decided it. */
|
|
38
53
|
export interface GameEnd {
|
|
@@ -76,5 +91,12 @@ export declare const savedLine: z.ZodObject<{
|
|
|
76
91
|
game: z.ZodString;
|
|
77
92
|
at: z.ZodInt;
|
|
78
93
|
move: z.ZodOptional<z.ZodString>;
|
|
94
|
+
variation: z.ZodOptional<z.ZodObject<{
|
|
95
|
+
at: z.ZodInt;
|
|
96
|
+
moves: z.ZodReadonly<z.ZodArray<z.ZodString>>;
|
|
97
|
+
cursor: z.ZodInt;
|
|
98
|
+
text: z.ZodString;
|
|
99
|
+
move: z.ZodOptional<z.ZodString>;
|
|
100
|
+
}, z.core.$strip>>;
|
|
79
101
|
}, z.core.$strip>>;
|
|
80
102
|
}, z.core.$strip>;
|
|
@@ -94,7 +94,12 @@ export const savedLine = z.object({
|
|
|
94
94
|
ply: z.int().min(0),
|
|
95
95
|
at: z.iso.datetime(),
|
|
96
96
|
agent: z.literal(true).optional(),
|
|
97
|
-
shown: z.object({
|
|
97
|
+
shown: z.object({
|
|
98
|
+
game: z.string(),
|
|
99
|
+
at: z.int().min(0),
|
|
100
|
+
move: z.string().optional(),
|
|
101
|
+
variation: z.object({ at: z.int().min(0), moves: z.array(z.string()).min(1).readonly(), cursor: z.int().min(0), text: z.string(), move: z.string().optional() }).optional(),
|
|
102
|
+
}).optional(),
|
|
98
103
|
});
|
|
99
104
|
const savedClock = z.object({
|
|
100
105
|
base: z.int().positive(),
|
package/dist/games/index.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export { aiHostOf, aiModelOf, forgetTheAi, noteAiHost, noteAiModel } from "./ai-model.ts";
|
|
2
|
-
export { type Analysis, type AnalysisStart, endAnalysis, forgetAnalyst, gameInAnalysis, startAnalysis } from "./analysis.ts";
|
|
3
|
-
export { archiveCoached, archivedGame, archivedGames, archivedGameUnplayed, archivedName, archivedTags, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, GameMissing, gameStemOf, hasListTags, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, type OpeningTags, playerPgn, rewriteGame, startOfNewGame, } from "./archive.ts";
|
|
2
|
+
export { type Analysis, type AnalysisStart, endAnalysis, forgetAnalyst, gameInAnalysis, startAnalysis, stopAnalysis, stoppedAnalysis } from "./analysis.ts";
|
|
3
|
+
export { archiveCoached, archivedGame, archivedGames, archivedGameUnplayed, archivedName, archivedTags, archivedWhen, archiveEnded, archiveGame, archiveMood, archiveName, deleteGame, GameMissing, gameStemOf, hasListTags, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, type OpeningTags, 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, GameChanged, type GameEnd, gameStamp, loadGame, removeGame, rerunIfChanged, saveGame, type Shown, type Speaker, type TalkLine } from "./current-game.ts";
|
|
6
|
+
export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, GameChanged, type GameEnd, gameStamp, loadGame, removeGame, rerunIfChanged, saveGame, type Shown, type ShownVariation, 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
9
|
export { forgetPositionShown, hostProcess, notePositionShown, type PositionShown, positionShownNote } from "./position-shown.ts";
|
package/dist/games/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The player's games on disk: the current one in game.json, the finished ones in games/ (D-27, D-41), the games kept to study in study/ (D-68). Below the commands, above the core.
|
|
2
2
|
export { aiHostOf, aiModelOf, forgetTheAi, noteAiHost, noteAiModel } from "./ai-model.js";
|
|
3
|
-
export { endAnalysis, forgetAnalyst, gameInAnalysis, startAnalysis } from "./analysis.js";
|
|
4
|
-
export { archiveCoached, archivedGame, archivedGames, archivedGameUnplayed, archivedName, archivedTags, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, GameMissing, gameStemOf, hasListTags, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.js";
|
|
3
|
+
export { endAnalysis, forgetAnalyst, gameInAnalysis, startAnalysis, stopAnalysis, stoppedAnalysis } from "./analysis.js";
|
|
4
|
+
export { archiveCoached, archivedGame, archivedGames, archivedGameUnplayed, archivedName, archivedTags, archivedWhen, archiveEnded, archiveGame, archiveMood, archiveName, deleteGame, GameMissing, gameStemOf, hasListTags, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.js";
|
|
5
5
|
export { cleanSince, lastChatStart, saveChatStart } from "./chat-start.js";
|
|
6
6
|
export { coachShown, coachStarted, coachWaitingSince, coachWords, forgetCoach, keepHint, keepJudgment, keepLead, startCoach, withCoach } from "./coach.js";
|
|
7
7
|
export { bothSidesMoved, drawOfferOf, endOf, GameChanged, gameStamp, loadGame, removeGame, rerunIfChanged, saveGame } from "./current-game.js";
|
package/dist/page/app/app.js
CHANGED
|
@@ -17,9 +17,10 @@ import { BadgeCounts } from "../game/badges.js";
|
|
|
17
17
|
import { stepped } from "../game/browse.js";
|
|
18
18
|
import { GameColumn } from "../game/column.js";
|
|
19
19
|
import { howEnded, useDeclined, useJustEnded, useSawEnd } from "../game/ending.js";
|
|
20
|
-
import { Steps } from "../game/move-list.js";
|
|
20
|
+
import { Steps, stepsLine } from "../game/move-list.js";
|
|
21
21
|
import { useOpeningBook } from "../game/opening-book.js";
|
|
22
22
|
import { aiSpoke, aiWords, analysisTalk, judgmentsOf, playerSpent, playerWords, pliesOf, reviewCount, withBook, withJudgments, withoutReviews } from "../game/plies.js";
|
|
23
|
+
import { afterMove, isJump, leaveLine, linePlies, lineRecord, onLine, stepLine } from "../game/variation.js";
|
|
23
24
|
import { GuideDialog } from "../guide/guide.js";
|
|
24
25
|
import { BareButton, HelpsCard, HintLine, MapButton } from "../helps/helps.js";
|
|
25
26
|
import { helpsOf } from "../helps/view.js";
|
|
@@ -42,7 +43,7 @@ import { AiNameButton, ChatDialog, tableNote } from "../table/window.js";
|
|
|
42
43
|
import { AiNote, AiWords, Compose, PointSquare, TalkFaces } from "../talk/talk.js";
|
|
43
44
|
import { textFor } from "../text.js";
|
|
44
45
|
import { anchorName, useAnchor } from "./anchor.js";
|
|
45
|
-
import { useBrowseKeys, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
|
|
46
|
+
import { useBrowseKeys, useEscapeKey, useFoldKeys, useLegendKey, useMapKey, useWheelSteps } from "./keys.js";
|
|
46
47
|
import { helpsShown, phaseOf, verdictOf } from "./phase.js";
|
|
47
48
|
import { ask, post, refusedText, sendMessage, sendMove, useServed } from "./served.js";
|
|
48
49
|
import { tabTitle, useTabTitle } from "./title.js";
|
|
@@ -72,7 +73,7 @@ export function App({ start }) {
|
|
|
72
73
|
};
|
|
73
74
|
const [dialog, setDialog] = useState(undefined);
|
|
74
75
|
const { data: served, problem, offline, reload } = useServed(() => setDialog("new-game"));
|
|
75
|
-
// Nuova partita with no chat at the table asks for one first (D-107): the chat's window, then the new game's
|
|
76
|
+
// Nuova partita with no chat at the table asks for one first (D-107): the chat's window, then the new game's once a chat starts; closed without, neither.
|
|
76
77
|
const [chatFirst, setChatFirst] = useState(false);
|
|
77
78
|
// At the table with no chat, and none opening: what asks for the AI opens the chat's window first, to choose its host and model (D-107, D-109).
|
|
78
79
|
const chatMissing = () => {
|
|
@@ -92,10 +93,14 @@ export function App({ start }) {
|
|
|
92
93
|
setChatToReview(served?.analysis !== undefined);
|
|
93
94
|
setDialog("chat");
|
|
94
95
|
};
|
|
95
|
-
const
|
|
96
|
+
const chatStarted = () => {
|
|
96
97
|
setDialog(chatFirst ? "new-game" : undefined);
|
|
97
98
|
setChatFirst(false);
|
|
98
99
|
};
|
|
100
|
+
const closeChat = () => {
|
|
101
|
+
setDialog(undefined);
|
|
102
|
+
setChatFirst(false);
|
|
103
|
+
};
|
|
99
104
|
const [selected, setSelected] = useState(undefined);
|
|
100
105
|
const [refusal, setRefusal] = useState(undefined);
|
|
101
106
|
// A link the page did not follow says why on the board, in the language of the settings the page has by then, until the player's next click or key.
|
|
@@ -169,7 +174,7 @@ export function App({ start }) {
|
|
|
169
174
|
const playedAs = studied === undefined ? name : aiNameOf(aiHostIn(live.record), aiModelIn(live.record)?.model) ?? text.ai;
|
|
170
175
|
const analyst = annotator === undefined ? name : aiNameOf(annotator.annotator, annotator.annotator) ?? text.ai;
|
|
171
176
|
const material = useMemo(() => materialOf(live.position.fen), [live]);
|
|
172
|
-
const
|
|
177
|
+
const piecesOnBoard = useMemo(() => piecesOf(live.position.fen), [live]);
|
|
173
178
|
// How many of the game's first plies a line of the book reaches, as the server counts them (D-68).
|
|
174
179
|
const bookPlies = studied !== undefined ? studied.book : game?.book ?? 0;
|
|
175
180
|
// While the game goes on, the book's first moves carry its badge, when the player wants it, as the AI's review gives it after (D-69).
|
|
@@ -203,13 +208,27 @@ export function App({ start }) {
|
|
|
203
208
|
// The legend of the marks on the board, open until the player closes it (D-99).
|
|
204
209
|
const [legendOpen, setLegendOpen] = useState(false);
|
|
205
210
|
useLegendKey(() => setLegendOpen((open) => !open));
|
|
211
|
+
// In analysis the player plays a variation from the move shown, White's moves and Black's (D-116): one line, kept by the page alone, faded once the board
|
|
212
|
+
// is back on the game.
|
|
213
|
+
const [line, setLine] = useState(undefined);
|
|
214
|
+
const analysing = studied !== undefined;
|
|
215
|
+
const onTheLine = analysing && onLine(line);
|
|
206
216
|
const past = useMemo(() => viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing) }), [live, viewing]);
|
|
207
|
-
const
|
|
208
|
-
const shown = useMemo(() => past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past]);
|
|
209
|
-
//
|
|
210
|
-
|
|
217
|
+
const lineGame = useMemo(() => onTheLine ? new Game(lineRecord(live.record, line)) : undefined, [live, onTheLine ? line : undefined]);
|
|
218
|
+
const shown = useMemo(() => lineGame ?? past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past, lineGame]);
|
|
219
|
+
// The move after the position shown, which a step back slides home: the line's own on the line.
|
|
220
|
+
const next = useMemo(() => onTheLine ? shown.legalMoves().find((move) => move.san === line.moves[line.cursor]) : viewing === undefined ? undefined : new Game({ tags: live.record.tags, moves: live.record.moves.slice(0, viewing + 1) }).lastMove, [live, viewing, shown]);
|
|
221
|
+
// Where the line starts its numbers when it leaves from the game's first position.
|
|
222
|
+
const firstPosition = useMemo(() => new Game({ tags: live.record.tags, moves: [] }).position, [live]);
|
|
223
|
+
// A square lit from the AI's words goes off with the position: a touch keeps it lit until then, a mouse until it leaves the word; in analysis a refusal
|
|
224
|
+
// goes off with it too.
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
setPointed(noSquares);
|
|
227
|
+
if (analysing) {
|
|
228
|
+
setRefusal(undefined);
|
|
229
|
+
}
|
|
230
|
+
}, [shown]);
|
|
211
231
|
// The player sees all of Chesspal's facts, whatever the AI's level (D-79): in a game with the helps on, always in analysis (D-114).
|
|
212
|
-
const analysing = studied !== undefined;
|
|
213
232
|
const helped = helpsShown(phase, settings.helps);
|
|
214
233
|
// Seen from the player's side (D-91): theirs in a game, in analysis the side the game names theirs, none when it names neither.
|
|
215
234
|
const you = analysing ? playerIn(studiedSides(live.record.tags, served?.profile.name ?? "", playedAs, text.sides, served?.profile.accounts)) : game?.ai === undefined ? undefined : game.ai === "white" ? "black" : "white";
|
|
@@ -225,6 +244,7 @@ export function App({ start }) {
|
|
|
225
244
|
setPending(undefined);
|
|
226
245
|
setViewing(undefined);
|
|
227
246
|
setPromoting(undefined);
|
|
247
|
+
setLine(undefined);
|
|
228
248
|
}, [gameKey]);
|
|
229
249
|
const played = studied === undefined ? game : null;
|
|
230
250
|
useGameSounds(played, replayed, sound);
|
|
@@ -252,16 +272,49 @@ export function App({ start }) {
|
|
|
252
272
|
const shownOpening = useMemo(() => bookAt === undefined || book === undefined ? undefined : openingOf(bookAt, book), [live, book, bookAt === undefined ? 0 : at]);
|
|
253
273
|
// Whether the move shown is still the book's, whatever the badges show: the card says the move the game left it at, once it is past it.
|
|
254
274
|
const leftBook = outOfBookMove(at, bookPlies);
|
|
255
|
-
// A jump of more than one move shows the position at once: only a step slides its pieces.
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
shownBefore.current
|
|
275
|
+
// A jump of more than one move shows the position at once: only a step slides its pieces, on the game or on a variation.
|
|
276
|
+
const place = { at, cursor: onTheLine ? line.cursor : 0 };
|
|
277
|
+
const shownBefore = useRef(place);
|
|
278
|
+
const jumped = isJump(shownBefore.current, place);
|
|
279
|
+
shownBefore.current = place;
|
|
280
|
+
// A move of the game shown takes the board off a variation, which stays faded.
|
|
259
281
|
const goTo = (count) => {
|
|
260
282
|
setSelected(undefined);
|
|
283
|
+
setLine(leaveLine);
|
|
261
284
|
setViewing(count >= total ? undefined : Math.max(0, count));
|
|
262
285
|
};
|
|
263
|
-
|
|
286
|
+
// On a variation the arrows back and forward step through it; to either end of the game they leave it.
|
|
287
|
+
const step = (to) => {
|
|
288
|
+
if (onTheLine && (to === "back" || to === "forward")) {
|
|
289
|
+
setSelected(undefined);
|
|
290
|
+
setLine(stepLine(line, to));
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
goTo(stepped(to, at, total));
|
|
294
|
+
}
|
|
295
|
+
};
|
|
264
296
|
useBrowseKeys(step, () => setFlipped(!flipped));
|
|
297
|
+
// A move of the variation shown: the board goes back on it, at the game's move it leaves from.
|
|
298
|
+
const showLine = (cursor) => {
|
|
299
|
+
if (line !== undefined) {
|
|
300
|
+
setSelected(undefined);
|
|
301
|
+
setLine({ ...line, cursor });
|
|
302
|
+
setViewing(line.at >= total ? undefined : line.at);
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
// A variation a line of the talk was written on goes back on the board, at the move it showed, or at its end when it was set aside (D-116).
|
|
306
|
+
const putLineBack = (sent) => {
|
|
307
|
+
setSelected(undefined);
|
|
308
|
+
setLine({ at: sent.at, moves: sent.moves, cursor: sent.cursor > 0 ? sent.cursor : sent.moves.length });
|
|
309
|
+
setViewing(sent.at >= total ? undefined : sent.at);
|
|
310
|
+
};
|
|
311
|
+
// The cross and Esc close the variation: the board shows the game where it left it.
|
|
312
|
+
const closeLine = () => {
|
|
313
|
+
setSelected(undefined);
|
|
314
|
+
setLine(undefined);
|
|
315
|
+
};
|
|
316
|
+
// Esc first lets go of a piece picked, and leaves the legend, a promotion's choice, the settings and the dialogs to close themselves.
|
|
317
|
+
useEscapeKey(line === undefined || legendOpen || promoting !== undefined || setting || dialog !== undefined ? undefined : () => selected === undefined ? closeLine() : setSelected(undefined));
|
|
265
318
|
const wheel = useWheelSteps(step);
|
|
266
319
|
// The move shown stays in the page's address, for a reload and a link to it; after the reset on a new game, which would undo the move it goes to. A
|
|
267
320
|
// game a link opens is there to look at: the analyst starts on the player's yes alone (D-89; Zoe and Federico).
|
|
@@ -269,7 +322,8 @@ export function App({ start }) {
|
|
|
269
322
|
useAnchor({ ready: served !== undefined, shown: studied !== undefined ? anchorName(studied.name) : idle ? undefined : playedName, played: playedName, going }, viewing, (plies) => goTo(plies ?? total), async (wanted) => await post("/api/study", { game: wanted, look: true }, text, name) === undefined, setLinkIgnored);
|
|
270
323
|
// The tab names the game and the move shown, as the cards name its players; between games, Chesspal alone.
|
|
271
324
|
const titled = idle || served === undefined ? undefined : studiedSides(live.record.tags, served.profile.name, playedAs, text.sides, served.profile.accounts, othersOf(served.profiles, served.profile.id));
|
|
272
|
-
|
|
325
|
+
const lineNumbered = line === undefined ? undefined : linePlies(line, lineStartOf(plies, line.at, firstPosition));
|
|
326
|
+
useTabTitle(titled === undefined ? undefined : onTheLine ? tabTitle(titled.white.name, titled.black.name, lineNumbered?.[line.cursor - 1], true) : tabTitle(titled.white.name, titled.black.name, at === 0 ? undefined : plies[at - 1]));
|
|
273
327
|
// The header's theme switch picks the other of the two the page shows, leaving the system's.
|
|
274
328
|
const switchTheme = () => change({ ...settings, theme: document.documentElement.dataset.theme === "dark" ? "light" : "dark" });
|
|
275
329
|
// An action of the player's on the game: the game the server sends next shows it; a refusal shows on the board.
|
|
@@ -322,7 +376,8 @@ export function App({ start }) {
|
|
|
322
376
|
const mood = moodAt(live.record, sides?.white.who === "ai" ? "white" : sides?.black.who === "ai" ? "black" : ai, at).mood;
|
|
323
377
|
const playerToMove = going && live.position.turn === player;
|
|
324
378
|
const ahead = (side) => side === "white" ? material.balance : -material.balance;
|
|
325
|
-
|
|
379
|
+
// On a variation the AI's badges, notes and drawings say nothing: they are of the game's moves (D-116).
|
|
380
|
+
const reviewed = onTheLine ? undefined : judged || at <= inBook ? plies[at - 1] : undefined;
|
|
326
381
|
// With the helps on, the coach's judgments of the player's last move, once the AI has answered it, and of one a held note let go only now (D-78, D-88).
|
|
327
382
|
const judgments = going && settings.helps ? judgmentsOf(plies, player, coached ?? noCoach) : [];
|
|
328
383
|
const review = reviewed?.review;
|
|
@@ -330,7 +385,7 @@ export function App({ start }) {
|
|
|
330
385
|
const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
|
|
331
386
|
const drawnByCoach = hint !== undefined && drawn === hint;
|
|
332
387
|
// The game over, when this is its last position: the kings' signs, on the board and in the legend.
|
|
333
|
-
const boardEnd = ended && !aborted && viewing === undefined && pending === undefined ? { how, winner, player, fresh: justEnded } : undefined;
|
|
388
|
+
const boardEnd = ended && !aborted && viewing === undefined && pending === undefined && !onTheLine ? { how, winner, player, fresh: justEnded } : undefined;
|
|
334
389
|
// The legend has the marks the board shows: none of the helps' or the drawings while the board is bare (D-99).
|
|
335
390
|
const shownBare = bare && helps !== undefined;
|
|
336
391
|
const legendRows = legendOf({
|
|
@@ -349,6 +404,8 @@ export function App({ start }) {
|
|
|
349
404
|
const reviewing = summary === undefined && studied?.reviewing === true && !offline;
|
|
350
405
|
// With nobody at it, a review not finished says so, and the player brings the game back to the AI (D-109).
|
|
351
406
|
const completeIt = summary === undefined && studied?.incomplete === true && !offline ? () => void studyGame({ game: studied.name }) : undefined;
|
|
407
|
+
// While it waits, the player may cancel the analysis asked, one click: what is written of the review stays, and the page goes back to the game.
|
|
408
|
+
const stopIt = reviewing ? () => act("/api/stop-analysis", { game: studied.name }) : undefined;
|
|
352
409
|
const closing = studied === undefined ? undefined : closingOf(live.record);
|
|
353
410
|
// How far the review has got, counted as status counts it for the AI: who is ahead after how many of the game's moves.
|
|
354
411
|
const count = reviewing || completeIt !== undefined ? reviewCount(plies) : undefined;
|
|
@@ -397,7 +454,7 @@ export function App({ start }) {
|
|
|
397
454
|
const play = (from, to, piece) => {
|
|
398
455
|
setSelected(undefined);
|
|
399
456
|
setRefusal(undefined);
|
|
400
|
-
if (piece === undefined && promotes(
|
|
457
|
+
if (piece === undefined && promotes(piecesOnBoard.get(from), to)) {
|
|
401
458
|
setPromoting({ from, to });
|
|
402
459
|
return;
|
|
403
460
|
}
|
|
@@ -416,9 +473,35 @@ export function App({ start }) {
|
|
|
416
473
|
}
|
|
417
474
|
});
|
|
418
475
|
};
|
|
419
|
-
//
|
|
476
|
+
// In analysis the player moves from the position shown, White's pieces and Black's, and a variation grows (D-116); the game's own next move only steps
|
|
477
|
+
// forward through it. The move sounds as the board stands after it.
|
|
478
|
+
const explore = (from, to, piece) => {
|
|
479
|
+
setSelected(undefined);
|
|
480
|
+
setRefusal(undefined);
|
|
481
|
+
if (piece === undefined && promotes(piecesOf(shown.position.fen).get(from), to)) {
|
|
482
|
+
setPromoting({ from, to });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
setPromoting(undefined);
|
|
486
|
+
const move = shown.legalMoves(from).find((legal) => legal.to === to && legal.promotion === piece);
|
|
487
|
+
if (move === undefined) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const after = afterMove(line, live.record.moves.map(({ san }) => san), at, move.san);
|
|
491
|
+
setLine(after.line);
|
|
492
|
+
setViewing(after.shown >= total ? undefined : after.shown);
|
|
493
|
+
const played = new Game(shown.record);
|
|
494
|
+
const moveSound = played.play(move.san).played && sound ? soundOf(played) : undefined;
|
|
495
|
+
if (moveSound !== undefined) {
|
|
496
|
+
playSound(moveSound);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
// A move the board refuses says why, without the AI (D-18), unless the player turned it off: the piece goes back, the reason over it. In analysis, from
|
|
500
|
+
// the position shown.
|
|
420
501
|
const refuse = (from, to) => {
|
|
421
|
-
const
|
|
502
|
+
const board = analysing ? shown : live;
|
|
503
|
+
const pieces = analysing ? piecesOf(shown.position.fen) : piecesOnBoard;
|
|
504
|
+
const illegal = settings.explain ? board.whyNot(from, to) : undefined;
|
|
422
505
|
if (illegal !== undefined) {
|
|
423
506
|
const letterOn = (square) => pieces.get(square).toUpperCase();
|
|
424
507
|
const sideOn = (square) => pieces.get(square) === letterOn(square) ? "white" : "black";
|
|
@@ -428,7 +511,7 @@ export function App({ start }) {
|
|
|
428
511
|
setRefusal({ text: words, square: from, piece: pieces.get(from), explanation: explain(illegal, to, king) });
|
|
429
512
|
}
|
|
430
513
|
};
|
|
431
|
-
// In analysis the words go with the game and the move shown (D-101).
|
|
514
|
+
// In analysis the words go with the game and the move shown (D-101), and the variation played there, on the board or set aside (D-116).
|
|
432
515
|
const sendWords = (position) => {
|
|
433
516
|
const words = draft;
|
|
434
517
|
// /clear and the other commands are for Claude's app: written here they would reach the AI as words (Sebastiano's playtest).
|
|
@@ -453,6 +536,29 @@ export function App({ start }) {
|
|
|
453
536
|
};
|
|
454
537
|
// 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).
|
|
455
538
|
const studiedCard = (side, place) => sides === undefined ? null : (_jsx(PlayerCard, { place: place, picture: pictureOf(sides[side], profile, served.profiles, 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 }));
|
|
539
|
+
// Where the words go in analysis: the game, the move shown, and the variation, on the board or set aside (D-116).
|
|
540
|
+
const shownIn = (game) => line === undefined ? { game, at } : { game, at, variation: line };
|
|
541
|
+
// «Analyse» on the variation sends the player's request, as their words would (D-116).
|
|
542
|
+
const analyseLine = () => {
|
|
543
|
+
if (studied !== undefined) {
|
|
544
|
+
setUnsent(undefined);
|
|
545
|
+
void sendMessage(text.variation.analyseWords, text, name, shownIn(studied.name)).then((refused) => setUnsent(refused?.text));
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
// The variation as the column and the steps show it.
|
|
549
|
+
const lineView = !analysing || line === undefined || lineNumbered === undefined
|
|
550
|
+
? undefined
|
|
551
|
+
: {
|
|
552
|
+
plies: lineNumbered,
|
|
553
|
+
at: line.at,
|
|
554
|
+
cursor: line.cursor,
|
|
555
|
+
onShow: showLine,
|
|
556
|
+
onClose: closeLine,
|
|
557
|
+
onBack: () => goTo(line.at),
|
|
558
|
+
onAnalyse: analyseLine,
|
|
559
|
+
canAnalyse: !asleep && !offline,
|
|
560
|
+
analyst: name,
|
|
561
|
+
};
|
|
456
562
|
// A game brought to analyse: the page shows it once the server has it in analysis; a refusal stays in the dialog. One the AI has to go over, with no
|
|
457
563
|
// chat at the table, opens the chat's window: the analyst works with the chat's model (D-109).
|
|
458
564
|
const studyGame = async (studied) => {
|
|
@@ -465,7 +571,7 @@ export function App({ start }) {
|
|
|
465
571
|
setDialog(toReview ? "chat" : undefined);
|
|
466
572
|
return undefined;
|
|
467
573
|
};
|
|
468
|
-
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", 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, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", review: chatToReview, table: table, settings: settings, onChange: change, name: name, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), onProfile: () => setDialog("profile"), you: profile.name, accounts: profile.accounts, others: othersOf(served.profiles, profile.id), ai: name, text: text }), served.update === undefined || served.table === undefined
|
|
574
|
+
return (_jsx(ChosenSet.Provider, { value: settings.set, children: _jsxs(PointSquare.Provider, { value: pointSquare, children: [panel, _jsx(ProfileDialog, { open: dialog === "profile", 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, onEdit: () => setDialog("profile"), onClose: () => setDialog(undefined), text: text }), table === undefined ? null : _jsx(ChatDialog, { open: dialog === "chat", review: chatToReview, table: table, settings: settings, onChange: change, name: name, onStarted: chatStarted, onClose: closeChat, text: text }), _jsx(NewGameDialog, { open: dialog === "new-game", choice: choice, going: going, onStart: startNewGame, onClose: () => setDialog(undefined), text: text }), guide, _jsx(StudyDialog, { open: dialog === "study", onStudy: studyGame, onClose: () => setDialog(undefined), onProfile: () => setDialog("profile"), you: profile.name, accounts: profile.accounts, others: othersOf(served.profiles, profile.id), ai: name, text: text }), served.update === undefined || served.table === undefined
|
|
469
575
|
? null
|
|
470
576
|
: (_jsx(UpdateDialog, { open: dialog === "update", version: served.update.version, current: served.version, installing: served.update.installing === true, installed: served.update.installed === true, going: going, chat: served.table.chat ? (served.table.held ? "held" : "byHand") : undefined, name: name, onClose: () => setDialog(undefined), text: text })), offline ? _jsx("p", { class: "offline", role: "alert", children: text.offline(name) }) : null, served.outdated === true
|
|
471
577
|
? _jsx("p", { class: "outdated", role: "status", children: text.outdated(name) })
|
|
@@ -474,10 +580,10 @@ export function App({ start }) {
|
|
|
474
580
|
: null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
|
|
475
581
|
? (_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 }))
|
|
476
582
|
: (_jsx(TalkFaces, { name: sides[ai].name, aiPicture: pictureOf(sides[ai], profile, served.profiles, 40, mood), you: sides[player].name, picture: pictureOf(sides[player], profile, served.profiles, 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
|
|
477
|
-
? (_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 } : {}), ...(reviewing ? { reviewing: true } : {}), ...(count === undefined ? {} : { count }), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(voiced ? { voice: text } : {}), 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) => {
|
|
583
|
+
? (_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 || onTheLine ? {} : { opening: shownOpening }), ...(at === total && closing !== undefined && !onTheLine ? { closing } : {}), ...(at === 0 && summary !== undefined && !onTheLine ? { summary } : {}), ...(reviewing ? { reviewing: true } : {}), ...(count === undefined ? {} : { count }), ...(completeIt === undefined ? {} : { onComplete: completeIt }), name: analyst, by: annotator, mood: mood, ...(voiced ? { voice: text } : {}), 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) => {
|
|
478
584
|
setDraft(words);
|
|
479
585
|
setUnsent(undefined);
|
|
480
|
-
}, onSend: () => sendWords(
|
|
586
|
+
}, onSend: () => sendWords(shownIn(studied.name)), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo, onLine: putLineBack }, ...(chatName === undefined ? {} : { to: chatName }), text: text }) }), studiedCard(player, "you")] })] }))
|
|
481
587
|
: (_jsxs(_Fragment, { children: [_jsxs("section", { class: "ai-block", "aria-label": name, children: [_jsx(PlayerCard, { place: "ai", picture: thinker(48), name: table === undefined ? name : _jsx(AiNameButton, { name: name, table: table, onOpen: askChat, text: text }), 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, ...(voiced ? { voice: text } : {}) })] }), _jsxs("section", { class: "you-block", "aria-label": profile.name, children: [_jsx(Compose, { lines: said, newestFirst: flipped,
|
|
482
588
|
// Between games, and before the first, the player writes freely, a line at a time, as the AI does (D-74, D-77).
|
|
483
589
|
spent: going ? playerSpent(plies, ai, talk) : 0, draft: draft, onDraft: (words) => {
|
|
@@ -485,19 +591,27 @@ export function App({ start }) {
|
|
|
485
591
|
setUnsent(undefined);
|
|
486
592
|
}, onSend: sendWords, canMove: playerToMove, name: name, problem: unsent, text: text }), _jsx(PlayerCard, { place: "you", picture: picture, name: _jsx(NameButton, { profile: profile, onOpen: () => setDialog("profiles"), text: text }), side: idle ? undefined : player, taken: material.taken[player], ahead: ahead(player), turn: playerToMove, clock: _jsxs(_Fragment, { children: [clockOf(player), startClocks, nextGame] }), text: text })] })] })) }), _jsxs("div", {
|
|
487
593
|
// 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.
|
|
488
|
-
|
|
594
|
+
// A variation rings the board in the accent (D-116).
|
|
595
|
+
class: between && !justEnded ? "board-wrap off" : onTheLine ? "board-wrap variation" : "board-wrap", onWheel: wheel,
|
|
489
596
|
// A press on the board while the player looks back brings the game as it is: the next one plays.
|
|
490
|
-
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
|
|
597
|
+
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: analysing ? (shown.outcome.over ? undefined : shown.position.turn) : playerToMove && pending === undefined && viewing === undefined ? player : undefined, selected: selected, targets: selected === undefined ? [] : analysing ? shown.legalMoves(selected) : viewing !== undefined ? [] : live.legalMoves(selected), dots: settings.dots, names: settings.squareNames, notice: viewing !== undefined && !analysing ? undefined : refusal ?? (linkIgnored === undefined ? undefined : { text: text.link[linkIgnored] }), promotion: promoting === undefined
|
|
598
|
+
? undefined
|
|
599
|
+
: analysing
|
|
600
|
+
? { ...promoting, side: shown.position.turn }
|
|
601
|
+
: viewing !== undefined || !playerToMove
|
|
602
|
+
? undefined
|
|
603
|
+
: { ...promoting, side: player }, onPromote: (piece) => piece === undefined ? setPromoting(undefined) : (analysing ? explore : play)(promoting.from, promoting.to, piece), onSelect: select, onMove: analysing ? explore : play, onRefuse: refuse, text: text }), folds.talk !== "open" && (asleepNote !== null || clockWaits)
|
|
491
604
|
? (_jsxs("div", { class: "over-board", children: [asleepNote, startClocks] }))
|
|
492
|
-
: null] }), studied === undefined ? null : (_jsxs("div", { class: "board-steps", children: [_jsx(Steps, { shown: at, total: total, onStep: step, text: text }), standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: at, onShow: goTo, 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, ...(leftBook === undefined ? {} : { outOfBook: leftBook }), 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 ? {} : {
|
|
605
|
+
: null] }), studied === undefined ? null : (_jsxs("div", { class: "board-steps", children: [_jsx(Steps, { shown: at, total: total, ...stepsLine(lineView, plies), onStep: step, text: text }), standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: at, onShow: goTo, 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, ...(leftBook === undefined ? {} : { outOfBook: leftBook }), 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 ? {} : {
|
|
493
606
|
analysis: {
|
|
494
607
|
by: analyst,
|
|
495
608
|
...(summary === undefined ? {} : { summary }),
|
|
496
609
|
...(reviewing ? { reviewing: true } : {}),
|
|
497
610
|
...(count === undefined ? {} : { count }),
|
|
611
|
+
...(stopIt === undefined ? {} : { onStop: stopIt }),
|
|
498
612
|
...(completeIt === undefined ? {} : { onComplete: completeIt }),
|
|
499
613
|
},
|
|
500
|
-
}), 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 && replayed.record.moves.length > 0 ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text }), _jsx(PageFoot, { version: served.version, text: text })] })] }) }));
|
|
614
|
+
}), shownPlies: at, ...(lineView === undefined ? {} : { line: lineView }), 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 && replayed.record.moves.length > 0 ? { onAnalyse: () => void studyGame({ last: true }) } : {}), text: text }), _jsx(PageFoot, { version: served.version, text: text })] })] }) }));
|
|
501
615
|
}
|
|
502
616
|
/** The picture of a player of the game in analysis: the player's own, the AI's Torretta with its mood, another profile's, or the plain face of someone else. */
|
|
503
617
|
function pictureOf({ who, profile: other }, profile, profiles, size, mood) {
|
|
@@ -580,6 +694,11 @@ function withMove(game, move) {
|
|
|
580
694
|
const copy = new Game(game.record);
|
|
581
695
|
return copy.play(move).played ? copy : game;
|
|
582
696
|
}
|
|
697
|
+
/** Where a variation that leaves the game after `at` moves starts its numbers: after the game's move there, or from the game's first position. */
|
|
698
|
+
function lineStartOf(plies, at, first) {
|
|
699
|
+
const fork = plies[at - 1];
|
|
700
|
+
return fork === undefined ? first : fork.side === "white" ? { moveNumber: fork.number, turn: "black" } : { moveNumber: fork.number + 1, turn: "white" };
|
|
701
|
+
}
|
|
583
702
|
/** Whether a pawn's move takes it to the last rank, where the player chooses what it becomes. */
|
|
584
703
|
function promotes(piece, to) {
|
|
585
704
|
return (piece === "P" && to.endsWith("8")) || (piece === "p" && to.endsWith("1"));
|
package/dist/page/app/keys.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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.
|
|
2
|
+
// through the moves and f and x turn the board, Esc closes a variation, 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";
|
|
@@ -87,6 +87,21 @@ export function useBrowseKeys(step, flip) {
|
|
|
87
87
|
return () => window.removeEventListener("keydown", press);
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/** Esc closes what `close` closes, a variation in analysis (D-116), when nothing else on the page takes it: none while it is undefined. */
|
|
91
|
+
export function useEscapeKey(close) {
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (close === undefined) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const press = (event) => {
|
|
97
|
+
if (event.key === "Escape" && forThePage(event)) {
|
|
98
|
+
close();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
window.addEventListener("keydown", press);
|
|
102
|
+
return () => window.removeEventListener("keydown", press);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
90
105
|
/** The mouse wheel over the board steps through the moves, as on Lichess: down forward, a step a notch, however finely a touchpad turns it. */
|
|
91
106
|
export function useWheelSteps(step) {
|
|
92
107
|
const turned = useRef(0);
|
package/dist/page/app/title.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// The tab's title names the game and the move shown, Stefano – Claude, 16. ♞f3, so that the player's bookmarks and tabs tell its games apart.
|
|
2
2
|
import { useEffect } from "preact/hooks";
|
|
3
3
|
import { figurinesOf } from "../../core/index.js";
|
|
4
|
-
/**
|
|
5
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The title of a game: its players by their first names, White first, so that the move fits in a tab (Zoe), and the move shown with its number, none before
|
|
6
|
+
* the first; a move of a variation in analysis after its fork, ⑂ (D-116).
|
|
7
|
+
*/
|
|
8
|
+
export function tabTitle(white, black, shown, line = false) {
|
|
6
9
|
const players = `${firstName(white)} – ${firstName(black)}`;
|
|
7
|
-
return shown === undefined ? players : `${players}, ${shown.number}${shown.side === "white" ? "." : "..."} ${figurinesOf(shown.san)}`;
|
|
10
|
+
return shown === undefined ? players : `${players}, ${line ? "⑂ " : ""}${shown.number}${shown.side === "white" ? "." : "..."} ${figurinesOf(shown.san)}`;
|
|
8
11
|
}
|
|
9
12
|
/** Keeps the tab's title, Chesspal's name while no game shows. */
|
|
10
13
|
export function useTabTitle(title) {
|
|
@@ -960,3 +960,9 @@
|
|
|
960
960
|
transition: opacity 400ms ease, --kept 400ms ease;
|
|
961
961
|
}
|
|
962
962
|
}
|
|
963
|
+
|
|
964
|
+
/* A variation in analysis rings the board in the accent, just outside its edge, on the page, where every skin leaves it in sight: nothing else changes (D-116) */
|
|
965
|
+
.board-wrap.variation > .board {
|
|
966
|
+
outline: 3px solid var(--accent);
|
|
967
|
+
outline-offset: 2px;
|
|
968
|
+
}
|