chesspal 0.4.1 → 0.6.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 +37 -8
- package/dist/cli/chat-hook.d.ts +4 -1
- package/dist/cli/chat-hook.js +12 -5
- package/dist/cli/cli.js +3 -3
- package/dist/cli/command-line.js +9 -9
- package/dist/commands/game/new.js +3 -5
- package/dist/commands/registry/context.d.ts +5 -0
- package/dist/commands/registry/context.js +2 -1
- package/dist/commands/setup/init.d.ts +4 -1
- package/dist/commands/setup/init.js +30 -10
- package/dist/core/review.d.ts +2 -2
- package/dist/core/review.js +7 -3
- package/dist/host-files/host-files.d.ts +10 -6
- package/dist/host-files/host-files.js +57 -9
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/app.js +112 -25
- 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 +3 -3
- package/dist/page/game/game.css +121 -1
- package/dist/page/game/move-list.js +48 -10
- package/dist/page/game/variation.js +65 -0
- package/dist/page/guide/en.md +3 -2
- package/dist/page/guide/it.md +3 -2
- package/dist/page/profile/settings.js +2 -0
- package/dist/page/table/action.js +19 -7
- package/dist/page/table/window.js +7 -5
- package/dist/page/text.js +4 -2
- package/dist/server/bell/antigravity.d.ts +10 -0
- package/dist/server/bell/antigravity.js +75 -0
- package/dist/server/bell/bell.d.ts +10 -2
- package/dist/server/bell/bell.js +33 -10
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -1
- package/dist/server/mcp/caller.js +26 -1
- package/dist/server/page/game.js +4 -3
- package/dist/server/page/page.js +1 -0
- package/dist/server/page/table.d.ts +14 -2
- package/dist/server/page/table.js +17 -5
- package/dist/server/table/hosts/chats.d.ts +4 -2
- package/dist/server/table/hosts/chats.js +24 -11
- package/dist/server/table/hosts/find.d.ts +6 -0
- package/dist/server/table/hosts/find.js +19 -1
- package/dist/server/table/hosts/hooks.d.ts +5 -2
- package/dist/server/table/hosts/hooks.js +6 -3
- package/dist/server/table/hosts/hosts.d.ts +19 -10
- package/dist/server/table/hosts/hosts.js +34 -16
- package/dist/server/table/hosts/models.d.ts +19 -0
- package/dist/server/table/hosts/models.js +95 -0
- package/dist/server/table/hosts/play.d.ts +7 -2
- package/dist/server/table/hosts/play.js +22 -12
- package/dist/server/table/hosts/seat.d.ts +5 -1
- package/dist/server/table/hosts/seat.js +1 -1
- package/dist/server/table/table.js +1 -1
- package/package.json +1 -1
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";
|
|
@@ -131,7 +132,7 @@ export function App({ start }) {
|
|
|
131
132
|
apply(settingsFrom(served.settings));
|
|
132
133
|
}
|
|
133
134
|
}, [profileId]);
|
|
134
|
-
// The AI as the host names it (Claude, Codex), also where the page sends the player to write; the last known while the page is offline.
|
|
135
|
+
// The AI as the host names it (Claude, Codex, Gemini), also where the page sends the player to write; the last known while the page is offline.
|
|
135
136
|
const name = served?.opponent ?? text.ai;
|
|
136
137
|
// Under it, the model and effort the AI played its last move with (D-82).
|
|
137
138
|
const signed = served?.aiModel === undefined ? undefined : signature(served.aiModel.model, served.aiModel.effort);
|
|
@@ -169,7 +170,7 @@ export function App({ start }) {
|
|
|
169
170
|
const playedAs = studied === undefined ? name : aiNameOf(aiHostIn(live.record), aiModelIn(live.record)?.model) ?? text.ai;
|
|
170
171
|
const analyst = annotator === undefined ? name : aiNameOf(annotator.annotator, annotator.annotator) ?? text.ai;
|
|
171
172
|
const material = useMemo(() => materialOf(live.position.fen), [live]);
|
|
172
|
-
const
|
|
173
|
+
const piecesOnBoard = useMemo(() => piecesOf(live.position.fen), [live]);
|
|
173
174
|
// How many of the game's first plies a line of the book reaches, as the server counts them (D-68).
|
|
174
175
|
const bookPlies = studied !== undefined ? studied.book : game?.book ?? 0;
|
|
175
176
|
// 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 +204,27 @@ export function App({ start }) {
|
|
|
203
204
|
// The legend of the marks on the board, open until the player closes it (D-99).
|
|
204
205
|
const [legendOpen, setLegendOpen] = useState(false);
|
|
205
206
|
useLegendKey(() => setLegendOpen((open) => !open));
|
|
207
|
+
// 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
|
|
208
|
+
// is back on the game.
|
|
209
|
+
const [line, setLine] = useState(undefined);
|
|
210
|
+
const analysing = studied !== undefined;
|
|
211
|
+
const onTheLine = analysing && onLine(line);
|
|
206
212
|
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
|
-
|
|
213
|
+
const lineGame = useMemo(() => onTheLine ? new Game(lineRecord(live.record, line)) : undefined, [live, onTheLine ? line : undefined]);
|
|
214
|
+
const shown = useMemo(() => lineGame ?? past ?? (pending === undefined ? live : withMove(live, pending)), [live, pending, past, lineGame]);
|
|
215
|
+
// The move after the position shown, which a step back slides home: the line's own on the line.
|
|
216
|
+
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]);
|
|
217
|
+
// Where the line starts its numbers when it leaves from the game's first position.
|
|
218
|
+
const firstPosition = useMemo(() => new Game({ tags: live.record.tags, moves: [] }).position, [live]);
|
|
219
|
+
// 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
|
|
220
|
+
// goes off with it too.
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
setPointed(noSquares);
|
|
223
|
+
if (analysing) {
|
|
224
|
+
setRefusal(undefined);
|
|
225
|
+
}
|
|
226
|
+
}, [shown]);
|
|
211
227
|
// 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
228
|
const helped = helpsShown(phase, settings.helps);
|
|
214
229
|
// 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
230
|
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 +240,7 @@ export function App({ start }) {
|
|
|
225
240
|
setPending(undefined);
|
|
226
241
|
setViewing(undefined);
|
|
227
242
|
setPromoting(undefined);
|
|
243
|
+
setLine(undefined);
|
|
228
244
|
}, [gameKey]);
|
|
229
245
|
const played = studied === undefined ? game : null;
|
|
230
246
|
useGameSounds(played, replayed, sound);
|
|
@@ -252,16 +268,43 @@ export function App({ start }) {
|
|
|
252
268
|
const shownOpening = useMemo(() => bookAt === undefined || book === undefined ? undefined : openingOf(bookAt, book), [live, book, bookAt === undefined ? 0 : at]);
|
|
253
269
|
// 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
270
|
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
|
|
271
|
+
// 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.
|
|
272
|
+
const place = { at, cursor: onTheLine ? line.cursor : 0 };
|
|
273
|
+
const shownBefore = useRef(place);
|
|
274
|
+
const jumped = isJump(shownBefore.current, place);
|
|
275
|
+
shownBefore.current = place;
|
|
276
|
+
// A move of the game shown takes the board off a variation, which stays faded.
|
|
259
277
|
const goTo = (count) => {
|
|
260
278
|
setSelected(undefined);
|
|
279
|
+
setLine(leaveLine);
|
|
261
280
|
setViewing(count >= total ? undefined : Math.max(0, count));
|
|
262
281
|
};
|
|
263
|
-
|
|
282
|
+
// On a variation the arrows back and forward step through it; to either end of the game they leave it.
|
|
283
|
+
const step = (to) => {
|
|
284
|
+
if (onTheLine && (to === "back" || to === "forward")) {
|
|
285
|
+
setSelected(undefined);
|
|
286
|
+
setLine(stepLine(line, to));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
goTo(stepped(to, at, total));
|
|
290
|
+
}
|
|
291
|
+
};
|
|
264
292
|
useBrowseKeys(step, () => setFlipped(!flipped));
|
|
293
|
+
// A move of the variation shown: the board goes back on it, at the game's move it leaves from.
|
|
294
|
+
const showLine = (cursor) => {
|
|
295
|
+
if (line !== undefined) {
|
|
296
|
+
setSelected(undefined);
|
|
297
|
+
setLine({ ...line, cursor });
|
|
298
|
+
setViewing(line.at >= total ? undefined : line.at);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
// The cross and Esc close the variation: the board shows the game where it left it.
|
|
302
|
+
const closeLine = () => {
|
|
303
|
+
setSelected(undefined);
|
|
304
|
+
setLine(undefined);
|
|
305
|
+
};
|
|
306
|
+
// Esc first lets go of a piece picked, and leaves the legend, a promotion's choice, the settings and the dialogs to close themselves.
|
|
307
|
+
useEscapeKey(line === undefined || legendOpen || promoting !== undefined || setting || dialog !== undefined ? undefined : () => selected === undefined ? closeLine() : setSelected(undefined));
|
|
265
308
|
const wheel = useWheelSteps(step);
|
|
266
309
|
// 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
310
|
// 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 +312,8 @@ export function App({ start }) {
|
|
|
269
312
|
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
313
|
// The tab names the game and the move shown, as the cards name its players; between games, Chesspal alone.
|
|
271
314
|
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
|
-
|
|
315
|
+
const lineNumbered = line === undefined ? undefined : linePlies(line, lineStartOf(plies, line.at, firstPosition));
|
|
316
|
+
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
317
|
// The header's theme switch picks the other of the two the page shows, leaving the system's.
|
|
274
318
|
const switchTheme = () => change({ ...settings, theme: document.documentElement.dataset.theme === "dark" ? "light" : "dark" });
|
|
275
319
|
// 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 +366,8 @@ export function App({ start }) {
|
|
|
322
366
|
const mood = moodAt(live.record, sides?.white.who === "ai" ? "white" : sides?.black.who === "ai" ? "black" : ai, at).mood;
|
|
323
367
|
const playerToMove = going && live.position.turn === player;
|
|
324
368
|
const ahead = (side) => side === "white" ? material.balance : -material.balance;
|
|
325
|
-
|
|
369
|
+
// On a variation the AI's badges, notes and drawings say nothing: they are of the game's moves (D-116).
|
|
370
|
+
const reviewed = onTheLine ? undefined : judged || at <= inBook ? plies[at - 1] : undefined;
|
|
326
371
|
// 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
372
|
const judgments = going && settings.helps ? judgmentsOf(plies, player, coached ?? noCoach) : [];
|
|
328
373
|
const review = reviewed?.review;
|
|
@@ -330,7 +375,7 @@ export function App({ start }) {
|
|
|
330
375
|
const drawn = hintOpen && hint !== undefined && viewing === undefined && (hint.arrows !== undefined || hint.squares !== undefined) ? hint : review;
|
|
331
376
|
const drawnByCoach = hint !== undefined && drawn === hint;
|
|
332
377
|
// 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;
|
|
378
|
+
const boardEnd = ended && !aborted && viewing === undefined && pending === undefined && !onTheLine ? { how, winner, player, fresh: justEnded } : undefined;
|
|
334
379
|
// The legend has the marks the board shows: none of the helps' or the drawings while the board is bare (D-99).
|
|
335
380
|
const shownBare = bare && helps !== undefined;
|
|
336
381
|
const legendRows = legendOf({
|
|
@@ -397,7 +442,7 @@ export function App({ start }) {
|
|
|
397
442
|
const play = (from, to, piece) => {
|
|
398
443
|
setSelected(undefined);
|
|
399
444
|
setRefusal(undefined);
|
|
400
|
-
if (piece === undefined && promotes(
|
|
445
|
+
if (piece === undefined && promotes(piecesOnBoard.get(from), to)) {
|
|
401
446
|
setPromoting({ from, to });
|
|
402
447
|
return;
|
|
403
448
|
}
|
|
@@ -416,9 +461,35 @@ export function App({ start }) {
|
|
|
416
461
|
}
|
|
417
462
|
});
|
|
418
463
|
};
|
|
419
|
-
//
|
|
464
|
+
// 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
|
|
465
|
+
// forward through it. The move sounds as the board stands after it.
|
|
466
|
+
const explore = (from, to, piece) => {
|
|
467
|
+
setSelected(undefined);
|
|
468
|
+
setRefusal(undefined);
|
|
469
|
+
if (piece === undefined && promotes(piecesOf(shown.position.fen).get(from), to)) {
|
|
470
|
+
setPromoting({ from, to });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
setPromoting(undefined);
|
|
474
|
+
const move = shown.legalMoves(from).find((legal) => legal.to === to && legal.promotion === piece);
|
|
475
|
+
if (move === undefined) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const after = afterMove(line, live.record.moves.map(({ san }) => san), at, move.san);
|
|
479
|
+
setLine(after.line);
|
|
480
|
+
setViewing(after.shown >= total ? undefined : after.shown);
|
|
481
|
+
const played = new Game(shown.record);
|
|
482
|
+
const moveSound = played.play(move.san).played && sound ? soundOf(played) : undefined;
|
|
483
|
+
if (moveSound !== undefined) {
|
|
484
|
+
playSound(moveSound);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
// 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
|
|
488
|
+
// the position shown.
|
|
420
489
|
const refuse = (from, to) => {
|
|
421
|
-
const
|
|
490
|
+
const board = analysing ? shown : live;
|
|
491
|
+
const pieces = analysing ? piecesOf(shown.position.fen) : piecesOnBoard;
|
|
492
|
+
const illegal = settings.explain ? board.whyNot(from, to) : undefined;
|
|
422
493
|
if (illegal !== undefined) {
|
|
423
494
|
const letterOn = (square) => pieces.get(square).toUpperCase();
|
|
424
495
|
const sideOn = (square) => pieces.get(square) === letterOn(square) ? "white" : "black";
|
|
@@ -453,6 +524,10 @@ export function App({ start }) {
|
|
|
453
524
|
};
|
|
454
525
|
// 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
526
|
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 }));
|
|
527
|
+
// The variation as the column and the steps show it.
|
|
528
|
+
const lineView = !analysing || line === undefined || lineNumbered === undefined
|
|
529
|
+
? undefined
|
|
530
|
+
: { plies: lineNumbered, at: line.at, cursor: line.cursor, onShow: showLine, onClose: closeLine, onBack: () => goTo(line.at) };
|
|
456
531
|
// 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
532
|
// chat at the table, opens the chat's window: the analyst works with the chat's model (D-109).
|
|
458
533
|
const studyGame = async (studied) => {
|
|
@@ -474,7 +549,7 @@ export function App({ start }) {
|
|
|
474
549
|
: null, _jsxs("main", { class: `app${flipped ? " flipped" : ""}${folds.talk === "open" ? "" : ` talk-${folds.talk}`}${folds.game === "folded" ? " folded" : ""}`, inert: offline, children: [header, sides === undefined
|
|
475
550
|
? (_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
551
|
: (_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) => {
|
|
552
|
+
? (_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
553
|
setDraft(words);
|
|
479
554
|
setUnsent(undefined);
|
|
480
555
|
}, onSend: () => sendWords({ game: studied.name, at }), canMove: false, name: name, problem: unsent, about: { ...(at === 0 ? {} : { ply: plies[at - 1] }), onShow: goTo }, ...(chatName === undefined ? {} : { to: chatName }), text: text }) }), studiedCard(player, "you")] })] }))
|
|
@@ -485,11 +560,18 @@ export function App({ start }) {
|
|
|
485
560
|
setUnsent(undefined);
|
|
486
561
|
}, 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
562
|
// 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
|
-
|
|
563
|
+
// A variation rings the board in the accent (D-116).
|
|
564
|
+
class: between && !justEnded ? "board-wrap off" : onTheLine ? "board-wrap variation" : "board-wrap", onWheel: wheel,
|
|
489
565
|
// 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
|
|
566
|
+
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
|
|
567
|
+
? undefined
|
|
568
|
+
: analysing
|
|
569
|
+
? { ...promoting, side: shown.position.turn }
|
|
570
|
+
: viewing !== undefined || !playerToMove
|
|
571
|
+
? undefined
|
|
572
|
+
: { ...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
573
|
? (_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 ? {} : {
|
|
574
|
+
: 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
575
|
analysis: {
|
|
494
576
|
by: analyst,
|
|
495
577
|
...(summary === undefined ? {} : { summary }),
|
|
@@ -497,7 +579,7 @@ export function App({ start }) {
|
|
|
497
579
|
...(count === undefined ? {} : { count }),
|
|
498
580
|
...(completeIt === undefined ? {} : { onComplete: completeIt }),
|
|
499
581
|
},
|
|
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 })] })] }) }));
|
|
582
|
+
}), 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
583
|
}
|
|
502
584
|
/** 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
585
|
function pictureOf({ who, profile: other }, profile, profiles, size, mood) {
|
|
@@ -580,6 +662,11 @@ function withMove(game, move) {
|
|
|
580
662
|
const copy = new Game(game.record);
|
|
581
663
|
return copy.play(move).played ? copy : game;
|
|
582
664
|
}
|
|
665
|
+
/** 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. */
|
|
666
|
+
function lineStartOf(plies, at, first) {
|
|
667
|
+
const fork = plies[at - 1];
|
|
668
|
+
return fork === undefined ? first : fork.side === "white" ? { moveNumber: fork.number, turn: "black" } : { moveNumber: fork.number + 1, turn: "white" };
|
|
669
|
+
}
|
|
583
670
|
/** Whether a pawn's move takes it to the last rank, where the player chooses what it becomes. */
|
|
584
671
|
function promotes(piece, to) {
|
|
585
672
|
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
|
+
}
|
package/dist/page/game/column.js
CHANGED
|
@@ -4,18 +4,18 @@ import { LeadBar, LeadChart } from "../lead/lead.js";
|
|
|
4
4
|
import { standingAt } from "../lead/view.js";
|
|
5
5
|
import { Piece } from "../pieces/pieces.js";
|
|
6
6
|
import { FoldButton, useCardFold } from "./card-fold.js";
|
|
7
|
-
import { MoveList, Steps } from "./move-list.js";
|
|
7
|
+
import { MoveList, Steps, stepsLine } from "./move-list.js";
|
|
8
8
|
import { ResultTile } from "./result-tile.js";
|
|
9
9
|
import { ReviewProgress } from "./review-progress.js";
|
|
10
10
|
/**
|
|
11
11
|
* 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
|
|
12
12
|
* (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.
|
|
13
13
|
*/
|
|
14
|
-
export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, outOfBook, helps, legend, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
|
|
14
|
+
export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, outOfBook, helps, legend, standings, analysis, shownPlies, line, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
|
|
15
15
|
const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
|
|
16
16
|
return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
|
|
17
17
|
? (_jsxs(_Fragment, { children: [_jsx("p", { class: "card empty", children: text.noGame }), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseLast, onAnalyse: onAnalyse })] }))
|
|
18
|
-
: (_jsxs(_Fragment, { children: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis?.reviewing !== true ? null : (_jsxs("div", { class: "card review waiting", children: [_jsx("p", { role: "status", children: text.reviewing(analysis.by) }), analysis.count === undefined ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.reviewing(analysis.by) })] })), analysis?.onComplete === undefined ? null : (_jsxs("div", { class: "card review incomplete", children: [_jsxs("p", { children: [_jsx("span", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: analysis.onComplete, children: text.complete })] }), analysis.count === undefined || analysis.count.done === 0 ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.incomplete, stopped: true })] })), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, ...(outOfBook === undefined ? {} : { outOfBook }), text: text }), helps] }), _jsxs("div", { class: "side-foot", children: [result !== undefined && plies.length === 0 ? null : _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
|
+
: (_jsxs(_Fragment, { children: [_jsxs("div", { class: "side-cards", children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis?.reviewing !== true ? null : (_jsxs("div", { class: "card review waiting", children: [_jsx("p", { role: "status", children: text.reviewing(analysis.by) }), analysis.count === undefined ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.reviewing(analysis.by) })] })), analysis?.onComplete === undefined ? null : (_jsxs("div", { class: "card review incomplete", children: [_jsxs("p", { children: [_jsx("span", { role: "status", children: text.incomplete }), _jsx("button", { type: "button", title: text.completeHint, "aria-label": text.completeHint, onClick: analysis.onComplete, children: text.complete })] }), analysis.count === undefined || analysis.count.done === 0 ? null : _jsx(ReviewProgress, { count: analysis.count, label: text.incomplete, stopped: true })] })), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, ...(outOfBook === undefined ? {} : { outOfBook }), text: text }), helps] }), _jsxs("div", { class: "side-foot", children: [result !== undefined && plies.length === 0 ? null : _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, ...(line === undefined ? {} : { line }), 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, ...stepsLine(line, plies), onStep: onStep, text: text })] })] })) }));
|
|
19
19
|
}
|
|
20
20
|
/** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
|
|
21
21
|
function SaveButton({ label, hint, onSave }) {
|
package/dist/page/game/game.css
CHANGED
|
@@ -504,7 +504,7 @@
|
|
|
504
504
|
font-weight: 600;
|
|
505
505
|
}
|
|
506
506
|
|
|
507
|
-
.moves li:nth-child(even) {
|
|
507
|
+
.moves li:nth-child(even of :not(.line)) {
|
|
508
508
|
background: var(--page);
|
|
509
509
|
}
|
|
510
510
|
|
|
@@ -555,6 +555,97 @@
|
|
|
555
555
|
box-shadow: 0 0 0 1.5px var(--card);
|
|
556
556
|
}
|
|
557
557
|
|
|
558
|
+
/* A variation in analysis (D-116): one row under the move it leaves from, indented, an accent edge on its left, its moves wrapping; the move it leaves
|
|
559
|
+
from underlined in the accent while the board shows the line */
|
|
560
|
+
.moves li.line {
|
|
561
|
+
display: flex;
|
|
562
|
+
align-items: flex-start;
|
|
563
|
+
gap: 2px;
|
|
564
|
+
margin: 2px 0 2px 24px;
|
|
565
|
+
padding: 2px 2px 2px 8px;
|
|
566
|
+
border-left: 3px solid var(--accent);
|
|
567
|
+
border-radius: 0 6px 6px 0;
|
|
568
|
+
background: color-mix(in oklab, var(--accent) 8%, var(--card));
|
|
569
|
+
font-size: 13px;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.line-moves {
|
|
573
|
+
flex: 1;
|
|
574
|
+
display: flex;
|
|
575
|
+
flex-wrap: wrap;
|
|
576
|
+
gap: 1px 2px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.moves .line .ply {
|
|
580
|
+
margin-left: 0;
|
|
581
|
+
padding: 1px 4px;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.line-number {
|
|
585
|
+
color: var(--soft);
|
|
586
|
+
margin-right: 1px;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.moves .line .current .line-number {
|
|
590
|
+
color: #4d5866;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.moves .ply.fork {
|
|
594
|
+
box-shadow: inset 0 -2px 0 var(--accent);
|
|
595
|
+
border-radius: 5px 5px 0 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* Back on the game the line fades, its moves still a click away */
|
|
599
|
+
.moves li.line.faded {
|
|
600
|
+
opacity: 0.55;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.moves li.line.faded:hover,
|
|
604
|
+
.moves li.line.faded:focus-within {
|
|
605
|
+
opacity: 0.85;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* The cross that closes it, at the row's end; a finger's 32 pixels on a touch screen */
|
|
609
|
+
.close-line {
|
|
610
|
+
display: grid;
|
|
611
|
+
place-items: center;
|
|
612
|
+
width: 24px;
|
|
613
|
+
height: 24px;
|
|
614
|
+
flex: none;
|
|
615
|
+
padding: 0;
|
|
616
|
+
border: 0;
|
|
617
|
+
border-radius: 50%;
|
|
618
|
+
background: none;
|
|
619
|
+
color: var(--soft);
|
|
620
|
+
cursor: pointer;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.close-line:hover {
|
|
624
|
+
background: color-mix(in oklab, var(--ink) 9%, transparent);
|
|
625
|
+
color: var(--ink);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.close-line .icon {
|
|
629
|
+
width: 14px;
|
|
630
|
+
height: 14px;
|
|
631
|
+
stroke-width: 2.2;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
@media (pointer: coarse) {
|
|
635
|
+
.close-line {
|
|
636
|
+
width: 32px;
|
|
637
|
+
height: 32px;
|
|
638
|
+
margin-block: -4px;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
@media (max-width: 479px) {
|
|
643
|
+
.moves li.line {
|
|
644
|
+
margin-left: 12px;
|
|
645
|
+
padding-left: 6px;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
558
649
|
/* The buttons that step through the moves, under them as on Lichess and chess.com */
|
|
559
650
|
.steps {
|
|
560
651
|
display: flex;
|
|
@@ -591,6 +682,35 @@
|
|
|
591
682
|
stroke-width: 2;
|
|
592
683
|
}
|
|
593
684
|
|
|
685
|
+
/* On a variation, the fork with the move it leaves from, in the accent: it takes the board back to the game */
|
|
686
|
+
.steps .fork-back {
|
|
687
|
+
flex: 1.6;
|
|
688
|
+
display: inline-flex;
|
|
689
|
+
align-items: center;
|
|
690
|
+
justify-content: center;
|
|
691
|
+
gap: 3px;
|
|
692
|
+
padding: 4px 6px;
|
|
693
|
+
border-color: var(--accent);
|
|
694
|
+
color: var(--accent);
|
|
695
|
+
font: inherit;
|
|
696
|
+
font-size: 13px;
|
|
697
|
+
white-space: nowrap;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.steps .fork-back:hover {
|
|
701
|
+
border-color: var(--accent);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.steps .fork-back span {
|
|
705
|
+
color: var(--ink);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.steps .fork-icon {
|
|
709
|
+
width: 16px;
|
|
710
|
+
height: 16px;
|
|
711
|
+
flex: none;
|
|
712
|
+
}
|
|
713
|
+
|
|
594
714
|
/* A piece in a move: the set's small one, White's white and Black's black, edged so either shows on either theme */
|
|
595
715
|
.figurine {
|
|
596
716
|
display: inline-block;
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
+
// The moves of the game beside the board, in pairs, with the pieces drawn as figures, the same in every language (D-50); the player looks back through them
|
|
3
|
+
// with a click on one or with the buttons under them. In analysis a variation the player plays is one row under the move it leaves from (D-116).
|
|
4
|
+
import { Fragment } from "preact";
|
|
2
5
|
import { useEffect, useRef } from "preact/hooks";
|
|
3
6
|
import { Figurine } from "../pieces/pieces.js";
|
|
4
7
|
import { BadgeIcon, NoteIcon } from "./badges.js";
|
|
5
8
|
import { FoldButton, useCardFold } from "./card-fold.js";
|
|
9
|
+
import { lineText, moveNumberOf } from "./variation.js";
|
|
6
10
|
/**
|
|
7
11
|
* 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
12
|
* shows it. The list keeps its last move in sight, and the one shown while the player looks back.
|
|
9
13
|
*/
|
|
10
|
-
export function MoveList({ plies, result, shown, onShow, tools, text }) {
|
|
14
|
+
export function MoveList({ plies, result, shown, line, onShow, tools, text }) {
|
|
11
15
|
const list = useRef(null);
|
|
12
16
|
const [folded, toggleFold] = useCardFold("moves");
|
|
13
17
|
const rows = rowsOf(plies);
|
|
14
|
-
const
|
|
18
|
+
const onTheLine = line !== undefined && line.cursor > 0;
|
|
19
|
+
const current = onTheLine ? undefined : plies[shown - 1];
|
|
20
|
+
const fork = onTheLine ? plies[line.at - 1] : undefined;
|
|
21
|
+
// The line goes under the row of the move it leaves from, before the first row when it leaves from the start.
|
|
22
|
+
const lineAfter = line === undefined ? undefined : line.at === 0 ? -1 : rows.findIndex((row) => row.white === plies[line.at - 1] || row.black === plies[line.at - 1]);
|
|
15
23
|
useEffect(() => {
|
|
16
24
|
list.current?.scrollTo({ top: list.current.scrollHeight });
|
|
17
25
|
}, [plies.length]);
|
|
18
26
|
// Only the list scrolls to the move shown: the page under the board stays where the player has it.
|
|
19
|
-
useEffect(() => keepInSight(list.current), [shown]);
|
|
27
|
+
useEffect(() => keepInSight(list.current), [shown, line?.cursor, line?.plies.length]);
|
|
20
28
|
// The cards above it change height with the position, the helps' card above all: the list follows, its move still in sight.
|
|
21
29
|
useEffect(() => {
|
|
22
30
|
const element = list.current;
|
|
@@ -27,26 +35,56 @@ export function MoveList({ plies, result, shown, onShow, tools, text }) {
|
|
|
27
35
|
observer.observe(element);
|
|
28
36
|
return () => observer.disconnect();
|
|
29
37
|
}, []);
|
|
30
|
-
const move = (ply) => _jsx(Move, { ply: ply, current: ply !== undefined && ply === current, onShow: () => onShow(plies.indexOf(ply) + 1), text: text });
|
|
38
|
+
const move = (ply) => _jsx(Move, { ply: ply, current: ply !== undefined && ply === current, fork: ply !== undefined && ply === fork, onShow: () => onShow(plies.indexOf(ply) + 1), text: text });
|
|
39
|
+
const lineRow = line === undefined ? null : _jsx(LineRow, { line: line, text: text });
|
|
31
40
|
// 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
41
|
// 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: [_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: [
|
|
42
|
+
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: [_jsxs("ol", { children: [lineAfter === -1 ? lineRow : null, rows.map(({ number, white, black }, index) => (_jsxs(Fragment, { children: [_jsxs("li", { children: [_jsxs("span", { class: "number", children: [number, "."] }), move(white), move(black)] }), index === lineAfter ? lineRow : null] }, number)))] }), result === undefined ? null : _jsx("p", { class: "result", children: result })] })] }));
|
|
34
43
|
}
|
|
35
|
-
/**
|
|
36
|
-
|
|
44
|
+
/**
|
|
45
|
+
* 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. On a variation,
|
|
46
|
+
* back and forward step through it, and the fork with the move it leaves from takes the board back to the game.
|
|
47
|
+
*/
|
|
48
|
+
export function Steps({ shown, total, line, fork, onStep, text }) {
|
|
49
|
+
const onTheLine = line !== undefined && line.cursor > 0;
|
|
37
50
|
const button = (step, path, off) => (_jsx("button", { type: "button", title: text.steps[step], "aria-label": text.steps[step], disabled: off, onClick: () => onStep(step), children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: path }) }) }));
|
|
38
|
-
return (_jsxs("nav", { class: "steps card", "aria-label": text.moves, children: [button("first", "M7 6V18M17 6L11 12L17 18", shown === 0), button("back", "M14.5 6L8.5 12L14.5 18", shown === 0), button("forward", "M9.5 6L15.5 12L9.5 18", shown === total), button("last", "M17 6V18M7 6L13 12L7 18", shown === total)] }));
|
|
51
|
+
return (_jsxs("nav", { class: "steps card", "aria-label": text.moves, children: [button("first", "M7 6V18M17 6L11 12L17 18", !onTheLine && shown === 0), button("back", "M14.5 6L8.5 12L14.5 18", !onTheLine && shown === 0), button("forward", "M9.5 6L15.5 12L9.5 18", onTheLine ? line.cursor === line.plies.length : shown === total), button("last", "M17 6V18M7 6L13 12L7 18", !onTheLine && shown === total), onTheLine ? _jsx(ForkButton, { fork: fork, onBack: line.onBack, text: text }) : null] }));
|
|
52
|
+
}
|
|
53
|
+
/** The variation for the steps, with the game's move it leaves from. */
|
|
54
|
+
export function stepsLine(line, plies) {
|
|
55
|
+
const fork = line === undefined ? undefined : plies[line.at - 1];
|
|
56
|
+
return line === undefined ? {} : fork === undefined ? { line } : { line, fork };
|
|
39
57
|
}
|
|
40
58
|
/** A move in SAN with each piece's letter drawn as its figure, the same in every language (D-50), and a capture's x in grey, so the squares stand out. */
|
|
41
59
|
export function Figurines({ san, side }) {
|
|
42
60
|
return (_jsx(_Fragment, { children: san.split(/([KQRBNx])/).map((part, index) => index % 2 === 0 ? part : part === "x" ? _jsx("span", { class: "takes", children: "x" }, index) : _jsx(Figurine, { letter: part, side: side }, index)) }));
|
|
43
61
|
}
|
|
44
|
-
function Move({ ply, current, onShow, text }) {
|
|
62
|
+
function Move({ ply, current, fork, onShow, text }) {
|
|
45
63
|
if (ply === undefined) {
|
|
46
64
|
return _jsx("span", { children: "\u2026" });
|
|
47
65
|
}
|
|
48
66
|
// A screen reader reads the move in SAN and the AI's badge by its name; the eye sees each piece's letter as its figure, and the badge's disc (D-69).
|
|
49
|
-
return (_jsxs("button", { type: "button", class: current ? "ply current" : "ply", "aria-current": current ? "step" : undefined, onClick: onShow, children: [_jsx("span", { class: "visually-hidden", children: ply.review?.badge === undefined ? ply.san : `${ply.san}, ${text.badges[ply.review.badge]}` }), _jsx("span", { "aria-hidden": "true", children: _jsx(Figurines, { san: ply.san, side: ply.side }) }), ply.review?.badge !== undefined ? _jsx(BadgeIcon, { badge: ply.review.badge }) : ply.review?.note !== undefined ? _jsx(NoteIcon, {}) : null] }));
|
|
67
|
+
return (_jsxs("button", { type: "button", class: current ? "ply current" : fork ? "ply fork" : "ply", "aria-current": current ? "step" : undefined, onClick: onShow, children: [_jsx("span", { class: "visually-hidden", children: ply.review?.badge === undefined ? ply.san : `${ply.san}, ${text.badges[ply.review.badge]}` }), _jsx("span", { "aria-hidden": "true", children: _jsx(Figurines, { san: ply.san, side: ply.side }) }), ply.review?.badge !== undefined ? _jsx(BadgeIcon, { badge: ply.review.badge }) : ply.review?.note !== undefined ? _jsx(NoteIcon, {}) : null] }));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The variation's row: its moves numbered as a PGN writes them, the one shown in the gold of the moves, and a cross that closes it; faded while the board
|
|
71
|
+
* shows the game, its moves still a click away.
|
|
72
|
+
*/
|
|
73
|
+
function LineRow({ line, text }) {
|
|
74
|
+
return (_jsxs("li", { class: line.cursor > 0 ? "line" : "line faded", "aria-label": `${text.variation.line}: ${lineText(line.plies)}`, children: [_jsx("span", { class: "line-moves", children: line.plies.map((ply, index) => {
|
|
75
|
+
const current = index + 1 === line.cursor;
|
|
76
|
+
const number = moveNumberOf(ply, index === 0);
|
|
77
|
+
return (_jsxs("button", { type: "button", class: current ? "ply current" : "ply", "aria-current": current ? "step" : undefined, onClick: () => line.onShow(index + 1), children: [_jsxs("span", { class: "visually-hidden", children: [number, ply.san] }), _jsxs("span", { "aria-hidden": "true", children: [number === "" ? null : _jsx("span", { class: "line-number", children: number.replace("...", "…") }), _jsx(Figurines, { san: ply.san, side: ply.side })] })] }, index));
|
|
78
|
+
}) }), _jsx("button", { type: "button", class: "close-line", title: text.variation.close, "aria-label": text.variation.close, onClick: line.onClose, children: _jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M7 7L17 17M17 7L7 17" }) }) })] }));
|
|
79
|
+
}
|
|
80
|
+
/** The fork with the game's move the variation leaves from: it takes the board back to the game there. */
|
|
81
|
+
function ForkButton({ fork, onBack, text }) {
|
|
82
|
+
const move = fork === undefined ? undefined : `${fork.number}${fork.side === "white" ? "." : "..."}${fork.san}`;
|
|
83
|
+
return (_jsxs("button", { type: "button", class: "fork-back", title: text.variation.back(move), "aria-label": text.variation.back(move), onClick: onBack, children: [_jsx(ForkIcon, {}), fork === undefined ? null : (_jsxs("span", { "aria-hidden": "true", children: [fork.number, fork.side === "white" ? "." : "…", _jsx(Figurines, { san: fork.san, side: fork.side })] }))] }));
|
|
84
|
+
}
|
|
85
|
+
/** The sign of a variation: a line that forks from the game's. */
|
|
86
|
+
export function ForkIcon() {
|
|
87
|
+
return (_jsx("svg", { class: "icon fork-icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M7 4V20M7 12C7 8 17 9 17 4M17 4L14 6.5M17 4L19.5 7" }) }));
|
|
50
88
|
}
|
|
51
89
|
/** Scrolls the list, and only the list, as little as it takes to show the whole row of its current move, and the result under the last one. */
|
|
52
90
|
function keepInSight(list) {
|