chesspal 0.2.2 → 0.2.3

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.
@@ -23,6 +23,18 @@ export interface Pin {
23
23
  readonly by: Square;
24
24
  readonly to: Square;
25
25
  }
26
+ /**
27
+ * A king, queen or rook attacked by a bishop, rook or queen with a piece of its side worth less behind it on the line, when the front piece must move (it
28
+ * is the king, the attacker is worth less, or it is undefended) and the attacker is worth less than the piece behind or that piece is undefended without
29
+ * counting the front piece (Anatolij).
30
+ */
31
+ export interface Skewer {
32
+ /** The piece in front, attacked. */
33
+ readonly square: Square;
34
+ readonly by: Square;
35
+ /** The piece behind it. */
36
+ readonly to: Square;
37
+ }
26
38
  /** The facts of a position: data for the AI and the page, never a judgment (D-19). */
27
39
  export interface Facts {
28
40
  /** Every piece, White's first, each side's from the king down: K Q R B N P. */
@@ -38,6 +50,7 @@ export interface Facts {
38
50
  /** Pieces neither attacked nor defended, kings left out: the first targets of a tactic. */
39
51
  readonly loose: readonly Square[];
40
52
  readonly pins: readonly Pin[];
53
+ readonly skewers: readonly Skewer[];
41
54
  /** The checks of the side to move, in SAN. */
42
55
  readonly checks: readonly string[];
43
56
  /** The checks the other side would have if it were to move; left out when the side to move is in check, since it could not be. */
@@ -1,4 +1,4 @@
1
- // What a position shows, computed without judging it (D-19, D-26): who attacks and defends each piece, what is at risk, the pins, the checks, the moves of each piece.
1
+ // What a position shows, computed without judging it (D-19, D-26): who attacks and defends each piece, what is at risk, the pins and skewers, the checks, the moves of each piece.
2
2
  import { Chess } from "chess.js";
3
3
  /** The facts of a chess.js board, whose legal moves are given: computed once for each position, by its key, and frozen. */
4
4
  export function factsOf(chess, legal) {
@@ -51,6 +51,7 @@ function computed(chess, legal) {
51
51
  const defended = atRisk.filter((piece) => piece.defenders.length > 0);
52
52
  const byCheaper = defended.filter((piece) => Math.min(...piece.attackers.map(valueAt)) < valueAt(piece.square));
53
53
  const relativePins = placed.filter((piece) => piece.type === "q" || piece.type === "r").flatMap((target) => pinsTo(chess, target).filter((pin) => valueAt(pin.by) < values[target.type] || factsAt.get(target.square)?.defenders.length === 0));
54
+ const skewers = placed.filter((piece) => piece.type === "k" || piece.type === "q" || piece.type === "r").flatMap((front) => skewersOf(chess, front, factsAt));
54
55
  const king = pieces.find((piece) => piece.piece === "K" && piece.side === sideOf(turn));
55
56
  const facts = {
56
57
  pieces,
@@ -60,6 +61,7 @@ function computed(chess, legal) {
60
61
  outnumbered: defended.filter((piece) => !byCheaper.includes(piece) && piece.attackers.length > piece.defenders.length).map((piece) => piece.square),
61
62
  loose: officers.filter((piece) => piece.attackers.length === 0 && piece.defenders.length === 0).map((piece) => piece.square),
62
63
  pins: [...kingPins, ...relativePins].sort((left, right) => compare(left.square, right.square)),
64
+ skewers: skewers.sort((left, right) => compare(left.square, right.square)),
63
65
  checks: legal.filter((move) => /[+#]$/.test(move.san)).map((move) => move.san),
64
66
  };
65
67
  return chess.inCheck() ? facts : { ...facts, otherChecks: otherChecks(chess) };
@@ -128,6 +130,30 @@ function pinsTo(chess, target) {
128
130
  }
129
131
  return pins;
130
132
  }
133
+ /**
134
+ * The skewers of a king, queen or rook: a rook, bishop or queen that attacks it as the facts count, and a piece of its side worth less right behind it.
135
+ * The piece behind is counted without the front one among its defenders: the front piece leaves the line when it moves (Anatolij).
136
+ */
137
+ function skewersOf(chess, front, factsAt) {
138
+ const skewers = [];
139
+ const facts = factsAt.get(front.square);
140
+ for (const [fileStep, rankStep, slider] of lines) {
141
+ const by = rayFrom(front.square, fileStep, rankStep).find((square) => chess.get(square) !== undefined);
142
+ const to = rayFrom(front.square, -fileStep, -rankStep).find((square) => chess.get(square) !== undefined);
143
+ const attacker = by === undefined ? undefined : chess.get(by);
144
+ const behind = to === undefined ? undefined : chess.get(to);
145
+ if (attacker === undefined || behind === undefined || attacker.color === front.color || behind.color !== front.color
146
+ || (attacker.type !== slider && attacker.type !== "q") || values[behind.type] >= values[front.type] || !facts.attackers.includes(by)) {
147
+ continue;
148
+ }
149
+ const mustMove = front.type === "k" || values[attacker.type] < values[front.type] || facts.defenders.length === 0;
150
+ const behindDefended = factsAt.get(to).defenders.some((square) => square !== front.square);
151
+ if (mustMove && (values[attacker.type] < values[behind.type] || !behindDefended)) {
152
+ skewers.push({ square: front.square, by: by, to: to });
153
+ }
154
+ }
155
+ return skewers;
156
+ }
131
157
  /** The pawn that can be taken en passant, and the square a pawn takes it on; none without an en passant square. */
132
158
  function enPassantVictim(chess) {
133
159
  const to = chess.fen().split(" ")[3];
@@ -1,7 +1,7 @@
1
1
  export { boardText, figurinesOf } from "./board-text.ts";
2
2
  export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.ts";
3
3
  export type { Clock, TimeControl } from "./clock.ts";
4
- export type { Facts, PieceFacts, PieceLetter, Pin } from "./facts.ts";
4
+ export type { Facts, PieceFacts, PieceLetter, Pin, Skewer } from "./facts.ts";
5
5
  export { Game, withSanLetters } from "./game.ts";
6
6
  export type { Ending, Move, Outcome, Played, Position, Promotion, Refusal, Side, Square } from "./game.ts";
7
7
  export type { Illegal, IllegalReason } from "./illegal.ts";
@@ -3,10 +3,10 @@ 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, kingSigns, Lifted, noSigns, 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
12
  import { files, kingToMove, piecesOf } from "./squares.js";
@@ -104,12 +104,17 @@ export function Board({ fen, bottom, lastMove, pointed, helps, control, bare, ba
104
104
  "--dy": ranks.indexOf(Number(from[1])) - row,
105
105
  };
106
106
  const risk = signsOfHelps === undefined ? undefined : riskOn(signsOfHelps, square);
107
+ // A pinned piece's pushpin and a skewered one's skewer go with their lines, so not over the control map.
108
+ const pin = signsOfHelps === undefined || control !== undefined ? undefined : pinOn(signsOfHelps, square);
109
+ const skewer = signsOfHelps === undefined || control !== undefined ? undefined : skewerOn(signsOfHelps, square);
107
110
  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
111
  // A click with no pointer behind it, as a screen reader's in browse mode, presses the square as Enter does.
109
112
  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
113
  ? (_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
114
+ : 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));
115
+ })), signsOfHelps === undefined || signsOfHelps.pins.length + signsOfHelps.skewers.length === 0 || control !== undefined
116
+ ? null
117
+ : _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
118
  ? null
114
119
  : _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
120
  ? "above"
@@ -1,12 +1,21 @@
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
+ /** 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. */
9
+ export function Pushpin({ tone }) {
10
+ return _jsx(Stuck, { kind: "help-pin", shape: pushpin, tone: tone });
11
+ }
12
+ /**
13
+ * 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,
14
+ * 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.
15
+ */
16
+ export function Skewer({ tone }) {
17
+ 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)) }));
18
+ }
10
19
  /** A king's badge in the corner of its square. */
11
20
  export function Badge({ kind }) {
12
21
  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 +55,20 @@ export function Arrows({ arrows, at, explain, occupied }) {
46
55
  const starts = [...new Set(shapes.map(({ from }) => from))];
47
56
  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
57
  }
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) => {
58
+ /**
59
+ * 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
60
+ * 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.
61
+ */
62
+ export function PinsAndSkewers({ pins, skewers, at }) {
63
+ const lines = [...pins.map((line) => ({ kind: "pin", line, other: skewers })), ...skewers.map((line) => ({ kind: "skewer", line, other: pins }))];
64
+ return (_jsx("svg", { class: "helps-layer", viewBox: "0 0 8 8", "aria-hidden": "true", children: lines.map(({ kind, line, other }) => {
52
65
  // 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}`));
66
+ const [startX, startY] = towards(at(line.by.square), at(line.to.square), pinTrim);
67
+ const [endX, endY] = towards(at(line.to.square), at(line.by.square), pinTrim);
68
+ const shift = other.some((each) => sharedSquares(each, line) > 1) ? (kind === "pin" ? 1 : -1) * sharedLineShift : 0;
69
+ const [dx, dy] = across([startX, startY], [endX, endY], shift);
70
+ const ends = { x1: startX + dx, y1: startY + dy, x2: endX + dx, y2: endY + dy };
71
+ return (_jsxs("g", { class: kind, children: [_jsx("line", { class: "halo", ...ends }), _jsx("line", { ...ends })] }, `${kind} ${line.by.square}${line.piece.square}`));
56
72
  }) }));
57
73
  }
58
74
  /** 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. */
@@ -135,6 +151,24 @@ function ArrowGroup({ shapes, edged }) {
135
151
  ? shapes.map(({ key, shaft, head }) => (_jsxs("g", { children: [_jsx("path", { class: "edge", d: shaft }), _jsx("polygon", { class: "edge", points: head })] }, `edge${key}`)))
136
152
  : null, shapes.map(({ key, colour, shaft, head }) => (_jsxs("g", { class: colour, children: [_jsx("path", { d: shaft }), _jsx("polygon", { points: head })] }, key)))] }));
137
153
  }
154
+ /**
155
+ * 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
156
+ * its halo only, so that it sits in the corner (Stefano).
157
+ */
158
+ function Stuck({ kind, shape, tone, children }) {
159
+ 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] }) }) }));
160
+ }
161
+ /** How many of their three squares a pin and a skewer share: two when they lie on the same stretch of a line. */
162
+ function sharedSquares(one, other) {
163
+ const squares = [one.by.square, one.piece.square, one.to.square];
164
+ return [other.by.square, other.piece.square, other.to.square].filter((square) => squares.includes(square)).length;
165
+ }
166
+ /** A step of the given length across a line, always to the same side of it whichever way the line runs. */
167
+ function across(start, end, distance) {
168
+ const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
169
+ const way = end[0] < start[0] || (end[0] === start[0] && end[1] < start[1]) ? -1 : 1;
170
+ return [-(end[1] - start[1]) / length * distance * way, (end[0] - start[0]) / length * distance * way];
171
+ }
138
172
  /** A point `distance` squares from `point` toward `target`. */
139
173
  function towards(point, target, distance) {
140
174
  const length = Math.hypot(target[0] - point[0], target[1] - point[1]);
@@ -150,10 +184,18 @@ function ExplainIcon({ kind, piece }) {
150
184
  : _jsx("path", { d: "M4 8h15M15 4l4 4-4 4M20 16H5M9 12l-4 4 4 4" }) })) }));
151
185
  }
152
186
  const promotionPieces = ["q", "n", "r", "b"];
187
+ // 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.
188
+ 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";
189
+ // 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.
190
+ 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";
191
+ // The skewer's beads, on its stick: the big one and the small one, as its shape draws them.
192
+ const skewerBeads = [{ y: 7.8, radius: 4.2 }, { y: 15.9, radius: 3.1 }];
153
193
  // 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
194
  const explainTrim = { start: 0.36, end: 0.4 };
155
195
  // How much a pin's line is cut at each end, in squares.
156
196
  const pinTrim = 0.35;
197
+ // 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.
198
+ const sharedLineShift = 0.08;
157
199
  // An arrow's head, in squares: how long, and how far each side of its line.
158
200
  const arrowHead = { length: 0.42, width: 0.3 };
159
201
  // The helps' point, smaller in step with their thinner shaft (Stefano).
@@ -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,18 @@
383
456
  fill: var(--card);
384
457
  }
385
458
 
459
+ /* The skewer of the board, in the line's colour and without its halo */
460
+ .help-rows .help-skewer {
461
+ left: 7px;
462
+ top: 4px;
463
+ height: 17px;
464
+ }
465
+
466
+ .help-rows .help-skewer path {
467
+ fill: var(--row);
468
+ stroke: none;
469
+ }
470
+
386
471
  .help-rows .row-icon {
387
472
  position: absolute;
388
473
  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, 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";
@@ -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,11 +86,14 @@ 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: the corner of a piece at risk, a pin's link, the skewer of the board, a check's plus, the loose pieces' i; its colour is the line's. */
90
90
  function LineIcon({ kind }) {
91
91
  if (kind === "risk") {
92
92
  return _jsx(Notch, {});
93
93
  }
94
+ if (kind === "skewer") {
95
+ return _jsx(Skewer, {});
96
+ }
94
97
  if (kind === "loose") {
95
98
  return _jsx(InfoIcon, {});
96
99
  }
@@ -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",
package/dist/page/text.js CHANGED
@@ -274,16 +274,18 @@ const english = {
274
274
  outnumbered: "more attackers than defenders",
275
275
  pinnedTo: (letter) => `pinned to the ${englishNames[letter]}`,
276
276
  pinnedBy: (letter) => `by the ${englishNames[letter]}`,
277
+ skeweredBefore: (letter) => `ahead of the ${englishNames[letter]}`,
277
278
  nothingAtRisk: "No piece at risk, no pins.",
278
279
  line: (kind, side, you, toMove) => {
279
280
  const name = side === "white" ? "White" : "Black";
281
+ const pieces = { risk: "pieces at risk", pin: "pinned pieces", skewer: "skewered pieces" };
280
282
  if (you === undefined) {
281
- return kind === "risk" ? `${name}'s pieces at risk` : kind === "pin" ? `${name}'s pinned pieces` : `${name}'s checks${toMove ? "" : `, if it were ${name}'s move`}`;
283
+ return kind === "check" ? `${name}'s checks${toMove ? "" : `, if it were ${name}'s move`}` : `${name}'s ${pieces[kind]}`;
282
284
  }
283
285
  if (kind === "check") {
284
286
  return side === you ? `Checks you ${toMove ? "can" : "could"} give` : `Checks the opponent ${toMove ? "can" : "could"} give you`;
285
287
  }
286
- return `${side === you ? "Your" : "The opponent's"} ${kind === "risk" ? "pieces at risk" : "pinned pieces"}`;
288
+ return `${side === you ? "Your" : "The opponent's"} ${pieces[kind]}`;
287
289
  },
288
290
  loose: "Pieces neither attacked nor defended",
289
291
  yours: "yours",
@@ -594,16 +596,18 @@ const italian = {
594
596
  outnumbered: "più attaccanti che difensori",
595
597
  pinnedTo: (letter) => `davanti ${italianWith("a", letter)}`,
596
598
  pinnedBy: (letter) => italianWith("da", letter),
599
+ skeweredBefore: (letter) => `con dietro ${italianThe[letter]}`,
597
600
  nothingAtRisk: "Nessun pezzo in pericolo, nessuna inchiodatura.",
598
601
  line: (kind, side, you, toMove) => {
599
602
  const name = side === "white" ? "Bianco" : "Nero";
603
+ const what = { risk: "in pericolo", pin: "inchiodati", skewer: "infilati" };
600
604
  if (you === undefined) {
601
- return kind === "risk" ? `Pezzi del ${name} in pericolo` : kind === "pin" ? `Pezzi del ${name} inchiodati` : `Scacchi del ${name}${toMove ? "" : ", se toccasse a lui"}`;
605
+ return kind === "check" ? `Scacchi del ${name}${toMove ? "" : ", se toccasse a lui"}` : `Pezzi del ${name} ${what[kind]}`;
602
606
  }
603
607
  if (kind === "check") {
604
608
  return side === you ? `Scacchi che ${toMove ? "puoi" : "potresti"} dare` : `Scacchi che l'avversario ${toMove ? "può" : "potrebbe"} darti`;
605
609
  }
606
- return `${side === you ? "I tuoi pezzi" : "I pezzi avversari"} ${kind === "risk" ? "in pericolo" : "inchiodati"}`;
610
+ return `${side === you ? "I tuoi pezzi" : "I pezzi avversari"} ${what[kind]}`;
607
611
  },
608
612
  loose: "Pezzi né attaccati né difesi",
609
613
  yours: "tuoi",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chesspal",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Play chess against an AI with a character, and learn with an AI coach: Chesspal keeps the rules, the board, the games and the memory; the AI plays, reasons and talks.",
5
5
  "type": "module",
6
6
  "bin": {