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
@@ -1,12 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
2
- // What the board draws over its squares and pieces: the kings' badges at the end, the arrows, the helps' corners and pins, the control map's counts, the
3
- // bubble of a refused move, the choice of a promotion and the piece lifted off the board.
4
2
  import { useEffect, useRef, useState } from "preact/hooks";
5
3
  import { Piece } from "../pieces/pieces.js";
6
4
  /** The corner folded on a piece at risk, red on the player's and green on the other side's, in ink when no side is theirs (D-91); before its line in the card. */
7
5
  export function Notch({ tone }) {
8
6
  return (_jsx("span", { class: tone === undefined ? "help-notch" : `help-notch ${tone}`, "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 10 10", children: [_jsx("path", { d: "M0.5 0.5V9.5H9.5Z" }), tone === undefined ? null : _jsx("path", { class: "edge", d: "M0.5 0.5V9.5H9.5Z" }), _jsx("circle", { cx: "2.9", cy: "7.1", r: "1.1" })] }) }));
9
7
  }
8
+ /**
9
+ * The pushpin stuck in a pinned piece from the top left corner, its needle toward the piece, in the tone of its corner at risk (D-91); in ink in no side's;
10
+ * in the card's line, in its colour.
11
+ */
12
+ export function Pushpin({ tone }) {
13
+ return _jsx(Stuck, { kind: "help-pin", shape: pushpin, tone: tone });
14
+ }
15
+ /**
16
+ * The skewer stuck in the skewered piece, the one in front, as the pushpin in a pinned one (D-91): a stick in ink with a big bead and a small one in the tone,
17
+ * as the piece in front and the cheaper one behind it, so that it reads apart from the pushpin, all in the tone (Stefano); in the card's line, in its colour.
18
+ */
19
+ export function Skewer({ tone }) {
20
+ return (_jsx(Stuck, { kind: "help-skewer", shape: skewer, tone: tone, children: tone === undefined || tone === "neutral" ? null : skewerBeads.map(({ y, radius }) => _jsx("circle", { cx: "12", cy: y, r: radius }, y)) }));
21
+ }
10
22
  /** A king's badge in the corner of its square. */
11
23
  export function Badge({ kind }) {
12
24
  return (_jsx("span", { class: `badge ${kind}`, "aria-hidden": "true", children: kind === "mate" ? "#" : kind === "half" ? "½" : (_jsx("svg", { viewBox: "0 0 24 24", children: kind === "crown" ? _jsx("path", { d: "M4 18L3 7L8.5 11L12 5L15.5 11L21 7L20 18Z" }) : _jsx("path", { d: "M6 21V4M6 4.5C10 2.5 13 6.5 18 4.5V13.5C13 15.5 10 11.5 6 13.5" }) })) }));
@@ -46,13 +58,20 @@ export function Arrows({ arrows, at, explain, occupied }) {
46
58
  const starts = [...new Set(shapes.map(({ from }) => from))];
47
59
  return (_jsx("svg", { class: explain !== undefined ? "arrows explain" : occupied !== undefined ? "arrows drawn" : "arrows", viewBox: "0 0 8 8", "aria-hidden": "true", children: starts.map((start) => _jsx(ArrowGroup, { shapes: shapes.filter(({ from }) => from === start), edged: occupied !== undefined }, start)) }));
48
60
  }
49
- /** Each pin as a dashed line from the pinning piece through the pinned one to what stands behind it (D-78), over a halo that reads on every board. */
50
- export function Pins({ pins, at }) {
51
- return (_jsx("svg", { class: "helps-layer", viewBox: "0 0 8 8", "aria-hidden": "true", children: pins.map((pin) => {
61
+ /**
62
+ * Each pin as a dashed line from the pinning piece through the pinned one to what stands behind it (D-78), and each skewer as a dash-dot one, beads on its stick, over
63
+ * a halo that reads on every board. A pin and a skewer on the same stretch of a line stand apart on its two sides, so that both read.
64
+ */
65
+ export function PinsAndSkewers({ pins, skewers, at }) {
66
+ const lines = [...pins.map((line) => ({ kind: "pin", line, other: skewers })), ...skewers.map((line) => ({ kind: "skewer", line, other: pins }))];
67
+ return (_jsx("svg", { class: "helps-layer", viewBox: "0 0 8 8", "aria-hidden": "true", children: lines.map(({ kind, line, other }) => {
52
68
  // The line stops short of both ends, as an explaining arrow does: it hides neither piece, nor the corner of a pinner at risk.
53
- const [x0, y0] = towards(at(pin.by.square), at(pin.to.square), pinTrim);
54
- const [x1, y1] = towards(at(pin.to.square), at(pin.by.square), pinTrim);
55
- return (_jsxs("g", { class: "pin", children: [_jsx("line", { class: "halo", x1: x0, y1: y0, x2: x1, y2: y1 }), _jsx("line", { x1: x0, y1: y0, x2: x1, y2: y1 })] }, `${pin.by.square}${pin.piece.square}`));
69
+ const [startX, startY] = towards(at(line.by.square), at(line.to.square), pinTrim);
70
+ const [endX, endY] = towards(at(line.to.square), at(line.by.square), pinTrim);
71
+ const shift = other.some((each) => sharedSquares(each, line) > 1) ? (kind === "pin" ? 1 : -1) * sharedLineShift : 0;
72
+ const [dx, dy] = across([startX, startY], [endX, endY], shift);
73
+ const ends = { x1: startX + dx, y1: startY + dy, x2: endX + dx, y2: endY + dy };
74
+ return (_jsxs("g", { class: kind, children: [_jsx("line", { class: "halo", ...ends }), _jsx("line", { ...ends })] }, `${kind} ${line.by.square}${line.piece.square}`));
56
75
  }) }));
57
76
  }
58
77
  /** A square's counts on the control map, as a domino in its middle: how many of White's pieces reach it, and of Black's; none when no piece does. */
@@ -106,35 +125,30 @@ export function Lifted({ drag, letter }) {
106
125
  const classes = ["lifted", up && !drag.back ? "up" : "", drag.touch ? "touch" : "", drag.back ? "back" : ""].filter((name) => name !== "").join(" ");
107
126
  return (_jsx("div", { class: classes, style: { "--x": `${drag.at[0]}px`, "--y": `${drag.at[1]}px` }, children: _jsx(Piece, { letter: letter }) }));
108
127
  }
109
- /** The kings' signs at the end, by their squares: the loser's king lies down when mated or resigning; both kings of a draw stand, the stalemated one stuck. */
110
- export function kingSigns(pieces, toMove, end) {
111
- const kingOf = (side) => [...pieces].find(([, letter]) => letter === (side === "white" ? "K" : "k"))?.[0];
112
- const signs = new Map();
113
- const sign = (side, badge, classes) => {
114
- const square = kingOf(side);
115
- if (square !== undefined) {
116
- signs.set(square, { badge, classes });
117
- }
118
- };
119
- if (end.winner === undefined) {
120
- sign("white", "half", end.how === "stalemate" && toMove === "white" ? "drawn stalled" : "drawn");
121
- sign("black", "half", end.how === "stalemate" && toMove === "black" ? "drawn stalled" : "drawn");
122
- return signs;
123
- }
124
- const loser = end.winner === "white" ? "black" : "white";
125
- sign(end.winner, "crown", "winner");
126
- if (end.how === "checkmate" || end.how === "resignation") {
127
- sign(loser, end.how === "checkmate" ? "mate" : "flag", "fallen");
128
- }
129
- return signs;
130
- }
131
- export const noSigns = new Map();
132
128
  /** Arrows off one square, the AI's on a white edge (D-78): every edge under every arrow, so one edge goes round them all where they meet. */
133
129
  function ArrowGroup({ shapes, edged }) {
134
130
  return (_jsxs("g", { children: [edged
135
131
  ? shapes.map(({ key, shaft, head }) => (_jsxs("g", { children: [_jsx("path", { class: "edge", d: shaft }), _jsx("polygon", { class: "edge", points: head })] }, `edge${key}`)))
136
132
  : null, shapes.map(({ key, colour, shaft, head }) => (_jsxs("g", { class: colour, children: [_jsx("path", { d: shaft }), _jsx("polygon", { points: head })] }, key)))] }));
137
133
  }
134
+ /**
135
+ * A sign stuck in a piece from its square's top left corner, tilted toward the piece; edged in ink when coloured (Zoe). Its box holds the tilted sign and
136
+ * its halo only, so that it sits in the corner (Stefano).
137
+ */
138
+ function Stuck({ kind, shape, tone, children }) {
139
+ return (_jsx("span", { class: tone === undefined ? kind : `${kind} ${tone}`, "aria-hidden": "true", children: _jsx("svg", { viewBox: "2 0.5 18.5 22.5", children: _jsxs("g", { transform: "rotate(-32 12 12)", children: [_jsx("path", { d: shape }), tone === undefined || tone === "neutral" ? null : _jsx("path", { class: "edge", d: shape }), children] }) }) }));
140
+ }
141
+ /** How many of their three squares a pin and a skewer share: two when they lie on the same stretch of a line. */
142
+ function sharedSquares(one, other) {
143
+ const squares = [one.by.square, one.piece.square, one.to.square];
144
+ return [other.by.square, other.piece.square, other.to.square].filter((square) => squares.includes(square)).length;
145
+ }
146
+ /** A step of the given length across a line, always to the same side of it whichever way the line runs. */
147
+ function across(start, end, distance) {
148
+ const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
149
+ const way = end[0] < start[0] || (end[0] === start[0] && end[1] < start[1]) ? -1 : 1;
150
+ return [-(end[1] - start[1]) / length * distance * way, (end[0] - start[0]) / length * distance * way];
151
+ }
138
152
  /** A point `distance` squares from `point` toward `target`. */
139
153
  function towards(point, target, distance) {
140
154
  const length = Math.hypot(target[0] - point[0], target[1] - point[1]);
@@ -150,10 +164,18 @@ function ExplainIcon({ kind, piece }) {
150
164
  : _jsx("path", { d: "M4 8h15M15 4l4 4-4 4M20 16H5M9 12l-4 4 4 4" }) })) }));
151
165
  }
152
166
  const promotionPieces = ["q", "n", "r", "b"];
167
+ // A pushpin standing up, as the bubble's pin: its cap, its body widening into the collar, and its needle. Tilted less than a diagonal, so it never lies along a pin's line.
168
+ const pushpin = "M8.6 2.2H15.4Q16.6 2.2 16.6 3.4V4.6Q16.6 5.8 15.4 5.8H14.8V9.4L18.4 13.1V14.9H13V20L12 22.6L11 20V14.9H5.6V13.1L9.2 9.4V5.8H8.6Q7.4 5.8 7.4 4.6V3.4Q7.4 2.2 8.6 2.2Z";
169
+ // A skewer standing up, in the pushpin's box: a round end, a big bead, a small one, a stretch of stick between them, and its point.
170
+ const skewer = "M10.9 2.6A1.1 1.1 0 0 1 13.1 2.6V3.75A4.2 4.2 0 0 1 13.1 11.85V13A3.1 3.1 0 0 1 13.1 18.8V20.2L12 23.4L10.9 20.2V18.8A3.1 3.1 0 0 1 10.9 13V11.85A4.2 4.2 0 0 1 10.9 3.75Z";
171
+ // The skewer's beads, on its stick: the big one and the small one, as its shape draws them.
172
+ const skewerBeads = [{ y: 7.8, radius: 4.2 }, { y: 15.9, radius: 3.1 }];
153
173
  // How much an arrow that explains is cut, in squares: off the culprit at its start, and before an occupied square at its point.
154
174
  const explainTrim = { start: 0.36, end: 0.4 };
155
175
  // How much a pin's line is cut at each end, in squares.
156
176
  const pinTrim = 0.35;
177
+ // How far a pin and a skewer on the same stretch of a line stand off it, each on its side, in squares: a little more than a line with its halo.
178
+ const sharedLineShift = 0.08;
157
179
  // An arrow's head, in squares: how long, and how far each side of its line.
158
180
  const arrowHead = { length: 0.42, width: 0.3 };
159
181
  // The helps' point, smaller in step with their thinner shaft (Stefano).
@@ -0,0 +1,23 @@
1
+ /** The kings' signs at the end, by their squares: the loser's king lies down when mated or resigning; both kings of a draw stand, the stalemated one stuck. */
2
+ export function kingSigns(pieces, toMove, end) {
3
+ const kingOf = (side) => [...pieces].find(([, letter]) => letter === (side === "white" ? "K" : "k"))?.[0];
4
+ const signs = new Map();
5
+ const sign = (side, badge, classes) => {
6
+ const square = kingOf(side);
7
+ if (square !== undefined) {
8
+ signs.set(square, { badge, classes });
9
+ }
10
+ };
11
+ if (end.winner === undefined) {
12
+ sign("white", "half", end.how === "stalemate" && toMove === "white" ? "drawn stalled" : "drawn");
13
+ sign("black", "half", end.how === "stalemate" && toMove === "black" ? "drawn stalled" : "drawn");
14
+ return signs;
15
+ }
16
+ const loser = end.winner === "white" ? "black" : "white";
17
+ sign(end.winner, "crown", "winner");
18
+ if (end.how === "checkmate" || end.how === "resignation") {
19
+ sign(loser, end.how === "checkmate" ? "mate" : "flag", "fallen");
20
+ }
21
+ return signs;
22
+ }
23
+ export const noSigns = new Map();
@@ -10,11 +10,11 @@ import { ResultTile } from "./result-tile.js";
10
10
  * The column, which the player folds with its tab; under the board, where three columns would leave the board small, always open. A game in analysis
11
11
  * (D-69) says above the moves that the AI is going over it, until its word on the whole game comes, which the talk shows.
12
12
  */
13
- export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
13
+ export function GameColumn({ plies, result, how, lit, started, move, toMove, opening, helps, legend, standings, analysis, shownPlies, onShow, onStep, onDownload, onSaveMoves, onAnalyse, text }) {
14
14
  const bar = standings === undefined ? undefined : _jsx(LeadBar, { standing: standingAt(standings, shownPlies), shown: shownPlies, text: text });
15
15
  return (_jsx("aside", { class: "side", id: "game", "aria-label": text.game, children: started === undefined && analysis === undefined
16
16
  ? (_jsxs(_Fragment, { children: [_jsx("p", { class: "card empty", children: text.noGame }), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseLast, onAnalyse: onAnalyse })] }))
17
- : (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps, analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })) }));
17
+ : (_jsxs(_Fragment, { children: [_jsx(GameCard, { plies: plies, result: result, how: how, lit: lit, started: started, move: move, toMove: toMove, bar: bar, text: text }), analysis === undefined || standings === undefined ? null : _jsx(LeadChart, { plies: plies, standings: standings, shown: shownPlies, onShow: onShow, text: text }), opening === undefined ? null : _jsx(OpeningCard, { opening: opening, text: text }), helps, analysis === undefined || analysis.summary !== undefined ? null : (_jsxs("p", { class: "card review waiting", role: "status", children: [_jsx("span", { class: "spinner", "aria-hidden": "true" }), text.reviewing(analysis.by)] })), _jsx(MoveList, { plies: plies, result: result, shown: shownPlies, onShow: onShow, tools: legend, text: text }), onDownload === undefined && onSaveMoves === undefined ? null : (_jsxs("div", { class: "card analysis-actions", children: [onDownload === undefined ? null : _jsx(SaveButton, { label: text.study.download, hint: text.study.downloadHint, onSave: onDownload }), onSaveMoves === undefined ? null : _jsx(SaveButton, { label: text.saveMoves, hint: text.saveMovesHint, onSave: onSaveMoves })] })), onAnalyse === undefined ? null : _jsx(AnalyseButton, { label: text.study.analyseIt, onAnalyse: onAnalyse }), _jsx(Steps, { shown: shownPlies, total: plies.length, onStep: onStep, text: text })] })) }));
18
18
  }
19
19
  /** A button that saves the game as a file in the player's downloads, its tooltip saying what the file holds. */
20
20
  function SaveButton({ label, hint, onSave }) {
@@ -352,6 +352,18 @@
352
352
  padding: 2px 4px 0;
353
353
  }
354
354
 
355
+ /* The title, and on its right the legend of the marks when no helps' card holds it (D-99) */
356
+ .moves-head {
357
+ display: flex;
358
+ align-items: center;
359
+ justify-content: space-between;
360
+ gap: 8px;
361
+ }
362
+
363
+ .moves-head .hold-button {
364
+ padding: 3px 6px;
365
+ }
366
+
355
367
  /* Positioned, so that the moves' hidden names scroll with the list instead of stretching the page */
356
368
  .moves {
357
369
  position: relative;
@@ -7,7 +7,7 @@ import { FoldButton, useCardFold } from "./card-fold.js";
7
7
  * The moves numbered in pairs, the one the board shows marked (`shown`, how many moves it shows), and the result once the game is over; a click on a move
8
8
  * shows it. The list keeps its last move in sight, and the one shown while the player looks back.
9
9
  */
10
- export function MoveList({ plies, result, shown, onShow, text }) {
10
+ export function MoveList({ plies, result, shown, onShow, tools, text }) {
11
11
  const list = useRef(null);
12
12
  const [folded, toggleFold] = useCardFold("moves");
13
13
  const rows = rowsOf(plies);
@@ -30,7 +30,7 @@ export function MoveList({ plies, result, shown, onShow, text }) {
30
30
  const move = (ply) => _jsx(Move, { ply: ply, current: ply !== undefined && ply === current, onShow: () => onShow(plies.indexOf(ply) + 1), text: text });
31
31
  // The card's title with its icon, a score sheet, as the other cards of the column, which folds the card to it (Stefano); only the list under it
32
32
  // scrolls, and it stays in the page while folded, so that it keeps its move in sight.
33
- return (_jsxs("section", { class: `card moves-card${folded ? " folded" : ""}`, "aria-label": text.moves, children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 3H15L19 7V21H6ZM15 3V7H19M9 11H16M9 14.5H16M9 18H13" }) }), text.moves] }) }), _jsxs("div", { ref: list, class: "moves", hidden: folded, children: [_jsx("ol", { children: rows.map(({ number, white, black }) => (_jsxs("li", { children: [_jsxs("span", { class: "number", children: [number, "."] }), move(white), move(black)] }, number))) }), result === undefined ? null : _jsx("p", { class: "result", children: result })] })] }));
33
+ return (_jsxs("section", { class: `card moves-card${folded ? " folded" : ""}`, "aria-label": text.moves, children: [_jsxs("div", { class: "moves-head", children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: toggleFold, text: text, children: [_jsx("svg", { class: "icon", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { d: "M6 3H15L19 7V21H6ZM15 3V7H19M9 11H16M9 14.5H16M9 18H13" }) }), text.moves] }) }), tools] }), _jsxs("div", { ref: list, class: "moves", hidden: folded, children: [_jsx("ol", { children: rows.map(({ number, white, black }) => (_jsxs("li", { children: [_jsxs("span", { class: "number", children: [number, "."] }), move(white), move(black)] }, number))) }), result === undefined ? null : _jsx("p", { class: "result", children: result })] })] }));
34
34
  }
35
35
  /** The buttons that step through the moves: to the start, back, forward, and to the game as it is; each one off where it could not go. */
36
36
  export function Steps({ shown, total, onStep, text }) {
@@ -58,6 +58,14 @@ export function judgmentsOf(plies, player, { judgments }) {
58
58
  }
59
59
  /** How long a line of the player's may be: the server's limit (D-54), which a test keeps equal. */
60
60
  export const talkLimits = { line: 200 };
61
+ /**
62
+ * The talk over a game in analysis (D-101): from the player's first line on it, the lines said since, but for the player's on another game; the last ten,
63
+ * which is all half the column holds (Zoe).
64
+ */
65
+ export function analysisTalk(talk, game) {
66
+ const first = talk.findIndex((line) => line.shown?.game === game);
67
+ return first === -1 ? [] : talk.slice(first).filter((line) => line.shown === undefined || line.shown.game === game).slice(-10);
68
+ }
61
69
  /** How many times the AI has spoken in the game, its comments on its moves and its lines on the page: the page sees it speak when the count grows. */
62
70
  export function aiSpoke(plies, ai, talk) {
63
71
  return plies.filter((ply) => ply.side === ai && ply.comment !== undefined).length + talk.filter((line) => line.who === "ai").length;
@@ -49,6 +49,74 @@
49
49
  stroke-width: 0.5;
50
50
  }
51
51
 
52
+ /* A pinned piece: a pushpin stuck in it from the top left corner, in the notch's colours and on its halo; it takes the place of the rank's number there.
53
+ A skewered piece, the one in front: a skewer stuck in it the same way, beside the pushpin when the piece is pinned too */
54
+ .help-pin,
55
+ .help-skewer {
56
+ position: absolute;
57
+ left: 1px;
58
+ top: 1px;
59
+ z-index: 2;
60
+ display: block;
61
+ height: clamp(15px, 3.6cqw, 26px);
62
+ aspect-ratio: 18.5 / 22.5;
63
+ pointer-events: none;
64
+ }
65
+
66
+ .help-pin svg,
67
+ .help-skewer svg {
68
+ display: block;
69
+ width: 100%;
70
+ height: 100%;
71
+ overflow: visible;
72
+ }
73
+
74
+ .help-pin path,
75
+ .help-skewer path {
76
+ fill: var(--help-ink);
77
+ stroke: var(--help-halo);
78
+ stroke-width: 2.6;
79
+ stroke-linejoin: round;
80
+ paint-order: stroke;
81
+ }
82
+
83
+ .help-pin.threat path {
84
+ fill: var(--badge-blunder);
85
+ }
86
+
87
+ .help-pin.chance path {
88
+ fill: var(--badge-best);
89
+ }
90
+
91
+ /* The skewer's stick stays in ink and its beads take the tone, so that it reads apart from the pushpin (Stefano) */
92
+ .help-skewer circle {
93
+ stroke: var(--help-ink);
94
+ stroke-width: 0.9;
95
+ }
96
+
97
+ .help-skewer.threat circle {
98
+ fill: var(--badge-blunder);
99
+ }
100
+
101
+ .help-skewer.chance circle {
102
+ fill: var(--badge-best);
103
+ }
104
+
105
+ .help-pin path.edge,
106
+ .help-skewer path.edge {
107
+ fill: none;
108
+ stroke: var(--help-ink);
109
+ stroke-width: 1;
110
+ }
111
+
112
+ .square:has(> .help-pin) > .help-skewer {
113
+ left: clamp(9px, 3cqw, 22px);
114
+ }
115
+
116
+ .square:has(> .help-pin, > .help-skewer) > .coordinate.rank {
117
+ visibility: hidden;
118
+ }
119
+
52
120
  /* A pin: a thin dashed ink line from the pinner to what stands behind, under the arrows; without a head, never an arrow */
53
121
  .helps-layer {
54
122
  position: absolute;
@@ -73,6 +141,11 @@
73
141
  stroke-linecap: round;
74
142
  }
75
143
 
144
+ /* A skewer's line: a dash and a dot, beads on its stick, so that it reads apart from a pin's even dashes */
145
+ .helps-layer .skewer line:not(.halo) {
146
+ stroke-dasharray: 0.22 0.11 0 0.11;
147
+ }
148
+
76
149
  /* The map (Zoe, after the round): every square on one neutral ground, then light where only White reaches, dark where only Black does, half and half
77
150
  when both do; the two counts together in the middle, as a domino. The drawings and the pieces step back */
78
151
  .board.mapping ~ .arrows,
@@ -383,6 +456,20 @@
383
456
  fill: var(--card);
384
457
  }
385
458
 
459
+ /* The pushpin and the skewer of the board, in the line's colour and without their halo */
460
+ .help-rows .help-pin,
461
+ .help-rows .help-skewer {
462
+ left: 7px;
463
+ top: 4px;
464
+ height: 17px;
465
+ }
466
+
467
+ .help-rows .help-pin path,
468
+ .help-rows .help-skewer path {
469
+ fill: var(--row);
470
+ stroke: none;
471
+ }
472
+
386
473
  .help-rows .row-icon {
387
474
  position: absolute;
388
475
  left: 6px;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-ru
3
3
  // the coach's words (D-88).
4
4
  import { Fragment } from "preact";
5
5
  import { useState } from "preact/hooks";
6
- import { Notch } from "../board/marks.js";
6
+ import { Notch, Pushpin, Skewer } from "../board/marks.js";
7
7
  import { BadgeIcon, NoteIcon } from "../game/badges.js";
8
8
  import { FoldButton, useCardFold } from "../game/card-fold.js";
9
9
  import { Figurines } from "../game/move-list.js";
@@ -15,7 +15,7 @@ import { groupOf, pinSquares, riskSquares, squaresOf, strategyOf } from "./view.
15
15
  * its button or M is held, and the board without its drawings while their button is. A line under the pointer lights its squares on the board. Its title
16
16
  * folds it to the title and opens it again (Stefano).
17
17
  */
18
- export function HelpsCard({ helps, judged, hint, hintOpen, onHint, onPoint, onMap, mapping, onBare, bare, text }) {
18
+ export function HelpsCard({ helps, judged, hint, hintOpen, onHint, onPoint, onMap, mapping, onBare, bare, legend, text }) {
19
19
  const [tab, setTab] = useState("tactics");
20
20
  // The tactics' lines the player opened stay open from move to move, so that a pin is followed as the game goes on.
21
21
  const [opened, setOpened] = useState(new Set());
@@ -29,7 +29,7 @@ export function HelpsCard({ helps, judged, hint, hintOpen, onHint, onPoint, onMa
29
29
  }
30
30
  toggleFold();
31
31
  };
32
- return (_jsxs("section", { class: `card helps-card${folded ? " folded" : ""}`, "aria-label": words.title, children: [_jsxs("div", { class: "helps-head", children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: fold, text: text, children: [_jsx(HelpsIcon, {}), words.title] }) }), _jsxs("span", { class: "helps-tools", children: [_jsx(MapButton, { mapping: mapping, onMap: onMap, text: text }), _jsx(BareButton, { bare: bare, onBare: onBare, text: text })] })] }), folded ? null : (_jsxs(_Fragment, { children: [judged.length === 0 && hint === undefined ? null : (_jsxs("section", { class: `coach${judged.length === 0 ? " hint-only" : ""}`, "aria-label": words.coach, children: [judged.map((ply) => _jsx(Judged, { ply: ply, text: text }, `${ply.number} ${ply.side}`)), hint === undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: onHint, text: text })] })), _jsxs("div", { class: "tabs", role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": tab === "tactics", onClick: () => setTab("tactics"), children: words.tactics }), _jsx("button", { type: "button", role: "tab", "aria-selected": tab === "strategy", onClick: () => setTab("strategy"), children: words.strategy })] }), tab === "tactics"
32
+ return (_jsxs("section", { class: `card helps-card${folded ? " folded" : ""}`, "aria-label": words.title, children: [_jsxs("div", { class: "helps-head", children: [_jsx("h3", { class: "card-title", children: _jsxs(FoldButton, { folded: folded, onFold: fold, text: text, children: [_jsx(HelpsIcon, {}), words.title] }) }), _jsxs("span", { class: "helps-tools", children: [_jsx(MapButton, { mapping: mapping, onMap: onMap, text: text }), _jsx(BareButton, { bare: bare, onBare: onBare, text: text }), legend] })] }), folded ? null : (_jsxs(_Fragment, { children: [judged.length === 0 && hint === undefined ? null : (_jsxs("section", { class: `coach${judged.length === 0 ? " hint-only" : ""}`, "aria-label": words.coach, children: [judged.map((ply) => _jsx(Judged, { ply: ply, text: text }, `${ply.number} ${ply.side}`)), hint === undefined ? null : _jsx(HintLine, { hint: hint, open: hintOpen, onOpen: onHint, text: text })] })), _jsxs("div", { class: "tabs", role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": tab === "tactics", onClick: () => setTab("tactics"), children: words.tactics }), _jsx("button", { type: "button", role: "tab", "aria-selected": tab === "strategy", onClick: () => setTab("strategy"), children: words.strategy })] }), tab === "tactics"
33
33
  ? _jsx(Tactics, { helps: helps, opened: opened, onOpen: openLine, onPoint: onPoint, text: text })
34
34
  : _jsx(Strategy, { helps: helps, onPoint: onPoint, text: text })] }))] }));
35
35
  }
@@ -70,7 +70,7 @@ function Judged({ ply, text }) {
70
70
  function Tactics({ helps, opened, onOpen, onPoint, text }) {
71
71
  const words = text.helpsCard;
72
72
  const point = pointing(onPoint);
73
- const calm = !helps.lines.some((line) => line.kind === "risk" || line.kind === "pin");
73
+ const calm = !helps.lines.some((line) => line.kind === "risk" || line.kind === "pin" || line.kind === "skewer");
74
74
  return (_jsxs("ul", { class: "help-rows", children: [calm
75
75
  ? (_jsxs("li", { class: "safe", children: [_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M5.2 8.2l2 2 3.8-4" })] }), words.nothingAtRisk] }))
76
76
  : null, helps.lines.map((line) => {
@@ -86,17 +86,20 @@ function pointing(onPoint) {
86
86
  onPointerLeave: (event) => event.pointerType === "mouse" && onPoint([]),
87
87
  });
88
88
  }
89
- /** A line's sign: the corner of a piece at risk, a pin's link, a check's plus, the loose pieces' i; its colour is the line's. */
89
+ /** A line's sign, as the board draws it: the corner of a piece at risk, the pushpin, the skewer; a check's plus, the loose pieces' i; its colour is the line's. */
90
90
  function LineIcon({ kind }) {
91
- if (kind === "risk") {
92
- return _jsx(Notch, {});
93
- }
94
- if (kind === "loose") {
95
- return _jsx(InfoIcon, {});
91
+ switch (kind) {
92
+ case "risk":
93
+ return _jsx(Notch, {});
94
+ case "pin":
95
+ return _jsx(Pushpin, {});
96
+ case "skewer":
97
+ return _jsx(Skewer, {});
98
+ case "loose":
99
+ return _jsx(InfoIcon, {});
100
+ case "check":
101
+ return (_jsxs(RowIcon, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 5v6M5 8h6" })] }));
96
102
  }
97
- return (_jsx(RowIcon, { children: kind === "pin"
98
- ? _jsx("path", { d: "M6.5 9.5l3-3M7.5 4.5l1-1a2.5 2.5 0 0 1 3.5 3.5l-1 1M8.5 11.5l-1 1a2.5 2.5 0 0 1-3.5-3.5l1-1" })
99
- : (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "8", cy: "8", r: "6.5" }), _jsx("path", { d: "M8 5v6M5 8h6" })] })) }));
100
103
  }
101
104
  /** How many: the loose pieces by side, each after its pawn, the player's first (Zoe); the others in one number. */
102
105
  function LineCount({ line, helps, words }) {
@@ -115,6 +118,8 @@ function LineDetails({ line, helps, point, text }) {
115
118
  return (_jsx("ul", { class: "line-details", children: line.items.map((risk) => (_jsxs("li", { ...point(riskSquares(risk)), children: [text.piece(risk.piece.piece, risk.piece.side), " ", _jsx(Squares, { squares: [risk.piece.square] }), " \u00B7 ", words.attackers, ": ", _jsx(Squares, { squares: risk.piece.attackers }), risk.why === "undefended" ? null : (_jsxs(_Fragment, { children: [" ", "\u00B7 ", words.defenders, ": ", _jsx(Squares, { squares: risk.piece.defenders })] })), " \u00B7 ", words[risk.why]] }, risk.piece.square))) }));
116
119
  case "pin":
117
120
  return (_jsx("ul", { class: "line-details", children: line.items.map((pin) => (_jsxs("li", { ...point(pinSquares(pin)), children: [text.piece(pin.piece.piece, pin.piece.side), " ", _jsx(Squares, { squares: [pin.piece.square] }), " ", words.pinnedTo(pin.to.piece), " ", _jsx(Squares, { squares: [pin.to.square] }), ", ", words.pinnedBy(pin.by.piece), " ", _jsx(Squares, { squares: [pin.by.square] })] }, pin.piece.square))) }));
121
+ case "skewer":
122
+ return (_jsx("ul", { class: "line-details", children: line.items.map((skewer) => (_jsxs("li", { ...point(pinSquares(skewer)), children: [text.piece(skewer.piece.piece, skewer.piece.side), " ", _jsx(Squares, { squares: [skewer.piece.square] }), " ", words.skeweredBefore(skewer.to.piece), " ", _jsx(Squares, { squares: [skewer.to.square] }), ", ", words.pinnedBy(skewer.by.piece), " ", _jsx(Squares, { squares: [skewer.by.square] })] }, skewer.piece.square))) }));
118
123
  case "check":
119
124
  return (_jsx("p", { class: "line-details", children: line.items.map((check) => (_jsx("span", { class: "help-move", children: _jsx(Figurines, { san: check, side: line.side }) }, check))) }));
120
125
  case "loose":
@@ -7,6 +7,7 @@ export function helpsOf(game, you) {
7
7
  const toMove = game.position.turn;
8
8
  const atRisk = [...risks(facts.undefended, "undefended"), ...risks(facts.attackedByCheaper, "cheaper"), ...risks(facts.outnumbered, "outnumbered")];
9
9
  const pins = facts.pins.map((pin) => ({ piece: on(pin.square), by: on(pin.by), to: on(pin.to) }));
10
+ const skewers = facts.skewers.map((skewer) => ({ piece: on(skewer.square), by: on(skewer.by), to: on(skewer.to) }));
10
11
  const checks = facts.checks;
11
12
  const otherChecks = facts.otherChecks ?? [];
12
13
  const loose = facts.loose.map(on);
@@ -15,10 +16,11 @@ export function helpsOf(game, you) {
15
16
  you,
16
17
  atRisk,
17
18
  pins,
19
+ skewers,
18
20
  checks,
19
21
  otherChecks,
20
22
  loose,
21
- lines: tacticLines({ toMove, atRisk, pins, checks, otherChecks, loose }, you),
23
+ lines: tacticLines({ toMove, atRisk, pins, skewers, checks, otherChecks, loose }, you),
22
24
  strategy: game.profile,
23
25
  sides: you === "black" ? ["black", "white"] : ["white", "black"],
24
26
  };
@@ -28,12 +30,23 @@ export function riskOn(helps, square) {
28
30
  const risk = helps.atRisk.find((risk) => risk.piece.square === square);
29
31
  return risk === undefined ? undefined : toneOf(risk.piece.side, helps.you);
30
32
  }
31
- /** The squares a line of the tactics lights on the board: its pieces, and those that attack or pin them; none for the checks. */
33
+ /** How the helps mark a square's pinned piece: in the tone of a piece at risk, red when it is the player's, green when it is the other side's (D-91). */
34
+ export function pinOn(helps, square) {
35
+ const pin = helps.pins.find((pin) => pin.piece.square === square);
36
+ return pin === undefined ? undefined : toneOf(pin.piece.side, helps.you);
37
+ }
38
+ /** How the helps mark a square's skewered piece, the one in front: in the tone of a piece at risk, as a pinned one (D-91). */
39
+ export function skewerOn(helps, square) {
40
+ const skewer = helps.skewers.find((skewer) => skewer.piece.square === square);
41
+ return skewer === undefined ? undefined : toneOf(skewer.piece.side, helps.you);
42
+ }
43
+ /** The squares a line of the tactics lights on the board: its pieces, and those that attack, pin or skewer them; none for the checks. */
32
44
  export function squaresOf(line) {
33
45
  switch (line.kind) {
34
46
  case "risk":
35
47
  return line.items.flatMap(riskSquares);
36
48
  case "pin":
49
+ case "skewer":
37
50
  return line.items.flatMap(pinSquares);
38
51
  case "check":
39
52
  return [];
@@ -45,7 +58,7 @@ export function squaresOf(line) {
45
58
  export function riskSquares(risk) {
46
59
  return [risk.piece.square, ...risk.piece.attackers];
47
60
  }
48
- /** A pinned piece, the one that pins it and the one behind it. */
61
+ /** A pinned or skewered piece, the one that pins or skewers it and the one behind it. */
49
62
  export function pinSquares(pin) {
50
63
  return [pin.piece.square, pin.by.square, pin.to.square];
51
64
  }
@@ -88,12 +101,13 @@ const cells = {
88
101
  castled: castlingOf,
89
102
  kingAttacks: (side) => side.king?.attacksNear ?? 0,
90
103
  };
91
- // The tactics' lines that have something: with the player's side, their pieces at risk and pinned and the other side's checks, then the same the other way
92
- // round; without it, White's then Black's; the loose pieces last.
104
+ // The tactics' lines that have something: with the player's side, their pieces at risk, pinned and skewered and the other side's checks, then the same the
105
+ // other way round; without it, White's then Black's; the loose pieces last.
93
106
  function tacticLines(facts, you) {
94
107
  const sideLines = (side) => [
95
108
  { kind: "risk", side, tone: toneOf(side, you), items: facts.atRisk.filter((risk) => risk.piece.side === side) },
96
109
  { kind: "pin", side, tone: toneOf(side, you), items: facts.pins.filter((pin) => pin.piece.side === side) },
110
+ { kind: "skewer", side, tone: toneOf(side, you), items: facts.skewers.filter((skewer) => skewer.piece.side === side) },
97
111
  ];
98
112
  const checkLine = (side) => ({
99
113
  kind: "check",
@@ -751,10 +751,31 @@
751
751
  display: none;
752
752
  }
753
753
 
754
- /* A game in analysis, turned round or not: the card on top, the board, its steps under the thumb, the Torretta's note, the card below, the moves (Zoe) */
755
- .app:has(.talk.studied),
754
+ /* A game in analysis: the card on top, the board, its steps under the thumb, the Torretta's note, the talk, the card below, the moves; turned round,
755
+ the player's card on top (Zoe) */
756
+ .app:has(.talk.studied) {
757
+ grid-template-areas: "head" "ai" "board" "steps" "note" "chat" "you" "side";
758
+ }
759
+
756
760
  .app.flipped:has(.talk.studied) {
757
- grid-template-areas: "head" "ai" "board" "steps" "note" "you" "side";
761
+ grid-template-areas: "head" "you" "board" "steps" "note" "chat" "ai" "side";
762
+ }
763
+
764
+ .talk.studied > .you-block {
765
+ display: contents;
766
+ }
767
+
768
+ .talk.studied > .you-block > :not(.chat) {
769
+ grid-area: you;
770
+ }
771
+
772
+ /* The talk, under the note: half the screen of it at most */
773
+ .talk.studied .chat {
774
+ grid-area: chat;
775
+ }
776
+
777
+ .talk.studied .sent {
778
+ max-height: 50vh;
758
779
  }
759
780
 
760
781
  .talk.studied > .ai-block {
@@ -771,9 +792,12 @@
771
792
  }
772
793
 
773
794
  /* With no chat listening, its note comes before the Torretta's (Zoe) */
774
- .app:has(.talk.studied > .ai-block > .asleep),
795
+ .app:has(.talk.studied > .ai-block > .asleep) {
796
+ grid-template-areas: "head" "ai" "board" "steps" "asleep" "note" "chat" "you" "side";
797
+ }
798
+
775
799
  .app.flipped:has(.talk.studied > .ai-block > .asleep) {
776
- grid-template-areas: "head" "ai" "board" "steps" "asleep" "note" "you" "side";
800
+ grid-template-areas: "head" "you" "board" "steps" "asleep" "note" "chat" "ai" "side";
777
801
  }
778
802
 
779
803
  .talk.studied > .ai-block > .asleep {