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.
- package/dist/core/facts.d.ts +13 -0
- package/dist/core/facts.js +27 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/page/board/board.js +9 -4
- package/dist/page/board/marks.js +50 -8
- package/dist/page/helps/helps.css +85 -0
- package/dist/page/helps/helps.js +8 -3
- package/dist/page/helps/view.js +19 -5
- package/dist/page/text.js +8 -4
- package/package.json +1 -1
package/dist/core/facts.d.ts
CHANGED
|
@@ -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. */
|
package/dist/core/facts.js
CHANGED
|
@@ -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];
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/page/board/board.js
CHANGED
|
@@ -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,
|
|
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
|
|
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"
|
package/dist/page/board/marks.js
CHANGED
|
@@ -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
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
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 [
|
|
54
|
-
const [
|
|
55
|
-
|
|
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;
|
package/dist/page/helps/helps.js
CHANGED
|
@@ -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":
|
package/dist/page/helps/view.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
|
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 === "
|
|
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
|
|
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 === "
|
|
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
|
|
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.
|
|
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": {
|