chesspal 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -2
- package/dist/cli/chat-hook.d.ts +2 -0
- package/dist/cli/chat-hook.js +33 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +28 -0
- package/dist/cli/command-line.d.ts +7 -0
- package/dist/cli/command-line.js +177 -0
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +28 -0
- package/dist/commands/ai-move.js +85 -0
- package/dist/commands/ai-say.d.ts +22 -0
- package/dist/commands/ai-say.js +58 -0
- package/dist/commands/analyse-game.d.ts +65 -0
- package/dist/commands/analyse-game.js +265 -0
- package/dist/commands/annotate-game.d.ts +39 -0
- package/dist/commands/annotate-game.js +131 -0
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +67 -0
- package/dist/commands/context.js +37 -0
- package/dist/commands/delete-game.d.ts +7 -0
- package/dist/commands/delete-game.js +30 -0
- package/dist/commands/delete-review.d.ts +8 -0
- package/dist/commands/delete-review.js +34 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +13 -0
- package/dist/commands/drawings.js +25 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +67 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +27 -0
- package/dist/commands/game-summary.d.ts +78 -0
- package/dist/commands/game-summary.js +102 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.js +64 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -0
- package/dist/commands/levels.d.ts +9 -0
- package/dist/commands/levels.js +25 -0
- package/dist/commands/look.d.ts +31 -0
- package/dist/commands/look.js +64 -0
- package/dist/commands/new-game.d.ts +68 -0
- package/dist/commands/new-game.js +218 -0
- package/dist/commands/opening-book.d.ts +13 -0
- package/dist/commands/opening-book.js +84 -0
- package/dist/commands/page-link.d.ts +11 -0
- package/dist/commands/page-link.js +28 -0
- package/dist/commands/past-games.d.ts +11 -0
- package/dist/commands/past-games.js +47 -0
- package/dist/commands/play-move.d.ts +37 -0
- package/dist/commands/play-move.js +94 -0
- package/dist/commands/player-move.d.ts +13 -0
- package/dist/commands/player-move.js +51 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +52 -0
- package/dist/commands/ponder.js +179 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +130 -0
- package/dist/commands/profiles.d.ts +69 -0
- package/dist/commands/profiles.js +163 -0
- package/dist/commands/registry.d.ts +59 -0
- package/dist/commands/registry.js +62 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +35 -0
- package/dist/commands/status.js +128 -0
- package/dist/commands/study-games.d.ts +5 -0
- package/dist/commands/study-games.js +40 -0
- package/dist/commands/study.d.ts +38 -0
- package/dist/commands/study.js +106 -0
- package/dist/commands/view.d.ts +14 -0
- package/dist/commands/view.js +125 -0
- package/dist/core/board-text.d.ts +7 -0
- package/dist/core/board-text.js +18 -0
- package/dist/core/book.json +1 -0
- package/dist/core/clock.d.ts +45 -0
- package/dist/core/clock.js +109 -0
- package/dist/core/facts.d.ts +55 -0
- package/dist/core/facts.js +175 -0
- package/dist/core/game.d.ts +95 -0
- package/dist/core/game.js +352 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +12 -0
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -0
- package/dist/core/levels.d.ts +20 -0
- package/dist/core/levels.js +33 -0
- package/dist/core/material.d.ts +9 -0
- package/dist/core/material.js +12 -0
- package/dist/core/moods.d.ts +21 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +35 -0
- package/dist/core/openings.js +98 -0
- package/dist/core/openings.json +1 -0
- package/dist/core/pgn.d.ts +10 -0
- package/dist/core/pgn.js +213 -0
- package/dist/core/profile.d.ts +117 -0
- package/dist/core/profile.js +232 -0
- package/dist/core/record.d.ts +21 -0
- package/dist/core/record.js +25 -0
- package/dist/core/review.d.ts +80 -0
- package/dist/core/review.js +146 -0
- package/dist/games/ai-model.d.ts +5 -0
- package/dist/games/ai-model.js +15 -0
- package/dist/games/analysis.d.ts +12 -0
- package/dist/games/analysis.js +26 -0
- package/dist/games/archive.d.ts +63 -0
- package/dist/games/archive.js +242 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +56 -0
- package/dist/games/current-game.js +134 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +53 -0
- package/dist/games/index.d.ts +11 -0
- package/dist/games/index.js +12 -0
- package/dist/games/plans.d.ts +9 -0
- package/dist/games/plans.js +23 -0
- package/dist/games/table-talk.d.ts +12 -0
- package/dist/games/table-talk.js +25 -0
- package/dist/games/talk.d.ts +36 -0
- package/dist/games/talk.js +131 -0
- package/dist/games/thoughts.d.ts +15 -0
- package/dist/games/thoughts.js +60 -0
- package/dist/home/data-folder.d.ts +12 -0
- package/dist/home/data-folder.js +55 -0
- package/dist/home/index.d.ts +2 -0
- package/dist/home/index.js +3 -0
- package/dist/home/open-folder.d.ts +2 -0
- package/dist/home/open-folder.js +14 -0
- package/dist/host-files/agents.d.ts +20 -0
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +15 -0
- package/dist/host-files/host-files.js +182 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/app/app.js +458 -0
- package/dist/page/app/keys.js +94 -0
- package/dist/page/app/phase.js +30 -0
- package/dist/page/app/served.js +73 -0
- package/dist/page/avatars/avatars.js +19 -0
- package/dist/page/avatars/beanie.webp +0 -0
- package/dist/page/avatars/beard.webp +0 -0
- package/dist/page/avatars/bun.webp +0 -0
- package/dist/page/avatars/cat.webp +0 -0
- package/dist/page/avatars/curls.webp +0 -0
- package/dist/page/avatars/glasses.webp +0 -0
- package/dist/page/avatars/moustache.webp +0 -0
- package/dist/page/avatars/owl.webp +0 -0
- package/dist/page/avatars/short-hair.webp +0 -0
- package/dist/page/avatars/silver-bob.webp +0 -0
- package/dist/page/avatars/thought.js +10 -0
- package/dist/page/avatars/torretta.js +38 -0
- package/dist/page/base.css +160 -0
- package/dist/page/board/board.css +959 -0
- package/dist/page/board/board.js +125 -0
- package/dist/page/board/explain.js +58 -0
- package/dist/page/board/marks.js +160 -0
- package/dist/page/board/motion.js +71 -0
- package/dist/page/board/pointer.js +239 -0
- package/dist/page/board/squares.js +26 -0
- package/dist/page/dialogs/analyses.js +195 -0
- package/dist/page/dialogs/dialog.js +26 -0
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/dialogs/new-game.js +42 -0
- package/dist/page/dialogs/profiles.js +152 -0
- package/dist/page/dialogs/search.js +31 -0
- package/dist/page/dialogs/settings.js +42 -0
- package/dist/page/dialogs/studied.js +39 -0
- package/dist/page/dialogs/study.js +51 -0
- package/dist/page/game/badges.js +50 -0
- package/dist/page/game/browse.js +17 -0
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +51 -0
- package/dist/page/game/ending.js +73 -0
- package/dist/page/game/game.css +577 -0
- package/dist/page/game/move-list.js +72 -0
- package/dist/page/game/opening-book.js +14 -0
- package/dist/page/game/plies.js +82 -0
- package/dist/page/game/result-tile.js +14 -0
- package/dist/page/helps/helps.css +689 -0
- package/dist/page/helps/helps.js +220 -0
- package/dist/page/helps/view.js +127 -0
- package/dist/page/layout/fold-tab.js +11 -0
- package/dist/page/layout/folds.js +43 -0
- package/dist/page/layout/header.js +28 -0
- package/dist/page/layout/layout.css +834 -0
- package/dist/page/lead/lead.js +82 -0
- package/dist/page/lead/view.js +38 -0
- package/dist/page/main.js +8 -0
- package/dist/page/pieces/CREDITS.md +19 -0
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +20 -0
- package/dist/page/pieces/nitida.svg +21 -0
- package/dist/page/pieces/pieces.js +28 -0
- package/dist/page/pieces/torretta-small.svg +20 -0
- package/dist/page/pieces/torretta.svg +21 -0
- package/dist/page/players/card.js +56 -0
- package/dist/page/players/clock-time.js +12 -0
- package/dist/page/players/model-name.js +20 -0
- package/dist/page/profile/profile.js +99 -0
- package/dist/page/profile/settings.js +77 -0
- package/dist/page/sounds/capture.ogg +0 -0
- package/dist/page/sounds/check.ogg +0 -0
- package/dist/page/sounds/draw.ogg +0 -0
- package/dist/page/sounds/flag.ogg +0 -0
- package/dist/page/sounds/mate.ogg +0 -0
- package/dist/page/sounds/move.ogg +0 -0
- package/dist/page/sounds/offer.ogg +0 -0
- package/dist/page/sounds/resign.ogg +0 -0
- package/dist/page/sounds/sounds.js +105 -0
- package/dist/page/style.css +9 -0
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/talk/talk.js +165 -0
- package/dist/page/talk/voice.js +38 -0
- package/dist/page/text.js +711 -0
- package/dist/page/theme.css +292 -0
- package/dist/profiles/avatar.d.ts +23 -0
- package/dist/profiles/avatar.js +84 -0
- package/dist/profiles/front-matter.d.ts +9 -0
- package/dist/profiles/front-matter.js +33 -0
- package/dist/profiles/index.d.ts +3 -0
- package/dist/profiles/index.js +5 -0
- package/dist/profiles/profiles.d.ts +69 -0
- package/dist/profiles/profiles.js +254 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/bell.d.ts +68 -0
- package/dist/server/bell.js +225 -0
- package/dist/server/caller.d.ts +21 -0
- package/dist/server/caller.js +132 -0
- package/dist/server/mcp.d.ts +13 -0
- package/dist/server/mcp.js +207 -0
- package/dist/server/page/analyses.d.ts +23 -0
- package/dist/server/page/analyses.js +65 -0
- package/dist/server/page/files.d.ts +5 -0
- package/dist/server/page/files.js +92 -0
- package/dist/server/page/game.d.ts +6 -0
- package/dist/server/page/game.js +381 -0
- package/dist/server/page/http.d.ts +29 -0
- package/dist/server/page/http.js +68 -0
- package/dist/server/page/page.d.ts +41 -0
- package/dist/server/page/page.js +229 -0
- package/dist/server/page/profiles.d.ts +19 -0
- package/dist/server/page/profiles.js +215 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +4 -0
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "preact/jsx-runtime";
|
|
2
|
+
// The board: eight ranks of squares from a FEN, one side at the bottom, Zoe's pieces, the ranks and files on the edge squares. The player moves
|
|
3
|
+
// with two clicks or by dragging a piece, which lifts off the board, in Zoe's design.
|
|
4
|
+
import { useMemo, useRef } from "preact/hooks";
|
|
5
|
+
import { BadgeIcon } from "../game/badges.js";
|
|
6
|
+
import { riskOn } from "../helps/view.js";
|
|
7
|
+
import { Piece } from "../pieces/pieces.js";
|
|
8
|
+
import { bubblePlace, keptSquares } from "./explain.js";
|
|
9
|
+
import { Arrows, Badge, Bubble, ControlCount, controlOf, kingSigns, Lifted, noSigns, Notch, Pins, PromotionChoice } from "./marks.js";
|
|
10
|
+
import { backOf, motionOf, noMotion, playedOn, rearranged } from "./motion.js";
|
|
11
|
+
import { useBoardPointer } from "./pointer.js";
|
|
12
|
+
import { files, kingToMove, piecesOf } from "./squares.js";
|
|
13
|
+
/**
|
|
14
|
+
* The board of a position, seen from one side, with the last move's squares marked. While the player may move, their pieces answer the pointer: a press
|
|
15
|
+
* picks one, and the legal moves of the piece picked show as dots, rings on captures; a second press on one of them, or a drag there, plays it. At any time
|
|
16
|
+
* the right button lights a square, or draws an arrow when dragged, until a left click.
|
|
17
|
+
*/
|
|
18
|
+
export function Board({ fen, bottom, lastMove, pointed, helps, control, bare, badge, drawn, nextMove, jumped, check, end, player, selected, targets, dots, names, notice, promotion, onSelect, onMove, onRefuse, onPromote, text }) {
|
|
19
|
+
const pieces = piecesOf(fen);
|
|
20
|
+
const mated = end?.how === "checkmate" ? kingToMove(fen, pieces) : undefined;
|
|
21
|
+
const signs = end === undefined ? noSigns : kingSigns(pieces, fen.split(" ")[1] === "b" ? "black" : "white", end);
|
|
22
|
+
const ranks = bottom === "white" ? [8, 7, 6, 5, 4, 3, 2, 1] : [1, 2, 3, 4, 5, 6, 7, 8];
|
|
23
|
+
const columns = bottom === "white" ? [...files] : [...files].reverse();
|
|
24
|
+
const seen = (square) => [columns.indexOf(square[0]), ranks.indexOf(Number(square[1]))];
|
|
25
|
+
const centreOf = (square) => [columns.indexOf(square[0]) + 0.5, ranks.indexOf(Number(square[1])) + 0.5];
|
|
26
|
+
const { board, drag, marks, drawing, focused, dragged, own, isTarget, down, moveTo, release, key, pressKey, promote, setCursor } = useBoardPointer({
|
|
27
|
+
columns,
|
|
28
|
+
ranks,
|
|
29
|
+
pieces,
|
|
30
|
+
player,
|
|
31
|
+
selected,
|
|
32
|
+
targets,
|
|
33
|
+
promotion,
|
|
34
|
+
onSelect,
|
|
35
|
+
onMove,
|
|
36
|
+
onRefuse,
|
|
37
|
+
onPromote,
|
|
38
|
+
});
|
|
39
|
+
// A new position slides the pieces of its move there, but for the one the player dragged, and shakes a king in check.
|
|
40
|
+
const before = useRef(undefined);
|
|
41
|
+
const motion = useMemo(() => {
|
|
42
|
+
const was = before.current === undefined ? undefined : piecesOf(before.current);
|
|
43
|
+
before.current = fen;
|
|
44
|
+
// A move played on the position before slides there, a step back over one slides it home; a jump through the game moves every piece to its place.
|
|
45
|
+
const fresh = was !== undefined && jumped !== true && lastMove !== undefined && playedOn(was, pieces, lastMove);
|
|
46
|
+
const undone = was !== undefined && jumped !== true && nextMove !== undefined && playedOn(pieces, was, nextMove);
|
|
47
|
+
const moved = was === undefined
|
|
48
|
+
? noMotion
|
|
49
|
+
: fresh
|
|
50
|
+
? motionOf(lastMove, dragged.current, check ? kingToMove(fen, pieces) : undefined, was)
|
|
51
|
+
: undone
|
|
52
|
+
? backOf(nextMove)
|
|
53
|
+
: jumped === true
|
|
54
|
+
? rearranged(was, pieces)
|
|
55
|
+
: noMotion;
|
|
56
|
+
dragged.current = undefined;
|
|
57
|
+
return moved;
|
|
58
|
+
}, [fen]);
|
|
59
|
+
// The arrows on the board, the AI's and the coach's under the player's, none while the board is bare; the helps' step aside while the player picks up
|
|
60
|
+
// a piece to move it (Stefano).
|
|
61
|
+
const aiArrows = bare === true || selected !== undefined || drag !== undefined ? [] : drawn?.arrows ?? [];
|
|
62
|
+
const ownArrows = bare === true ? [] : drawing === undefined ? marks.arrows : [...marks.arrows, drawing];
|
|
63
|
+
// The bare board hides the helps' signs too, the corners on the pieces at risk and the pins' lines (Stefano).
|
|
64
|
+
const signsOfHelps = bare === true ? undefined : helps;
|
|
65
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: board, class: ["board", player === undefined ? "" : "playable", end === undefined ? "" : endClasses(end), control === undefined ? "" : "mapping", names ? "named" : ""].filter((name) => name !== "").join(" "), role: "group", "aria-label": text.boardLabel(fen.split(" ")[1] === "b" ? "black" : "white", lastMove?.san), onPointerDown: down, onPointerMove: moveTo, onPointerUp: (event) => release(event, false), onPointerCancel: (event) => release(event, true), onContextMenu: (event) => event.preventDefault(),
|
|
66
|
+
// A click takes no focus, nor leaves it elsewhere: the arrows keep browsing the moves; a square has the focus only from the keyboard.
|
|
67
|
+
onMouseDown: (event) => {
|
|
68
|
+
event.preventDefault();
|
|
69
|
+
document.activeElement?.blur();
|
|
70
|
+
}, children: [ranks.flatMap((rank, row) => columns.map((file, column) => {
|
|
71
|
+
const square = `${file}${rank}`;
|
|
72
|
+
const piece = pieces.get(square);
|
|
73
|
+
const dark = (files.indexOf(file) + rank) % 2 === 1;
|
|
74
|
+
const marked = lastMove !== undefined && (lastMove.from === square || lastMove.to === square);
|
|
75
|
+
const target = targets.find((move) => move.to === square);
|
|
76
|
+
const sign = signs.get(square);
|
|
77
|
+
const tinted = bare === true ? undefined : drawn?.squares?.find((one) => one.square === square)?.colour;
|
|
78
|
+
const classes = [
|
|
79
|
+
"square",
|
|
80
|
+
dark ? "dark" : "light",
|
|
81
|
+
marked ? "last" : "",
|
|
82
|
+
mated === square ? "mated" : "",
|
|
83
|
+
sign?.classes ?? "",
|
|
84
|
+
selected === square ? "selected" : "",
|
|
85
|
+
tinted === undefined ? "" : `noted ${tinted}`,
|
|
86
|
+
bare !== true && marks.squares.includes(square) ? "marked" : "",
|
|
87
|
+
pointed?.includes(square) === true ? "pointed" : "",
|
|
88
|
+
notice?.explanation?.ring === square ? "culprit" : "",
|
|
89
|
+
own(square) ? "own" : "",
|
|
90
|
+
target === undefined ? "" : !dots ? "target" : target.san.includes("x") ? "hint capture" : "hint",
|
|
91
|
+
drag?.over === square && drag.over !== drag.from ? "over" : "",
|
|
92
|
+
drag?.from === square ? "from" : "",
|
|
93
|
+
motion.slides.has(square) ? "slide" : "",
|
|
94
|
+
motion.shake === square ? "shake" : "",
|
|
95
|
+
motion.appear?.has(square) === true ? "appear" : "",
|
|
96
|
+
].filter((name) => name !== "").join(" ");
|
|
97
|
+
const named = piece === undefined ? square : `${square}, ${text.piece(piece.toUpperCase(), piece === piece.toUpperCase() ? "white" : "black")}`;
|
|
98
|
+
const label = [named, mated === square ? text.checkmate : "", isTarget(square) ? text.canGo : ""].filter((part) => part !== "").join(", ");
|
|
99
|
+
const from = motion.slides.get(square);
|
|
100
|
+
const moving = from !== undefined || motion.shake === square || motion.appear?.has(square) === true;
|
|
101
|
+
// How far the sliding piece comes from, in squares as the board is turned.
|
|
102
|
+
const away = from === undefined ? undefined : {
|
|
103
|
+
"--dx": columns.indexOf(from[0]) - column,
|
|
104
|
+
"--dy": ranks.indexOf(Number(from[1])) - row,
|
|
105
|
+
};
|
|
106
|
+
const risk = signsOfHelps === undefined ? undefined : riskOn(signsOfHelps, square);
|
|
107
|
+
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
|
+
// A click with no pointer behind it, as a screen reader's in browse mode, presses the square as Enter does.
|
|
109
|
+
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
|
+
? (_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
|
|
113
|
+
? null
|
|
114
|
+
: _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
|
+
? "above"
|
|
116
|
+
: bubblePlace(seen(notice.square), keptSquares(notice.explanation).map(seen), (board.current?.clientWidth ?? 0) < narrowBoard) }))] }));
|
|
117
|
+
}
|
|
118
|
+
/** The board's classes at the end: how the game ended, and, a moment after it did, whether the player won or lost, which pick the effects in the style. */
|
|
119
|
+
function endClasses(end) {
|
|
120
|
+
const side = end.winner === undefined ? "" : end.winner === end.player ? "player-won" : "player-lost";
|
|
121
|
+
const classes = ["ended", end.how === undefined ? "" : `by-${end.how.replaceAll(" ", "-")}`, end.fresh ? `fresh ${side}` : ""];
|
|
122
|
+
return classes.filter((name) => name !== "").join(" ").trim();
|
|
123
|
+
}
|
|
124
|
+
// A board narrower than this, in pixels, is a phone's: its bubble takes the board's width, and goes under the board when it has no room on it.
|
|
125
|
+
const narrowBoard = 520;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** The drawing for a move refused, `to` being where it went and `king` the square of the player's king. */
|
|
2
|
+
export function explain(illegal, to, king) {
|
|
3
|
+
const kind = kinds[illegal.reason];
|
|
4
|
+
const by = illegal.by;
|
|
5
|
+
if (by === undefined) {
|
|
6
|
+
return { kind };
|
|
7
|
+
}
|
|
8
|
+
const target = illegal.reason === "attacked" ? to : illegal.reason === "castle-through" ? illegal.at : arrowsToKing.has(illegal.reason) ? king : undefined;
|
|
9
|
+
return target === undefined ? { kind, ring: by } : { kind, arrow: { from: by, to: target } };
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Where the bubble of a refused move goes (Zoe's rule), squares as seen, in columns and rows from the top left: over the piece moved, else under it,
|
|
13
|
+
* so as never to hide the squares to `keep` (the culprit, the arrow's point); else under the board on a narrow one, or beside the piece on a wide one.
|
|
14
|
+
*/
|
|
15
|
+
export function bubblePlace(piece, keep, narrow) {
|
|
16
|
+
const [column, row] = piece;
|
|
17
|
+
// On a phone's board the bubble is taller than a square: it takes two rows.
|
|
18
|
+
const rows = narrow ? 2 : 1;
|
|
19
|
+
const free = (first, step) => first + step * (rows - 1) >= 0 && first + step * (rows - 1) <= 7 && !keep.some(([, kept]) => (kept - first) * step >= 0 && (kept - first) * step < rows);
|
|
20
|
+
if (free(row - 1, -1)) {
|
|
21
|
+
return "above";
|
|
22
|
+
}
|
|
23
|
+
if (free(row + 1, 1)) {
|
|
24
|
+
return "below";
|
|
25
|
+
}
|
|
26
|
+
if (narrow) {
|
|
27
|
+
return "under-board";
|
|
28
|
+
}
|
|
29
|
+
const right = keep.some(([kept, keptRow]) => keptRow === row && kept > column);
|
|
30
|
+
const left = keep.some(([kept, keptRow]) => keptRow === row && kept < column);
|
|
31
|
+
return right === left ? (column < 4 ? "right" : "left") : right ? "left" : "right";
|
|
32
|
+
}
|
|
33
|
+
/** The squares a bubble must leave in sight: the culprit, with its ring or the start of its arrow, and the arrow's point. */
|
|
34
|
+
export function keptSquares(explanation) {
|
|
35
|
+
if (explanation === undefined) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return explanation.arrow === undefined ? (explanation.ring === undefined ? [] : [explanation.ring]) : [explanation.arrow.from, explanation.arrow.to];
|
|
39
|
+
}
|
|
40
|
+
const kinds = {
|
|
41
|
+
"own": "piece",
|
|
42
|
+
"shape": "piece",
|
|
43
|
+
"pawn-straight": "piece",
|
|
44
|
+
"pawn-double": "piece",
|
|
45
|
+
"pawn-diagonal": "piece",
|
|
46
|
+
"en-passant-late": "piece",
|
|
47
|
+
"blocked": "blocked",
|
|
48
|
+
"pinned": "pin",
|
|
49
|
+
"check": "king",
|
|
50
|
+
"attacked": "king",
|
|
51
|
+
"castle-lost": "castle",
|
|
52
|
+
"castle-rook-gone": "castle",
|
|
53
|
+
"castle-blocked": "castle",
|
|
54
|
+
"castle-check": "castle",
|
|
55
|
+
"castle-through": "castle",
|
|
56
|
+
};
|
|
57
|
+
// The reasons whose culprit aims at the player's king.
|
|
58
|
+
const arrowsToKing = new Set(["pinned", "check", "castle-check"]);
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
import { useEffect, useRef, useState } from "preact/hooks";
|
|
5
|
+
import { Piece } from "../pieces/pieces.js";
|
|
6
|
+
/** 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
|
+
export function Notch({ tone }) {
|
|
8
|
+
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
|
+
}
|
|
10
|
+
/** A king's badge in the corner of its square. */
|
|
11
|
+
export function Badge({ kind }) {
|
|
12
|
+
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" }) })) }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The arrows over the board, in squares from its top left corner: a knight's turns once, the long leg first. In ink when they explain a refused move, and
|
|
16
|
+
* cut short (Zoe): off the culprit's piece, and before the piece they point at, so both stay in sight.
|
|
17
|
+
*/
|
|
18
|
+
export function Arrows({ arrows, at, explain, occupied }) {
|
|
19
|
+
if (arrows.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const point = occupied === undefined ? arrowHead : helpsHead;
|
|
23
|
+
const shapes = arrows.map(({ from, to, colour }) => {
|
|
24
|
+
const [startX, startY] = at(from);
|
|
25
|
+
const [pointX, pointY] = at(to);
|
|
26
|
+
const knight = Math.abs(pointX - startX) * Math.abs(pointY - startY) === 2;
|
|
27
|
+
const corner = !knight
|
|
28
|
+
? undefined
|
|
29
|
+
: Math.abs(pointY - startY) > Math.abs(pointX - startX)
|
|
30
|
+
? [startX, pointY]
|
|
31
|
+
: [pointX, startY];
|
|
32
|
+
const [x0, y0] = explain === undefined ? [startX, startY] : towards([startX, startY], corner ?? [pointX, pointY], explainTrim.start);
|
|
33
|
+
const [x1, y1] = explain?.occupied === true || occupied?.(to) === true ? towards([pointX, pointY], corner ?? [startX, startY], explainTrim.end) : [pointX, pointY];
|
|
34
|
+
const [fromX, fromY] = corner ?? [x0, y0];
|
|
35
|
+
const length = Math.hypot(x1 - fromX, y1 - fromY);
|
|
36
|
+
const [dx, dy] = [(x1 - fromX) / length, (y1 - fromY) / length];
|
|
37
|
+
const [endX, endY] = [x1 - dx * point.length, y1 - dy * point.length];
|
|
38
|
+
const side = (sign) => `${endX - dy * point.width * sign},${endY + dx * point.width * sign}`;
|
|
39
|
+
// The shaft runs a quarter into the point, and its round cap a little further, so no seam shows and the cap stays inside the point.
|
|
40
|
+
const [shaftX, shaftY] = [x1 - dx * point.length * 0.75, y1 - dy * point.length * 0.75];
|
|
41
|
+
const shaft = `M${x0} ${y0}${corner === undefined ? "" : ` L${corner[0]} ${corner[1]}`} L${shaftX} ${shaftY}`;
|
|
42
|
+
const head = `${side(1)} ${x1},${y1} ${side(-1)}`;
|
|
43
|
+
return { key: `${from}${to}`, from, colour, shaft, head };
|
|
44
|
+
});
|
|
45
|
+
// Arrows off one square meet in one shape; an arrow from elsewhere keeps its own edge over them, so no shape is lost where points meet (Stefano).
|
|
46
|
+
const starts = [...new Set(shapes.map(({ from }) => from))];
|
|
47
|
+
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
|
+
}
|
|
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) => {
|
|
52
|
+
// 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}`));
|
|
56
|
+
}) }));
|
|
57
|
+
}
|
|
58
|
+
/** 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. */
|
|
59
|
+
export function ControlCount({ counts }) {
|
|
60
|
+
const [white, black] = counts ?? [0, 0];
|
|
61
|
+
if (white === 0 && black === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return (_jsxs("span", { class: "control-count", "aria-hidden": "true", children: [_jsx("b", { class: "white", children: white }), _jsx("b", { class: "black", children: black })] }));
|
|
65
|
+
}
|
|
66
|
+
/** Who reaches a square on the control map: only White, only Black, both or none. */
|
|
67
|
+
export function controlOf(counts) {
|
|
68
|
+
const [white, black] = counts ?? [0, 0];
|
|
69
|
+
return white > 0 && black > 0 ? "both" : white > 0 ? "white" : black > 0 ? "black" : "none";
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A bubble by the piece of a refused move, where it hides neither the culprit nor the arrow's point (`where`), kept on the board at its edges; in the
|
|
73
|
+
* board's middle without a piece.
|
|
74
|
+
*/
|
|
75
|
+
export function Bubble({ notice, place, where }) {
|
|
76
|
+
if (place === undefined) {
|
|
77
|
+
return _jsx("p", { class: "notice centre", role: "alert", children: notice.text }, notice.text);
|
|
78
|
+
}
|
|
79
|
+
const [column, row] = place;
|
|
80
|
+
const align = column <= 1 ? "start" : column >= 6 ? "end" : "middle";
|
|
81
|
+
const kind = notice.explanation?.kind;
|
|
82
|
+
return (_jsxs("p", { class: `notice ${align} ${where}`, role: "alert", style: { "--column": column, "--row": row }, children: [kind === undefined || notice.piece === undefined ? null : _jsx(ExplainIcon, { kind: kind, piece: notice.piece }), _jsx("span", { children: notice.text })] }, `${notice.square} ${notice.text}`));
|
|
83
|
+
}
|
|
84
|
+
/** The pieces a pawn may become, in a column from its square toward the middle of the board, over a shade that cancels the move; Esc cancels it too. */
|
|
85
|
+
export function PromotionChoice({ side, column, down, onPick, text }) {
|
|
86
|
+
const first = useRef(null);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
first.current?.focus();
|
|
89
|
+
const escape = (event) => {
|
|
90
|
+
if (event.key === "Escape") {
|
|
91
|
+
onPick(undefined);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
addEventListener("keydown", escape);
|
|
95
|
+
return () => removeEventListener("keydown", escape);
|
|
96
|
+
}, []);
|
|
97
|
+
return (_jsxs("div", { class: "promotion", role: "group", "aria-label": text.promoteTo, children: [_jsx("div", { class: "promotion-shade", onPointerDown: () => onPick(undefined) }), promotionPieces.map((piece, index) => (_jsx("button", { ref: index === 0 ? first : undefined, type: "button", class: "promotion-piece", style: { "--column": column, "--row": down ? index : 7 - index }, "aria-label": text.piece(piece.toUpperCase(), side), onClick: () => onPick(piece), children: _jsx(Piece, { letter: side === "white" ? piece.toUpperCase() : piece }) }, piece)))] }));
|
|
98
|
+
}
|
|
99
|
+
/** The piece lifted off the board: it rises a frame after it appears, so it grows into the air. */
|
|
100
|
+
export function Lifted({ drag, letter }) {
|
|
101
|
+
const [up, setUp] = useState(false);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
const frame = requestAnimationFrame(() => setUp(true));
|
|
104
|
+
return () => cancelAnimationFrame(frame);
|
|
105
|
+
}, []);
|
|
106
|
+
const classes = ["lifted", up && !drag.back ? "up" : "", drag.touch ? "touch" : "", drag.back ? "back" : ""].filter((name) => name !== "").join(" ");
|
|
107
|
+
return (_jsx("div", { class: classes, style: { "--x": `${drag.at[0]}px`, "--y": `${drag.at[1]}px` }, children: _jsx(Piece, { letter: letter }) }));
|
|
108
|
+
}
|
|
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
|
+
/** 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
|
+
function ArrowGroup({ shapes, edged }) {
|
|
134
|
+
return (_jsxs("g", { children: [edged
|
|
135
|
+
? shapes.map(({ key, shaft, head }) => (_jsxs("g", { children: [_jsx("path", { class: "edge", d: shaft }), _jsx("polygon", { class: "edge", points: head })] }, `edge${key}`)))
|
|
136
|
+
: null, shapes.map(({ key, colour, shaft, head }) => (_jsxs("g", { class: colour, children: [_jsx("path", { d: shaft }), _jsx("polygon", { points: head })] }, key)))] }));
|
|
137
|
+
}
|
|
138
|
+
/** A point `distance` squares from `point` toward `target`. */
|
|
139
|
+
function towards(point, target, distance) {
|
|
140
|
+
const length = Math.hypot(target[0] - point[0], target[1] - point[1]);
|
|
141
|
+
return [point[0] + (target[0] - point[0]) / length * distance, point[1] + (target[1] - point[1]) / length * distance];
|
|
142
|
+
}
|
|
143
|
+
/** The bubble's icon for a kind of reason: the piece moved, a pin, the player's king on red, a bar, the two ways of castling. */
|
|
144
|
+
function ExplainIcon({ kind, piece }) {
|
|
145
|
+
const white = piece === piece.toUpperCase();
|
|
146
|
+
return (_jsx("span", { class: `explain-icon ${kind}`, "aria-hidden": "true", children: kind === "piece" ? _jsx(Piece, { letter: piece }) : kind === "king" ? _jsx(Piece, { letter: white ? "K" : "k" }) : (_jsx("svg", { viewBox: "0 0 24 24", children: kind === "pin"
|
|
147
|
+
? _jsx("path", { d: "M9 3h6M10 3v6l-3 4h10l-3-4V3M12 13v8" })
|
|
148
|
+
: kind === "blocked"
|
|
149
|
+
? (_jsxs(_Fragment, { children: [_jsx("circle", { cx: "12", cy: "12", r: "8.5" }), _jsx("path", { class: "bar", d: "M7.5 12h9" })] }))
|
|
150
|
+
: _jsx("path", { d: "M4 8h15M15 4l4 4-4 4M20 16H5M9 12l-4 4 4 4" }) })) }));
|
|
151
|
+
}
|
|
152
|
+
const promotionPieces = ["q", "n", "r", "b"];
|
|
153
|
+
// 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
|
+
const explainTrim = { start: 0.36, end: 0.4 };
|
|
155
|
+
// How much a pin's line is cut at each end, in squares.
|
|
156
|
+
const pinTrim = 0.35;
|
|
157
|
+
// An arrow's head, in squares: how long, and how far each side of its line.
|
|
158
|
+
const arrowHead = { length: 0.42, width: 0.3 };
|
|
159
|
+
// The helps' point, smaller in step with their thinner shaft (Stefano).
|
|
160
|
+
const helpsHead = { length: 0.3, width: 0.21 };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { files } from "./squares.js";
|
|
2
|
+
export const noMotion = { slides: new Map() };
|
|
3
|
+
/**
|
|
4
|
+
* The last move slides, but for a piece the player dragged there (`dragged`, its squares), over the piece it takes, as it stood `before`; a castling rook
|
|
5
|
+
* slides always.
|
|
6
|
+
*/
|
|
7
|
+
export function motionOf(move, dragged, shake, before) {
|
|
8
|
+
const slides = new Map();
|
|
9
|
+
let taken;
|
|
10
|
+
if (dragged !== `${move.from}${move.to}`) {
|
|
11
|
+
slides.set(move.to, move.from);
|
|
12
|
+
// A pawn that takes en passant lands beside the pawn it takes.
|
|
13
|
+
const square = before.has(move.to) ? move.to : before.get(move.from)?.toLowerCase() === "p" && move.from[0] !== move.to[0] ? `${move.to[0]}${move.from[1]}` : undefined;
|
|
14
|
+
const letter = square === undefined ? undefined : before.get(square);
|
|
15
|
+
taken = square === undefined || letter === undefined ? undefined : { square, letter };
|
|
16
|
+
}
|
|
17
|
+
const rank = move.from[1];
|
|
18
|
+
if (move.san.startsWith("O-O-O")) {
|
|
19
|
+
slides.set(`d${rank}`, `a${rank}`);
|
|
20
|
+
}
|
|
21
|
+
else if (move.san.startsWith("O-O")) {
|
|
22
|
+
slides.set(`f${rank}`, `h${rank}`);
|
|
23
|
+
}
|
|
24
|
+
return { slides, ...(taken === undefined ? {} : { taken }), ...(shake === undefined ? {} : { shake }) };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A jump through the game, from the pieces `before` to those `now`: each piece slides to the nearest square where one like it stands now, the closest pairs
|
|
28
|
+
* first; a piece left over fades, and one that has no piece to come from appears.
|
|
29
|
+
*/
|
|
30
|
+
export function rearranged(before, now) {
|
|
31
|
+
const left = [...before].filter(([square, letter]) => now.get(square) !== letter);
|
|
32
|
+
const arrived = [...now].filter(([square, letter]) => before.get(square) !== letter);
|
|
33
|
+
const pairs = arrived
|
|
34
|
+
.flatMap(([to, letter]) => left.filter(([, was]) => was === letter).map(([from]) => ({ from, to, distance: distanceOf(from, to) })))
|
|
35
|
+
.sort((one, other) => one.distance - other.distance);
|
|
36
|
+
const slides = new Map();
|
|
37
|
+
const moved = new Set();
|
|
38
|
+
for (const { from, to } of pairs) {
|
|
39
|
+
if (!slides.has(to) && !moved.has(from)) {
|
|
40
|
+
slides.set(to, from);
|
|
41
|
+
moved.add(from);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
slides,
|
|
46
|
+
appear: new Set(arrived.map(([square]) => square).filter((square) => !slides.has(square))),
|
|
47
|
+
gone: left.filter(([square]) => !moved.has(square)).map(([square, letter]) => ({ square, letter })),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** A move taken back: its piece, and a castling rook, slide home; a piece it took is there again under the one leaving. */
|
|
51
|
+
export function backOf(move) {
|
|
52
|
+
const rank = move.from[1];
|
|
53
|
+
const slides = new Map([[move.from, move.to]]);
|
|
54
|
+
if (move.san.startsWith("O-O-O")) {
|
|
55
|
+
slides.set(`a${rank}`, `d${rank}`);
|
|
56
|
+
}
|
|
57
|
+
else if (move.san.startsWith("O-O")) {
|
|
58
|
+
slides.set(`h${rank}`, `f${rank}`);
|
|
59
|
+
}
|
|
60
|
+
return { slides };
|
|
61
|
+
}
|
|
62
|
+
/** Whether the pieces `now` are those `before` with the move played: the piece on its square came from where it began, a pawn there when it promoted. */
|
|
63
|
+
export function playedOn(before, now, move) {
|
|
64
|
+
const piece = before.get(move.from);
|
|
65
|
+
const landed = now.get(move.to);
|
|
66
|
+
return piece !== undefined && !now.has(move.from) && (landed === piece || (move.san.includes("=") && piece.toLowerCase() === "p"));
|
|
67
|
+
}
|
|
68
|
+
/** How far apart two squares are, in squares. */
|
|
69
|
+
function distanceOf(from, to) {
|
|
70
|
+
return Math.hypot(files.indexOf(from[0]) - files.indexOf(to[0]), Number(from[1]) - Number(to[1]));
|
|
71
|
+
}
|