/* 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 (
{Array.from({ length: 64 }).map((_, i) => { const row = Math.floor(i / 8); const col = i % 8; const light = (row + col) % 2 === 0; const name = squareName(col, row); const piece = position[i]; const isSelected = name === selected; const isHighlightFrom = name === highlightFrom; const isHighlightTo = name === highlightTo; const move = candidateMoves.find( (m) => (m.endsWith('!') ? m.slice(0, -1) : m) === name ); const isCapture = move && move.endsWith('!'); const isMove = !!move; const isHinted = selectable && selectable.has(name); const handleClick = onSquareClick ? () => onSquareClick(name) : undefined; return (
{/* From / to last-move tint */} {(isHighlightFrom || isHighlightTo) && (
)} {/* Hinted (clickable cue) outline */} {isHinted && !isSelected && (
)} {/* Selected outline */} {isSelected && (
)} {/* Piece */} {piece && (
)} {/* Candidate move markers — rendered ABOVE pieces */} {isMove && !isCapture && (
)} {isMove && isCapture && (
)} {/* Rank / file coordinates */} {showCoords && col === 0 && ( {8 - row} )} {showCoords && row === 7 && ( {'abcdefgh'[col]} )}
); })}
); } Object.assign(window, { Chessboard, PieceImage, makePosition, sqToIndex, indexToSq, pieceSrc, STARTING_POSITION, FORK_POSITION, FORK_E5_KNIGHT_MOVES, FORK_SOLUTION, });