import React, { useMemo, useState } from "react";
/**
* A self-contained chess game in React (no external chess libs).
* Features:
* - Legal move generation (incl. castling, en passant, promotion)
* - Check / checkmate / stalemate detection
* - Click-to-move UI + legal-move highlights
*
* Notes:
* - This is a "rules-correct" implementation for standard chess.
* - 0,0 is a8; 7,7 is h1.
*/
// --- Types ---
/** @typedef {'w'|'b'} Color */
/** @typedef {'p'|'n'|'b'|'r'|'q'|'k'} PieceType */
/** @typedef {{ c: Color, t: PieceType }} Piece */
/** @typedef {Piece | null} Square */
/** @typedef {Square[][]} Board */
/** @typedef {{ r: number, c: number }} Pos */
/** @typedef {{ from: Pos, to: Pos, promo?: PieceType, isCastle?: boolean, isEnPassant?: boolean }} Move */
const FILES = "abcdefgh";
// --- Helpers ---
function inBounds(r, c) {
return r >= 0 && r < 8 && c >= 0 && c < 8;
}
function cloneBoard(board) {
return board.map((row) => row.map((sq) => (sq ? { ...sq } : null)));
}
function posEq(a, b) {
return a.r === b.r && a.c === b.c;
}
function algebraic(pos) {
// r=0 is rank 8
const file = FILES[pos.c];
const rank = 8 - pos.r;
return `${file}${rank}`;
}
function opposite(color) {
return color === "w" ? "b" : "w";
}
function pieceChar(p) {
// Unicode pieces
if (!p) return "";
const map = {
w: { k: "♔", q: "♕", r: "♖", b: "♗", n: "♘", p: "♙" },
b: { k: "♚", q: "♛", r: "♜", b: "♝", n: "♞", p: "♟" },
};
return map[p.c][p.t];
}
function makeInitialBoard() {
/** @type {Board} */
const b = Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => null));
const back = /** @type {PieceType[]} */ (["r", "n", "b", "q", "k", "b", "n", "r"]);
for (let c = 0; c < 8; c++) {
b[0][c] = { c: "b", t: back[c] };
b[1][c] = { c: "b", t: "p" };
b[6][c] = { c: "w", t: "p" };
b[7][c] = { c: "w", t: back[c] };
}
return b;
}
// Game state needed for special moves
function makeInitialState() {
return {
board: makeInitialBoard(),
turn: /** @type {Color} */ ("w"),
// castling rights
castle: {
wK: true,
wQ: true,
bK: true,
bQ: true,
},
// en passant target square (the square a pawn could capture onto)
ep: /** @type {Pos | null} */ (null),
// halfmove and fullmove counters (for display; not 50-move rule enforcement here)
halfmove: 0,
fullmove: 1,
// move log
log: /** @type {string[]} */ ([]),
// game over status
result: /** @type {null | { type: 'checkmate'|'stalemate'|'draw', winner?: Color }} */ (null),
};
}
// --- Move generation ---
function findKing(board, color) {
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const p = board[r][c];
if (p && p.c === color && p.t === "k") return { r, c };
}
}
return null;
}
function isSquareAttacked(board, target, byColor) {
// Generate pseudo-attacks from byColor and see if any hits target.
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const p = board[r][c];
if (!p || p.c !== byColor) continue;
const from = { r, c };
const attacks = pseudoAttacksForPiece(board, from, p);
if (attacks.some((pos) => posEq(pos, target))) return true;
}
}
return false;
}
function pseudoAttacksForPiece(board, from, p) {
/** @type {Pos[]} */
const out = [];
const { r, c } = from;
if (p.t === "p") {
const dir = p.c === "w" ? -1 : 1;
for (const dc of [-1, 1]) {
const rr = r + dir;
const cc = c + dc;
if (inBounds(rr, cc)) out.push({ r: rr, c: cc });
}
return out;
}
if (p.t === "n") {
const deltas = [
[-2, -1],
[-2, 1],
[-1, -2],
[-1, 2],
[1, -2],
[1, 2],
[2, -1],
[2, 1],
];
for (const [dr, dc] of deltas) {
const rr = r + dr;
const cc = c + dc;
if (inBounds(rr, cc)) out.push({ r: rr, c: cc });
}
return out;
}
if (p.t === "k") {
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const rr = r + dr;
const cc = c + dc;
if (inBounds(rr, cc)) out.push({ r: rr, c: cc });
}
}
return out;
}
const sliders = {
b: [
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
],
r: [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
],
q: [
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
],
};
const dirs = sliders[p.t];
if (!dirs) return out;
for (const [dr, dc] of dirs) {
let rr = r + dr;
let cc = c + dc;
while (inBounds(rr, cc)) {
out.push({ r: rr, c: cc });
if (board[rr][cc]) break; // ray blocked
rr += dr;
cc += dc;
}
}
return out;
}
function generatePseudoLegalMoves(state, from) {
const { board, turn, castle, ep } = state;
const p = board[from.r][from.c];
if (!p || p.c !== turn) return [];
/** @type {Move[]} */
const moves = [];
const addMove = (to, extra = {}) => {
moves.push({ from, to, ...extra });
};
const r = from.r;
const c = from.c;
if (p.t === "p") {
const dir = p.c === "w" ? -1 : 1;
const startRank = p.c === "w" ? 6 : 1;
const promoRank = p.c === "w" ? 0 : 7;
// forward 1
const r1 = r + dir;
if (inBounds(r1, c) && !board[r1][c]) {
if (r1 === promoRank) {
for (const promo of /** @type {PieceType[]} */ (["q", "r", "b", "n"])) {
addMove({ r: r1, c }, { promo });
}
} else {
addMove({ r: r1, c });
}
// forward 2 from start
const r2 = r + 2 * dir;
if (r === startRank && inBounds(r2, c) && !board[r2][c]) {
addMove({ r: r2, c });
}
}
// captures
for (const dc of [-1, 1]) {
const rr = r + dir;
const cc = c + dc;
if (!inBounds(rr, cc)) continue;
const target = board[rr][cc];
if (target && target.c !== p.c) {
if (rr === promoRank) {
for (const promo of /** @type {PieceType[]} */ (["q", "r", "b", "n"])) {
addMove({ r: rr, c: cc }, { promo });
}
} else {
addMove({ r: rr, c: cc });
}
}
// en passant
if (ep && ep.r === rr && ep.c === cc) {
addMove({ r: rr, c: cc }, { isEnPassant: true });
}
}
return moves;
}
if (p.t === "n") {
const deltas = [
[-2, -1],
[-2, 1],
[-1, -2],
[-1, 2],
[1, -2],
[1, 2],
[2, -1],
[2, 1],
];
for (const [dr, dc] of deltas) {
const rr = r + dr;
const cc = c + dc;
if (!inBounds(rr, cc)) continue;
const target = board[rr][cc];
if (!target || target.c !== p.c) addMove({ r: rr, c: cc });
}
return moves;
}
if (p.t === "k") {
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const rr = r + dr;
const cc = c + dc;
if (!inBounds(rr, cc)) continue;
const target = board[rr][cc];
if (!target || target.c !== p.c) addMove({ r: rr, c: cc });
}
}
// castling (pseudo; we validate check conditions in legal filtering)
if (p.c === "w" && r === 7 && c === 4) {
// King-side: squares f1,g1 empty and rook h1 present
if (castle.wK && !board[7][5] && !board[7][6]) {
const rook = board[7][7];
if (rook && rook.c === "w" && rook.t === "r") {
addMove({ r: 7, c: 6 }, { isCastle: true });
}
}
// Queen-side: squares d1,c1,b1 empty and rook a1 present
if (castle.wQ && !board[7][3] && !board[7][2] && !board[7][1]) {
const rook = board[7][0];
if (rook && rook.c === "w" && rook.t === "r") {
addMove({ r: 7, c: 2 }, { isCastle: true });
}
}
}
if (p.c === "b" && r === 0 && c === 4) {
if (castle.bK && !board[0][5] && !board[0][6]) {
const rook = board[0][7];
if (rook && rook.c === "b" && rook.t === "r") {
addMove({ r: 0, c: 6 }, { isCastle: true });
}
}
if (castle.bQ && !board[0][3] && !board[0][2] && !board[0][1]) {
const rook = board[0][0];
if (rook && rook.c === "b" && rook.t === "r") {
addMove({ r: 0, c: 2 }, { isCastle: true });
}
}
}
return moves;
}
const dirs =
p.t === "b"
? [
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
]
: p.t === "r"
? [
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
]
: [
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
[-1, 0],
[1, 0],
[0, -1],
[0, 1],
]; // queen
for (const [dr, dc] of dirs) {
let rr = r + dr;
let cc = c + dc;
while (inBounds(rr, cc)) {
const target = board[rr][cc];
if (!target) {
addMove({ r: rr, c: cc });
} else {
if (target.c !== p.c) addMove({ r: rr, c: cc });
break;
}
rr += dr;
cc += dc;
}
}
return moves;
}
function applyMove(state, move) {
const { board, turn } = state;
const b = cloneBoard(board);
const fromPiece = b[move.from.r][move.from.c];
if (!fromPiece) return state;
const captured = b[move.to.r][move.to.c];
// clear en-passant captured pawn
if (move.isEnPassant && fromPiece.t === "p") {
const dir = fromPiece.c === "w" ? 1 : -1; // pawn captured behind target square
b[move.to.r + dir][move.to.c] = null;
}
// move piece
b[move.from.r][move.from.c] = null;
b[move.to.r][move.to.c] = {
c: fromPiece.c,
t: move.promo ? move.promo : fromPiece.t,
};
// castling rook move
if (move.isCastle && fromPiece.t === "k") {
// king ends on g-file (c=6) or c-file (c=2)
if (move.to.c === 6) {
// rook: h -> f
b[move.to.r][5] = b[move.to.r][7];
b[move.to.r][7] = null;
} else if (move.to.c === 2) {
// rook: a -> d
b[move.to.r][3] = b[move.to.r][0];
b[move.to.r][0] = null;
}
}
// update castling rights
const nextCastle = { ...state.castle };
// moving king removes both rights
if (fromPiece.t === "k") {
if (fromPiece.c === "w") {
nextCastle.wK = false;
nextCastle.wQ = false;
} else {
nextCastle.bK = false;
nextCastle.bQ = false;
}
}
// moving rook removes that side
if (fromPiece.t === "r") {
if (fromPiece.c === "w") {
if (move.from.r === 7 && move.from.c === 0) nextCastle.wQ = false;
if (move.from.r === 7 && move.from.c === 7) nextCastle.wK = false;
} else {
if (move.from.r === 0 && move.from.c === 0) nextCastle.bQ = false;
if (move.from.r === 0 && move.from.c === 7) nextCastle.bK = false;
}
}
// capturing rook removes opponent right
if (captured && captured.t === "r") {
if (captured.c === "w") {
if (move.to.r === 7 && move.to.c === 0) nextCastle.wQ = false;
if (move.to.r === 7 && move.to.c === 7) nextCastle.wK = false;
} else {
if (move.to.r === 0 && move.to.c === 0) nextCastle.bQ = false;
if (move.to.r === 0 && move.to.c === 7) nextCastle.bK = false;
}
}
// update en passant target
let nextEp = null;
if (fromPiece.t === "p" && Math.abs(move.to.r - move.from.r) === 2) {
// target is the square jumped over
nextEp = { r: (move.to.r + move.from.r) / 2, c: move.from.c };
}
// halfmove clock
const isPawnMove = fromPiece.t === "p";
const isCapture = !!captured || !!move.isEnPassant;
const nextHalfmove = isPawnMove || isCapture ? 0 : state.halfmove + 1;
const nextTurn = opposite(turn);
const nextFullmove = turn === "b" ? state.fullmove + 1 : state.fullmove;
return {
...state,
board: b,
turn: nextTurn,
castle: nextCastle,
ep: nextEp,
halfmove: nextHalfmove,
fullmove: nextFullmove,
};
}
function isMoveLegal(state, move) {
const p = state.board[move.from.r][move.from.c];
if (!p || p.c !== state.turn) return false;
// castling legality: king can't castle out of, through, or into check
if (move.isCastle && p.t === "k") {
const kingFrom = move.from;
const kingTo = move.to;
const enemy = opposite(p.c);
// if king currently in check: illegal
if (isSquareAttacked(state.board, kingFrom, enemy)) return false;
// squares passed through
const pathSquares = [];
if (kingTo.c === 6) {
// e->f->g
pathSquares.push({ r: kingFrom.r, c: 5 }, { r: kingFrom.r, c: 6 });
} else if (kingTo.c === 2) {
// e->d->c
pathSquares.push({ r: kingFrom.r, c: 3 }, { r: kingFrom.r, c: 2 });
}
for (const sq of pathSquares) {
// simulate king stepping (without rook movement) for attack test
if (isSquareAttacked(state.board, sq, enemy)) return false;
}
}
// general legality: can't leave own king in check
const next = applyMove(state, move);
const kingPos = findKing(next.board, p.c);
if (!kingPos) return false;
return !isSquareAttacked(next.board, kingPos, opposite(p.c));
}
function legalMovesFrom(state, from) {
const pseudo = generatePseudoLegalMoves(state, from);
return pseudo.filter((m) => isMoveLegal(state, m));
}
function allLegalMoves(state) {
/** @type {Move[]} */
const out = [];
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const p = state.board[r][c];
if (!p || p.c !== state.turn) continue;
out.push(...legalMovesFrom(state, { r, c }));
}
}
return out;
}
function detectGameResult(state) {
const moves = allLegalMoves(state);
const kingPos = findKing(state.board, state.turn);
const inCheck = kingPos ? isSquareAttacked(state.board, kingPos, opposite(state.turn)) : false;
if (moves.length > 0) return null;
if (inCheck) {
return { type: "checkmate", winner: opposite(state.turn) };
}
return { type: "stalemate" };
}
// --- Notation (simple SAN-ish) ---
function moveToNotation(prevState, move) {
const board = prevState.board;
const p = board[move.from.r][move.from.c];
if (!p) return "";
// Castling
if (move.isCastle && p.t === "k") {
return move.to.c === 6 ? "O-O" : "O-O-O";
}
const target = board[move.to.r][move.to.c];
const isCapture = !!target || !!move.isEnPassant;
const pieceLetter = p.t === "p" ? "" : p.t.toUpperCase();
const fromFile = FILES[move.from.c];
const toSq = algebraic(move.to);
let s = "";
if (p.t === "p" && isCapture) {
s += fromFile;
} else {
s += pieceLetter;
}
if (isCapture) s += "x";
s += toSq;
if (move.promo) {
s += "=" + move.promo.toUpperCase();
}
// Add check/checkmate suffix
const next = applyMove(prevState, move);
const res = detectGameResult(next);
const enemyKing = findKing(next.board, next.turn);
const givesCheck = enemyKing
? isSquareAttacked(next.board, enemyKing, opposite(next.turn))
: false;
if (res?.type === "checkmate") s += "#";
else if (givesCheck) s += "+";
return s;
}
// --- UI ---
function SquareButton({
piece,
isLight,
isSelected,
isLegalTarget,
isLastMove,
onClick,
label,
}) {
const base = {
width: 56,
height: 56,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 30,
border: "1px solid rgba(0,0,0,0.12)",
cursor: "pointer",
userSelect: "none",
position: "relative",
};
let background = isLight ? "#f0d9b5" : "#b58863";
if (isLastMove) background = isLight ? "#f6f669" : "#d6d64b";
if (isSelected) background = isLight ? "#a9d18e" : "#6aa84f";
return (
<button
onClick={onClick}
style={{
...base,
background,
outline: isSelected ? "2px solid rgba(0,0,0,0.4)" : "none",
}}
aria-label={label}
title={label}
>
<span>{pieceChar(piece)}</span>
{isLegalTarget && (
<span
style={{
position: "absolute",
width: 12,
height: 12,
borderRadius: 999,
background: "rgba(0,0,0,0.35)",
}}
/>
)}
</button>
);
}
function PromotionPicker({ color, onPick }) {
const options = /** @type {PieceType[]} */ (["q", "r", "b", "n"]);
return (
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.35)",
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 16,
}}
>
<div
style={{
background: "white",
borderRadius: 12,
padding: 16,
width: 320,
boxShadow: "0 10px 30px rgba(0,0,0,0.25)",
}}
>
<div style={{ fontSize: 16, fontWeight: 700, marginBottom: 10 }}>
Choose promotion
</div>
<div style={{ display: "flex", gap: 10, justifyContent: "space-between" }}>
{options.map((t) => (
<button
key={t}
onClick={() => onPick(t)}
style={{
flex: 1,
height: 56,
borderRadius: 10,
border: "1px solid rgba(0,0,0,0.15)",
cursor: "pointer",
background: "#fafafa",
fontSize: 26,
}}
aria-label={`Promote to ${t}`}
title={`Promote to ${t.toUpperCase()}`}
>
{pieceChar({ c: color, t })}
</button>
))}
</div>
</div>
</div>
);
}
export default function ChessGame() {
const [state, setState] = useState(makeInitialState);
const [selected, setSelected] = useState(/** @type {Pos | null} */ (null));
const [pendingPromo, setPendingPromo] = useState(
/** @type {null | { baseMove: Move, color: Color }} */ (null)
);
const [lastMove, setLastMove] = useState(/** @type {Move | null} */ (null));
const legalTargets = useMemo(() => {
if (!selected) return [];
return legalMovesFrom(state, selected);
}, [state, selected]);
const kingPos = useMemo(() => findKing(state.board, state.turn), [state.board, state.turn]);
const inCheck = useMemo(() => {
if (!kingPos) return false;
return isSquareAttacked(state.board, kingPos, opposite(state.turn));
}, [state.board, kingPos, state.turn]);
const statusText = useMemo(() => {
if (state.result?.type === "checkmate") {
return `Checkmate — ${state.result.winner === "w" ? "White" : "Black"} wins`;
}
if (state.result?.type === "stalemate") return "Stalemate — draw";
return `${state.turn === "w" ? "White" : "Black"} to move${inCheck ? " (in check)" : ""}`;
}, [state.result, state.turn, inCheck]);
function reset() {
setState(makeInitialState());
setSelected(null);
setPendingPromo(null);
setLastMove(null);
}
function commitMove(move) {
// Handle promotion choice UI
const fromP = state.board[move.from.r][move.from.c];
if (fromP && fromP.t === "p") {
const promoRank = fromP.c === "w" ? 0 : 7;
if (move.to.r === promoRank && !move.promo) {
setPendingPromo({ baseMove: move, color: fromP.c });
return;
}
}
const notation = moveToNotation(state, move);
const next = applyMove(state, move);
const result = detectGameResult(next);
setState({
...next,
log: [...state.log, notation],
result,
});
setLastMove(move);
setSelected(null);
}
function onSquareClick(r, c) {
if (state.result) return;
if (pendingPromo) return;
const p = state.board[r][c];
const clicked = { r, c };
if (!selected) {
if (p && p.c === state.turn) setSelected(clicked);
return;
}
// same square: deselect
if (selected.r === r && selected.c === c) {
setSelected(null);
return;
}
// if selecting another own piece
if (p && p.c === state.turn) {
setSelected(clicked);
return;
}
// try move to clicked square
const m = legalTargets.find((mv) => mv.to.r === r && mv.to.c === c);
if (m) commitMove(m);
else setSelected(null);
}
function onPickPromotion(t) {
if (!pendingPromo) return;
commitMove({ ...pendingPromo.baseMove, promo: t });
setPendingPromo(null);
}
// Display move list as pairs
const movePairs = useMemo(() => {
const pairs = [];
for (let i = 0; i < state.log.length; i += 2) {
pairs.push({
n: 1 + i / 2,
w: state.log[i] || "",
b: state.log[i + 1] || "",
});
}
return pairs;
}, [state.log]);
return (
<div style={{ fontFamily: "system-ui, -apple-system, Segoe UI, Roboto, sans-serif", padding: 16 }}>
<div style={{ maxWidth: 920, margin: "0 auto" }}>
<div
style={{
display: "flex",
gap: 18,
alignItems: "flex-start",
flexWrap: "wrap",
}}
>
<div style={{ position: "relative" }}>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(8, 56px)",
gridTemplateRows: "repeat(8, 56px)",
borderRadius: 12,
overflow: "hidden",
boxShadow: "0 10px 30px rgba(0,0,0,0.12)",
}}
>
{state.board.map((row, r) =>
row.map((sq, c) => {
const isLight = (r + c) % 2 === 0;
const isSelected = !!selected && selected.r === r && selected.c === c;
const isLegalTarget = legalTargets.some((m) => m.to.r === r && m.to.c === c);
const isLastMove =
!!lastMove &&
((lastMove.from.r === r && lastMove.from.c === c) ||
(lastMove.to.r === r && lastMove.to.c === c));
const label = `${algebraic({ r, c })}${sq ? ` ${sq.c}${sq.t}` : ""}`;
return (
<SquareButton
key={`${r}-${c}`}
piece={sq}
isLight={isLight}
isSelected={isSelected}
isLegalTarget={isLegalTarget}
isLastMove={isLastMove}
onClick={() => onSquareClick(r, c)}
label={label}
/>
);
})
)}
</div>
{pendingPromo && <PromotionPicker color={pendingPromo.color} onPick={onPickPromotion} />}
</div>
<div
style={{
width: 320,
flex: "0 0 320px",
border: "1px solid rgba(0,0,0,0.12)",
borderRadius: 12,
padding: 14,
boxShadow: "0 10px 30px rgba(0,0,0,0.06)",
}}
>
<div style={{ fontSize: 18, fontWeight: 800, marginBottom: 8 }}>Chess</div>
<div style={{ fontSize: 14, marginBottom: 12, opacity: 0.85 }}>{statusText}</div>
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
<button
onClick={reset}
style={{
padding: "10px 12px",
borderRadius: 10,
border: "1px solid rgba(0,0,0,0.15)",
background: "#fafafa",
cursor: "pointer",
fontWeight: 700,
}}
>
New game
</button>
<button
onClick={() => setSelected(null)}
style={{
padding: "10px 12px",
borderRadius: 10,
border: "1px solid rgba(0,0,0,0.15)",
background: "#fff",
cursor: "pointer",
fontWeight: 700,
}}
disabled={!selected}
title={selected ? "Clear selection" : ""}
>
Deselect
</button>
</div>
<div style={{ fontSize: 13, fontWeight: 800, marginBottom: 6 }}>Moves</div>
<div
style={{
maxHeight: 340,
overflow: "auto",
borderRadius: 10,
border: "1px solid rgba(0,0,0,0.12)",
}}
>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
<thead>
<tr style={{ background: "rgba(0,0,0,0.03)" }}>
<th style={{ textAlign: "left", padding: 8, width: 40 }}>#</th>
<th style={{ textAlign: "left", padding: 8 }}>White</th>
<th style={{ textAlign: "left", padding: 8 }}>Black</th>
</tr>
</thead>
<tbody>
{movePairs.length === 0 ? (
<tr>
<td colSpan={3} style={{ padding: 10, opacity: 0.7 }}>
No moves yet.
</td>
</tr>
) : (
movePairs.map((p) => (
<tr key={p.n}>
<td style={{ padding: 8, borderTop: "1px solid rgba(0,0,0,0.08)", opacity: 0.7 }}>
{p.n}.
</td>
<td style={{ padding: 8, borderTop: "1px solid rgba(0,0,0,0.08)" }}>{p.w}</td>
<td style={{ padding: 8, borderTop: "1px solid rgba(0,0,0,0.08)" }}>{p.b}</td>
</tr>
))
)}
</tbody>
</table>
</div>
<div style={{ marginTop: 12, fontSize: 12, opacity: 0.75, lineHeight: 1.35 }}>
Tip: click a piece to see legal moves. Click a highlighted square to move.
Promotion pops up automatically.
</div>
</div>
</div>
</div>
</div>
);
}