chesspal 0.5.0 → 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/dist/page/app/app.js +111 -24
- 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 +1 -0
- package/dist/page/guide/it.md +1 -0
- package/dist/page/text.js +2 -0
- package/dist/server/page/game.js +4 -3
- package/dist/server/page/table.d.ts +12 -2
- package/dist/server/page/table.js +16 -4
- 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/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";
|
|
@@ -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) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { stepped } from "./browse.js";
|
|
2
|
+
/** Whether the board shows a move of the line, not the game. */
|
|
3
|
+
export function onLine(line) {
|
|
4
|
+
return line !== undefined && line.cursor > 0;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The line after the player moves `san` where the board stands, and how many of the game's moves the board shows by then: on the game, its own next move
|
|
8
|
+
* steps forward and the line's first move goes back into the line; any other starts a new line. On the line, its own next move steps forward and any other
|
|
9
|
+
* cuts the line there and goes on with it.
|
|
10
|
+
*/
|
|
11
|
+
export function afterMove(line, game, shown, san) {
|
|
12
|
+
if (line !== undefined && line.cursor > 0) {
|
|
13
|
+
const moves = line.moves[line.cursor] === san ? line.moves : [...line.moves.slice(0, line.cursor), san];
|
|
14
|
+
return { line: { at: line.at, moves, cursor: line.cursor + 1 }, shown: line.at };
|
|
15
|
+
}
|
|
16
|
+
if (game[shown] === san) {
|
|
17
|
+
return { line: line === undefined ? undefined : { ...line, cursor: 0 }, shown: shown + 1 };
|
|
18
|
+
}
|
|
19
|
+
if (line?.at === shown && line.moves[0] === san) {
|
|
20
|
+
return { line: { ...line, cursor: 1 }, shown };
|
|
21
|
+
}
|
|
22
|
+
return { line: { at: shown, moves: [san], cursor: 1 }, shown };
|
|
23
|
+
}
|
|
24
|
+
/** The line after a step back or forward through it: back from its first move the board is on the game again, where the line leaves it. */
|
|
25
|
+
export function stepLine(line, step) {
|
|
26
|
+
return { ...line, cursor: stepped(step, line.cursor, line.moves.length) };
|
|
27
|
+
}
|
|
28
|
+
/** The line with the board back on the game, the line kept faded. */
|
|
29
|
+
export function leaveLine(line) {
|
|
30
|
+
return line === undefined ? undefined : { ...line, cursor: 0 };
|
|
31
|
+
}
|
|
32
|
+
/** The record of the position the board shows: the game's moves up to the line, then the line's up to its cursor. */
|
|
33
|
+
export function lineRecord(record, line) {
|
|
34
|
+
const moves = [...record.moves.slice(0, line.at).map(({ san }) => san), ...line.moves.slice(0, line.cursor)];
|
|
35
|
+
return { tags: record.tags, moves: moves.map((san) => ({ san })) };
|
|
36
|
+
}
|
|
37
|
+
/** The line's moves numbered from the position it leaves the game at, given by its move number and the side to move. */
|
|
38
|
+
export function linePlies(line, start) {
|
|
39
|
+
let { moveNumber: number, turn: side } = start;
|
|
40
|
+
return line.moves.map((san) => {
|
|
41
|
+
const ply = { san, number, side };
|
|
42
|
+
number += side === "black" ? 1 : 0;
|
|
43
|
+
side = side === "white" ? "black" : "white";
|
|
44
|
+
return ply;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** The moves written as a PGN writes them, 12...Kf8 13.Nxe5 Bxe2: the number before White's moves, and before the first when it is Black's. */
|
|
48
|
+
export function lineText(plies) {
|
|
49
|
+
return plies.map((ply, index) => `${moveNumberOf(ply, index === 0)}${ply.san}`).join(" ");
|
|
50
|
+
}
|
|
51
|
+
/** The number written before a move of a line, 13. or 12..., none before Black's but the first. */
|
|
52
|
+
export function moveNumberOf(ply, first) {
|
|
53
|
+
return ply.side === "white" ? `${ply.number}.` : first ? `${ply.number}...` : "";
|
|
54
|
+
}
|
|
55
|
+
/** Whether the board goes from one place to another by more than one move, which shows the position at once instead of sliding a move. */
|
|
56
|
+
export function isJump(from, to) {
|
|
57
|
+
if (from.cursor === 0 && to.cursor === 0) {
|
|
58
|
+
return Math.abs(from.at - to.at) > 1;
|
|
59
|
+
}
|
|
60
|
+
if (from.cursor > 0 && to.cursor > 0) {
|
|
61
|
+
return from.at !== to.at || Math.abs(from.cursor - to.cursor) > 1;
|
|
62
|
+
}
|
|
63
|
+
const [game, line] = from.cursor === 0 ? [from, to] : [to, from];
|
|
64
|
+
return !(game.at === line.at && line.cursor === 1);
|
|
65
|
+
}
|
package/dist/page/guide/en.md
CHANGED
|
@@ -53,6 +53,7 @@ No chess engine plays for the AI: every move and every judgment is its own. Ches
|
|
|
53
53
|

|
|
54
54
|
|
|
55
55
|
- When the game is over, press **Analyse the game**: the AI goes over it and puts its judgments and notes on the moves. Read them by stepping through the moves with the arrows at the foot of the column, or by clicking a move.
|
|
56
|
+
- In analysis, **move the pieces**, White's and Black's, from any move: a variation starts, with a coloured edge round the board. Click a move of the game to go back to it; the ✕, or Esc, closes the variation.
|
|
56
57
|
- **Analyses**, in the header, opens your analyses, and loads a game played elsewhere: paste its PGN, or choose a .pgn file.
|
|
57
58
|
- With your names on Lichess and Chess.com in your profile, **Load a game** lists your latest games there: click one to analyse it.
|
|
58
59
|
- Ask the AI about the game in the box under its name.
|
package/dist/page/guide/it.md
CHANGED
|
@@ -53,6 +53,7 @@ Nessun motore di scacchi gioca al posto dell'AI: ogni mossa e ogni giudizio sono
|
|
|
53
53
|

|
|
54
54
|
|
|
55
55
|
- A partita finita premi **Analizza la partita**: l'AI la ripassa e mette sulle mosse i suoi giudizi e le sue note. Le leggi scorrendo le mosse con le frecce in fondo alla colonna, o cliccando una mossa.
|
|
56
|
+
- Nell'analisi **muovi i pezzi**, bianchi e neri, da qualunque mossa: nasce una variante, con un bordo colorato attorno alla scacchiera. Clicca una mossa della partita per tornarci; la ✕, o Esc, chiude la variante.
|
|
56
57
|
- **Analisi**, nell'intestazione, apre le tue analisi e carica una partita giocata altrove: incolla il suo PGN, o scegli un file .pgn.
|
|
57
58
|
- Con i tuoi nomi su Lichess e Chess.com nel profilo, **Carica una partita** elenca le tue ultime partite là: clicca su una per analizzarla.
|
|
58
59
|
- Chiedi all'AI della partita nella casella sotto il suo nome.
|
package/dist/page/text.js
CHANGED
|
@@ -84,6 +84,7 @@ const english = {
|
|
|
84
84
|
game: "The game",
|
|
85
85
|
moves: "Moves",
|
|
86
86
|
steps: { first: "Start", back: "Previous move", forward: "Next move", last: "Back to the game" },
|
|
87
|
+
variation: { line: "Variation", close: "Close the variation (Esc)", back: (move) => move === undefined ? "Back to the start of the game" : `Back to the game, at ${move}` },
|
|
87
88
|
noGame: "No game yet: press + to start one, or ask for one in the chat.",
|
|
88
89
|
foldCard: "Fold the card",
|
|
89
90
|
openCard: "Open the card",
|
|
@@ -523,6 +524,7 @@ const italian = {
|
|
|
523
524
|
game: "La partita",
|
|
524
525
|
moves: "Mosse",
|
|
525
526
|
steps: { first: "Inizio", back: "Mossa precedente", forward: "Mossa successiva", last: "Torna alla partita" },
|
|
527
|
+
variation: { line: "Variante", close: "Chiudi la variante (Esc)", back: (move) => move === undefined ? "Torna all'inizio della partita" : `Torna alla partita, a ${move}` },
|
|
526
528
|
noGame: "Nessuna partita: premi + per iniziarne una, o chiedila nella chat.",
|
|
527
529
|
foldCard: "Chiudi la scheda",
|
|
528
530
|
openCard: "Apri la scheda",
|
package/dist/server/page/game.js
CHANGED
|
@@ -7,7 +7,7 @@ import { asTheChatSees } from "./chat.js";
|
|
|
7
7
|
import { tellWatchers } from "./events.js";
|
|
8
8
|
import { bodyOf, bytesOf, isJson, json, jsonOf, runRoute, send, textOf } from "./http.js";
|
|
9
9
|
import { profilesShown } from "./profiles.js";
|
|
10
|
-
import { analystAtWork, chatPlayedAtLength, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
|
|
10
|
+
import { analystAtWork, chatForAnalysis, chatPlayedAtLength, chatToRelaunch, chatToWaitFor, refuseWaited, relaunchChat, tableShown } from "./table.js";
|
|
11
11
|
import { updateShown } from "./update.js";
|
|
12
12
|
/** Answers the page's requests about the game: true when the request was one. */
|
|
13
13
|
export function answerGame(request, response, url, served) {
|
|
@@ -132,7 +132,8 @@ async function messageFromPage(request, response, served) {
|
|
|
132
132
|
/**
|
|
133
133
|
* A new game from the page, with the sides, the level and the clock the player chose: the sides and the level they leave out are as in the game
|
|
134
134
|
* before, and no clock means a game without one. One going on goes to the archive unfinished, and the doorbell tells the chat, unless the table starts
|
|
135
|
-
* it again. A chat the table holds that must be clear for it, and is at work, has the game wait with the choices kept
|
|
135
|
+
* it again. A chat the table holds that must be clear for it, or that it started for the analysis, and is at work, has the game wait with the choices kept
|
|
136
|
+
* until it has ended its answer (D-107).
|
|
136
137
|
*/
|
|
137
138
|
async function newGameFromPage(request, response, served) {
|
|
138
139
|
const asked = 'Send the new game as JSON: {"ai_colour": "black", "level": "club", "clock": "10+5"}, each one optional; no clock for a game without one.';
|
|
@@ -152,7 +153,7 @@ async function newGameFromPage(request, response, served) {
|
|
|
152
153
|
}
|
|
153
154
|
// The game starts from the page, open: its clocks wait for the player's Start there. A chat the table starts again is clean: no /clear is asked.
|
|
154
155
|
relaunching = chatToRelaunch(served);
|
|
155
|
-
if (!relaunching && chatToWaitFor(served) && chatPlayedAtLength(served, before)) {
|
|
156
|
+
if (!relaunching && chatToWaitFor(served) && (chatPlayedAtLength(served, before) || chatForAnalysis(served))) {
|
|
156
157
|
waitForChat(served, input.data);
|
|
157
158
|
send(response, 200, json, JSON.stringify({ waiting: true }));
|
|
158
159
|
return;
|
|
@@ -22,11 +22,15 @@ export interface TableChats {
|
|
|
22
22
|
/** Tells `changed` when the hosts' choices change: Antigravity's models, read from the player's account. */
|
|
23
23
|
watch?(changed: () => void): void;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The chat `chesspal play` holds: the model and effort its host started with, none for the host's own, the seat, the play's process, and `analysis` when
|
|
27
|
+
* it started it for the game in analysis on the page.
|
|
28
|
+
*/
|
|
26
29
|
export interface HeldChat {
|
|
27
30
|
readonly model?: string | undefined;
|
|
28
31
|
readonly effort?: string | undefined;
|
|
29
32
|
readonly seat?: number;
|
|
33
|
+
readonly analysis?: true;
|
|
30
34
|
}
|
|
31
35
|
/** A chat opening at the table: one the page asked for, the one started again for a new game, or with another model or host (D-107). */
|
|
32
36
|
export type Opening = "chat" | "again" | "restart";
|
|
@@ -69,7 +73,8 @@ export declare function tableShown(served: Served): {
|
|
|
69
73
|
export declare function answerTable(request: IncomingMessage, response: ServerResponse, url: URL, served: Served): boolean;
|
|
70
74
|
/**
|
|
71
75
|
* Whether a new game starts the chat at the table again, clean (D-57): `chesspal play` holds it, it waits for the bell, no analyst of it is at work, and it
|
|
72
|
-
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game.
|
|
76
|
+
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game. One
|
|
77
|
+
* `chesspal play` started for the analysis starts again all the same, at rest or still opening, or its first turn would analyse a game no longer there.
|
|
73
78
|
*/
|
|
74
79
|
export declare function chatToRelaunch(served: Served): boolean;
|
|
75
80
|
/**
|
|
@@ -82,6 +87,11 @@ export declare function relaunchChat(served: Served, how?: "again" | "restart",
|
|
|
82
87
|
* it, it is at work, and no analyst of it is; with an analyst at work the player is asked for /clear, as with a chat opened by hand.
|
|
83
88
|
*/
|
|
84
89
|
export declare function chatToWaitFor(served: Served): boolean;
|
|
90
|
+
/**
|
|
91
|
+
* Whether the chat `chesspal play` holds was started for the analysis on the page: the one at the table, or, with none at it yet, the one still opening.
|
|
92
|
+
* A chat opened by hand never is.
|
|
93
|
+
*/
|
|
94
|
+
export declare function chatForAnalysis(served: Served): boolean;
|
|
85
95
|
/**
|
|
86
96
|
* Whether the chat at the table has played the game `current` at length (D-57), counted from the first position it was shown of it. A chat the table
|
|
87
97
|
* started again for a game began after it, and new_game takes it for one cleared since: it holds the whole game all the same.
|
|
@@ -62,22 +62,25 @@ export function answerTable(request, response, url, served) {
|
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
64
|
* Whether a new game starts the chat at the table again, clean (D-57): `chesspal play` holds it, it waits for the bell, no analyst of it is at work, and it
|
|
65
|
-
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game.
|
|
65
|
+
* has seen a game move since it was first shown one; one that only greeted the player stays, as one opened from the chat's window before a new game. One
|
|
66
|
+
* `chesspal play` started for the analysis starts again all the same, at rest or still opening, or its first turn would analyse a game no longer there.
|
|
66
67
|
*/
|
|
67
68
|
export function chatToRelaunch(served) {
|
|
68
69
|
const { positionSeen, positionFirstSeen } = served.chat;
|
|
69
70
|
const played = positionSeen?.started !== undefined && (positionFirstSeen === undefined || !samePosition(positionSeen, positionFirstSeen));
|
|
70
|
-
return played && heldChat(served) !== undefined && chatAtRest(served);
|
|
71
|
+
return chatForAnalysis(served) ? served.attached === undefined || chatAtRest(served) : played && heldChat(served) !== undefined && chatAtRest(served);
|
|
71
72
|
}
|
|
72
73
|
/**
|
|
73
74
|
* Asks `chesspal play` to start the chat at the table again: once the new game has started, or with another model or host in its place (D-107). Whether
|
|
74
75
|
* it was asked; the pages then show it opening.
|
|
75
76
|
*/
|
|
76
77
|
export function relaunchChat(served, how = "again", host) {
|
|
77
|
-
|
|
78
|
+
// One started for the analysis is asked again while it is still opening, in the table's folder.
|
|
79
|
+
const opening = served.attached === undefined && how === "again" && chatForAnalysis(served);
|
|
80
|
+
const folder = opening ? served.context.folder : served.attached?.chat.folder;
|
|
78
81
|
// Another host may first ask whether it trusts the table's folder: the pages say so, as for a chat opened.
|
|
79
82
|
const switching = how === "restart" && host !== undefined && host !== hostAtTable(served);
|
|
80
|
-
if (folder === undefined || heldChat(served) === undefined || served.table?.chats?.relaunch(folder, host) !== true) {
|
|
83
|
+
if (folder === undefined || (!opening && heldChat(served) === undefined) || served.table?.chats?.relaunch(folder, host) !== true) {
|
|
81
84
|
return false;
|
|
82
85
|
}
|
|
83
86
|
noteOpening(served, switching ? "chat" : how);
|
|
@@ -90,6 +93,15 @@ export function relaunchChat(served, how = "again", host) {
|
|
|
90
93
|
export function chatToWaitFor(served) {
|
|
91
94
|
return heldChat(served) !== undefined && !chatAtRest(served) && !analystAtWork(served);
|
|
92
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether the chat `chesspal play` holds was started for the analysis on the page: the one at the table, or, with none at it yet, the one still opening.
|
|
98
|
+
* A chat opened by hand never is.
|
|
99
|
+
*/
|
|
100
|
+
export function chatForAnalysis(served) {
|
|
101
|
+
const { attached, context, table } = served;
|
|
102
|
+
const held = table?.chats?.held(attached?.chat.folder ?? context.folder);
|
|
103
|
+
return held?.analysis === true && (attached === undefined || held.seat === attached.chat.seat);
|
|
104
|
+
}
|
|
93
105
|
/**
|
|
94
106
|
* Whether the chat at the table has played the game `current` at length (D-57), counted from the first position it was shown of it. A chat the table
|
|
95
107
|
* started again for a game began after it, and new_game takes it for one cleared since: it holds the whole game all the same.
|
|
@@ -34,7 +34,7 @@ export function terminalChats(context, chesspal = [process.execPath, ...process.
|
|
|
34
34
|
// Only a chat in the table's folder can be the one chesspal play holds.
|
|
35
35
|
held: (folder) => {
|
|
36
36
|
const seat = samePath(folder, tableFolder(context.home())) ? seatHeld(folder, context.now()) : undefined;
|
|
37
|
-
return seat === undefined ? undefined : { model: seat.model, effort: seat.effort, seat: seat.pid };
|
|
37
|
+
return seat === undefined ? undefined : { model: seat.model, effort: seat.effort, seat: seat.pid, ...(seat.analysis === true ? { analysis: true } : {}) };
|
|
38
38
|
},
|
|
39
39
|
relaunch: (folder, host, updated) => samePath(folder, tableFolder(context.home())) && askAgain(folder, context.now(), { ...(host === undefined ? {} : { host }), ...(updated === true ? { updated } : {}) }),
|
|
40
40
|
};
|
|
@@ -4,7 +4,7 @@ import { type HooksNews } from "./hooks.ts";
|
|
|
4
4
|
import { type HostChoice, type HostName } from "./hosts.ts";
|
|
5
5
|
/**
|
|
6
6
|
* What `chesspal play` starts: the host's program, its arguments, folder and environment, whether it needs cmd, as a .cmd shim does on Windows, and the
|
|
7
|
-
* model and effort it is given, which the seat notes for the page (D-107).
|
|
7
|
+
* model and effort it is given, which the seat notes for the page (D-107), as it notes a chat started for the game in analysis.
|
|
8
8
|
*/
|
|
9
9
|
export interface HostLaunch {
|
|
10
10
|
readonly program: string;
|
|
@@ -13,6 +13,8 @@ export interface HostLaunch {
|
|
|
13
13
|
readonly env: NodeJS.ProcessEnv;
|
|
14
14
|
readonly shell: boolean;
|
|
15
15
|
readonly choice: HostChoice;
|
|
16
|
+
/** Started for the game in analysis on the page, with no game going on: a new game from the page starts it again. */
|
|
17
|
+
readonly analysis: boolean;
|
|
16
18
|
/** Chesspal wrote Codex's hooks, the first time or again: Codex runs them only once the player trusts them. */
|
|
17
19
|
readonly hooks?: HooksNews;
|
|
18
20
|
}
|
|
@@ -146,16 +146,17 @@ export function hostLaunch(context, host, env = process.env, exists) {
|
|
|
146
146
|
const choices = tableChoices(folder, host);
|
|
147
147
|
const choice = choiceIn(profileSettings(home, activeProfile(home).id), host, choices);
|
|
148
148
|
const games = gameFolder(home);
|
|
149
|
-
const
|
|
149
|
+
const analysis = analysisAsked(games);
|
|
150
|
+
const args = hostArgs(host, startMessage(languageIn(games, context.language), analysis), choice, folder, choices);
|
|
150
151
|
// An effort the player set for every chat would win over the one chosen on the page: spawn leaves out a variable that is undefined.
|
|
151
152
|
const effort = host === "claude" && choice.effort !== undefined ? { CLAUDE_CODE_EFFORT_LEVEL: undefined } : {};
|
|
152
153
|
// The seat this play holds, by which its chat's server tells the table it is the chat held, and not another opened in the same folder.
|
|
153
154
|
const childEnv = { ...env, ...Object.fromEntries(sessionMarks.map((name) => [name, undefined])), ...effort, CHESSPAL_SEAT: String(process.pid) };
|
|
154
155
|
const script = npmScript(host, found, context.platform, exists ?? existsSync);
|
|
155
156
|
if (script !== undefined) {
|
|
156
|
-
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, ...hooks };
|
|
157
|
+
return { program: process.execPath, args: [script, ...args], folder, env: childEnv, shell: false, choice, analysis, ...hooks };
|
|
157
158
|
}
|
|
158
|
-
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, ...hooks };
|
|
159
|
+
return { program: found, args, folder, env: childEnv, shell: context.platform === "win32" && /\.(cmd|bat)$/i.test(found), choice, analysis, ...hooks };
|
|
159
160
|
}
|
|
160
161
|
/** Whether the board page shows a game in analysis, none going on: the chat opened there is for that game, brought to review or to talk over, not for a new one. */
|
|
161
162
|
function analysisAsked(folder) {
|
|
@@ -185,10 +186,10 @@ const closedAtStartWords = {
|
|
|
185
186
|
english: ["Claude Code closed as soon as it opened.", 'If it asks whether you trust this folder, go down to "Yes, I trust this folder" with the arrow keys, then press Enter.'],
|
|
186
187
|
italian: ["Claude Code si è chiuso appena aperto.", "Se ti chiede se ti fidi della cartella, scendi con le frecce su «Yes, I trust this folder», poi premi Invio."],
|
|
187
188
|
};
|
|
188
|
-
// Runs the host as runHost does, the seat noting the model and effort it is given; one not installed, or that could not start, fails saying so.
|
|
189
|
+
// Runs the host as runHost does, the seat noting the model and effort it is given, and whether for the analysis; one not installed, or that could not start, fails saying so.
|
|
189
190
|
async function started(context, host, again) {
|
|
190
191
|
const launch = hostLaunch(context, host);
|
|
191
|
-
takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice });
|
|
192
|
+
takeSeat(launch.folder, { pid: process.pid, host, ...launch.choice, ...(launch.analysis ? { analysis: true } : {}) });
|
|
192
193
|
// Codex asks the player to trust the hooks Chesspal wrote: said here, above its question, as init says it.
|
|
193
194
|
if (launch.hooks !== undefined) {
|
|
194
195
|
const words = hooksWords[launch.hooks];
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The play process in the seat: its process id, its host, and the model and effort it started that host with, none for the host's own; `analysis` when
|
|
3
|
+
* it started it for the game in analysis on the page, which a new game then starts again.
|
|
4
|
+
*/
|
|
2
5
|
export interface Seat {
|
|
3
6
|
readonly pid: number;
|
|
4
7
|
readonly host: string;
|
|
5
8
|
readonly model?: string | undefined;
|
|
6
9
|
readonly effort?: string | undefined;
|
|
10
|
+
readonly analysis?: true;
|
|
7
11
|
}
|
|
8
12
|
/**
|
|
9
13
|
* Takes the seat for this play process, over a request left for one before it; it throws while another one holds it. A seat is held while its play
|
|
@@ -70,7 +70,7 @@ function seatIn(folder) {
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
const text = (value) => (typeof value === "string" ? value : undefined);
|
|
73
|
-
return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort) };
|
|
73
|
+
return { pid: seat.pid, host: seat.host, model: text(seat.model), effort: text(seat.effort), ...(seat.analysis === true ? { analysis: true } : {}) };
|
|
74
74
|
}
|
|
75
75
|
function jsonIn(path) {
|
|
76
76
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chesspal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|