chesspal 0.2.2 → 0.2.4

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