/* global React */
// =====================================================================
// Scroll Gambit chess module — dynamic board built from the user's
// piece sprites. One coordinate system (a8 top-left, h1 bottom-right).
// =====================================================================
// ---- Piece sprite map ----
const PIECE_SRC = {
wP: 'assets/piece-w-pawn.png',
wN: 'assets/piece-w-knight.png',
wB: 'assets/piece-w-bishop.png',
wR: 'assets/piece-w-rook.png',
wQ: 'assets/piece-w-queen.png',
wK: 'assets/piece-w-king.png',
bP: 'assets/piece-b-pawn.png',
bN: 'assets/piece-b-knight.png',
bB: 'assets/piece-b-bishop.png',
bR: 'assets/piece-b-rook.png',
bQ: 'assets/piece-b-queen.png',
bK: 'assets/piece-b-king.png',
};
function pieceSrc(code) {
// code like 'wK' or 'bN'
if (typeof window !== 'undefined' && window.__resources) {
const key = 'piece' + code; // e.g. piecewK
if (window.__resources[key]) return window.__resources[key];
}
return PIECE_SRC[code];
}
function PieceImage({ kind, size = 64, style = {} }) {
// kind can be 'wN' or just 'N' (defaults to white).
const code = kind.length === 1 ? 'w' + kind : kind;
const src = pieceSrc(code);
if (!src) return null;
return (
);
}
// ---- Position helpers ----
function makePosition(map) {
// map: { 'e4': 'wP', 'd5': 'bN', ... }
const out = Array(64).fill('');
for (const [sq, p] of Object.entries(map)) {
const file = sq.charCodeAt(0) - 97;
const rank = parseInt(sq[1], 10);
out[(8 - rank) * 8 + file] = p;
}
return out;
}
function sqToIndex(sq) {
const file = sq.charCodeAt(0) - 97;
const rank = parseInt(sq[1], 10);
return (8 - rank) * 8 + file;
}
function indexToSq(i) {
const file = i % 8;
const rank = 8 - Math.floor(i / 8);
return 'abcdefgh'[file] + rank;
}
// ---- Positions ----
const STARTING_POSITION = makePosition({
a8: 'bR', b8: 'bN', c8: 'bB', d8: 'bQ', e8: 'bK', f8: 'bB', g8: 'bN', h8: 'bR',
a7: 'bP', b7: 'bP', c7: 'bP', d7: 'bP', e7: 'bP', f7: 'bP', g7: 'bP', h7: 'bP',
a2: 'wP', b2: 'wP', c2: 'wP', d2: 'wP', e2: 'wP', f2: 'wP', g2: 'wP', h2: 'wP',
a1: 'wR', b1: 'wN', c1: 'wB', d1: 'wQ', e1: 'wK', f1: 'wB', g1: 'wN', h1: 'wR',
});
// "Find the fork" — white to move, Nxf7! (knight forks Q on d8 and R on f8)
const FORK_POSITION = makePosition({
a8: 'bR', d8: 'bQ', f8: 'bR', g8: 'bK', c8: 'bB',
e7: 'bB', f6: 'bN', c6: 'bN',
a7: 'bP', b7: 'bP', c7: 'bP', d7: 'bP', e6: 'bP', f7: 'bP', g7: 'bP', h7: 'bP',
c4: 'wB', e5: 'wN', c3: 'wN',
a2: 'wP', b2: 'wP', c2: 'wP', d4: 'wP', e4: 'wP', f2: 'wP', g2: 'wP', h2: 'wP',
a1: 'wR', c1: 'wB', d1: 'wQ', f1: 'wR', g1: 'wK',
});
// Knight on e5 can go to: c4(own bishop), d3, c6(capture), d7(capture),
// f7(WINNING capture), g6, g4, f3. Filter own-side blocks → 7 legal moves.
const FORK_E5_KNIGHT_MOVES = ['d3', 'c6!', 'd7!', 'f7!', 'g6', 'g4', 'f3'];
const FORK_SOLUTION = { from: 'e5', to: 'f7' };
// =====================================================================
// Chessboard — branded ivory + electric-blue squares, piece sprites on
// top. Optional click hotspots and move/highlight overlays.
// =====================================================================
function Chessboard({
position,
size = 360,
onSquareClick,
selected,
candidateMoves = [],
highlightFrom,
highlightTo,
selectable, // optional set of square names that should look clickable
showCoords = true,
}) {
const sq = size / 8;
function squareName(file, row) {
// row 0 = top = rank 8
const rank = 8 - row;
return 'abcdefgh'[file] + rank;
}
return (