{
  "name": "chessboard",
  "title": "ChessBoard",
  "type": "registry:component",
  "description": "A chess board with a full set of turned pieces, set from a FEN position or played out from the seed into a mid-game, on a table top or its own pedestal.",
  "dependencies": [
    "@react-three/rapier@^2.2.0",
    "@runek/core@^0.15.0",
    "three@^0.184.0"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "ChessBoard.tsx",
      "content": "import { CuboidCollider, RigidBody } from '@react-three/rapier'\nimport { int, rng, sub, useWorld, type WorldComponentProps } from '@runek/core'\nimport { useLayoutEffect, useMemo, useRef } from 'react'\nimport {\n  BoxGeometry,\n  type BufferGeometry,\n  type InstancedMesh,\n  LatheGeometry,\n  Matrix4,\n  Quaternion,\n  Vector2,\n  Vector3,\n} from 'three'\n\nexport interface ChessBoardProps extends WorldComponentProps {\n  /** A position in Forsyth-Edwards Notation (the placement field is enough), or `start`.\n   *  Unset, the seed plays out a mid-game: some pieces taken, pawns pushed, pieces developed. */\n  fen?: string\n  /** Board width across its frame, in units. */\n  size?: number\n  /** Stand it on its own pedestal table; otherwise it lies on whatever top it is placed on. */\n  table?: boolean\n  /** The light squares. */\n  lightSquare?: string\n  /** The dark squares (a1 is dark). */\n  darkSquare?: string\n  /** The frame and the pedestal; defaults to the palette's `wood`. */\n  frameColor?: string\n  /** White's pieces. */\n  whiteColor?: string\n  /** Black's pieces. */\n  blackColor?: string\n}\n\ntype PieceType = 'p' | 'n' | 'b' | 'r' | 'q' | 'k'\n\nexport interface ChessPiece {\n  type: PieceType\n  white: boolean\n  file: number\n  rank: number\n}\n\nconst START = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'\nconst TYPES: PieceType[] = ['p', 'n', 'b', 'r', 'q', 'k']\n\n/** The placement field of a FEN, or null when it does not describe an 8 by 8 board. */\nfunction parseFen(fen: string): ChessPiece[] | null {\n  const rows = fen.trim().split(/\\s+/)[0]?.split('/') ?? []\n  if (rows.length !== 8) return null\n  const pieces: ChessPiece[] = []\n  for (let i = 0; i < 8; i++) {\n    let file = 0\n    for (const ch of rows[i]) {\n      if (/[1-8]/.test(ch)) {\n        file += Number(ch)\n        continue\n      }\n      const type = ch.toLowerCase() as PieceType\n      if (!TYPES.includes(type) || file > 7) return null\n      pieces.push({ type, white: ch !== type, file, rank: 7 - i })\n      file++\n    }\n    if (file !== 8) return null\n  }\n  return pieces\n}\n\n/** A plausible middle game from a seed: captures, pushed pawns, developed pieces. */\nfunction midgame(seed: number): ChessPiece[] {\n  const r = rng(sub(seed, 0))\n  const pieces = parseFen(START) ?? []\n  const taken = int(r, 4, 14)\n  for (let i = 0; i < taken; i++) {\n    const options = pieces.filter((p) => p.type !== 'k')\n    pieces.splice(pieces.indexOf(options[Math.floor(r() * options.length)]), 1)\n  }\n  const free = (file: number, rank: number) =>\n    !pieces.some((p) => p.file === file && p.rank === rank)\n  for (const p of pieces) {\n    if (p.type === 'p') {\n      const dir = p.white ? 1 : -1\n      const steps = r() < 0.6 ? int(r, 1, 3) : 0\n      for (let s = 0; s < steps && free(p.file, p.rank + dir); s++) p.rank += dir\n    } else if (p.type !== 'k' && r() < 0.55) {\n      const file = int(r, 0, 7)\n      const rank = p.white ? int(r, 1, 4) : int(r, 3, 6)\n      if (free(file, rank)) Object.assign(p, { file, rank })\n    }\n  }\n  return pieces\n}\n\n/** The pieces on the board: from `fen` (`start`, or a placement the board falls back to the\n *  start from when it cannot read), or a seeded mid-game when `fen` is unset. Files and ranks\n *  count from 0 (a1 is file 0, rank 0). */\nexport function chessPieces(fen: string | undefined, seed: number): ChessPiece[] {\n  if (!fen) return midgame(seed)\n  return parseFen(fen === 'start' ? START : fen) ?? parseFen(START) ?? []\n}\n\n// Turned profiles in square widths, (radius, height), foot to crown.\nconst PROFILES: Record<PieceType, [number, number][]> = {\n  p: [\n    [0, 0],\n    [0.32, 0],\n    [0.32, 0.08],\n    [0.24, 0.14],\n    [0.16, 0.2],\n    [0.11, 0.5],\n    [0.18, 0.56],\n    [0.1, 0.6],\n    [0.16, 0.7],\n    [0.17, 0.8],\n    [0.12, 0.92],\n    [0, 0.95],\n  ],\n  r: [\n    [0, 0],\n    [0.36, 0],\n    [0.36, 0.08],\n    [0.28, 0.16],\n    [0.22, 0.25],\n    [0.2, 0.75],\n    [0.27, 0.8],\n    [0.27, 1.0],\n    [0.2, 1.0],\n    [0.2, 0.94],\n    [0, 0.94],\n  ],\n  b: [\n    [0, 0],\n    [0.35, 0],\n    [0.35, 0.08],\n    [0.26, 0.15],\n    [0.15, 0.25],\n    [0.11, 0.75],\n    [0.2, 0.8],\n    [0.1, 0.86],\n    [0.16, 0.98],\n    [0.14, 1.12],\n    [0.05, 1.22],\n    [0.06, 1.26],\n    [0, 1.3],\n  ],\n  q: [\n    [0, 0],\n    [0.37, 0],\n    [0.37, 0.08],\n    [0.28, 0.16],\n    [0.16, 0.28],\n    [0.12, 0.95],\n    [0.22, 1.0],\n    [0.12, 1.06],\n    [0.2, 1.32],\n    [0.12, 1.36],\n    [0.07, 1.4],\n    [0.07, 1.44],\n    [0, 1.48],\n  ],\n  k: [\n    [0, 0],\n    [0.38, 0],\n    [0.38, 0.08],\n    [0.29, 0.16],\n    [0.17, 0.3],\n    [0.13, 1.0],\n    [0.23, 1.05],\n    [0.13, 1.1],\n    [0.2, 1.4],\n    [0.1, 1.46],\n    [0, 1.48],\n  ],\n  n: [\n    [0, 0],\n    [0.35, 0],\n    [0.35, 0.08],\n    [0.26, 0.16],\n    [0.22, 0.3],\n    [0, 0.3],\n  ],\n}\n\ninterface Part {\n  geometry: BufferGeometry\n  local: Matrix4\n}\n\nconst box = (w: number, h: number, d: number, pos: Vector3, tilt = 0) => ({\n  geometry: new BoxGeometry(w, h, d),\n  local: new Matrix4().compose(\n    pos,\n    new Quaternion().setFromAxisAngle(new Vector3(1, 0, 0), tilt),\n    new Vector3(1, 1, 1),\n  ),\n})\n\n/** Each piece as one or more parts in square widths: a turned body, plus the knight's head and\n *  the king's cross. The knight faces local +Z. */\nfunction pieceParts(): Record<PieceType, Part[]> {\n  const out = {} as Record<PieceType, Part[]>\n  for (const t of TYPES) {\n    out[t] = [\n      {\n        geometry: new LatheGeometry(\n          PROFILES[t].map(([x, y]) => new Vector2(x, y)),\n          t === 'r' ? 12 : 18,\n        ),\n        local: new Matrix4(),\n      },\n    ]\n  }\n  out.k.push(\n    box(0.07, 0.24, 0.07, new Vector3(0, 1.58, 0)),\n    box(0.2, 0.07, 0.07, new Vector3(0, 1.6, 0)),\n  )\n  out.n.push(\n    box(0.26, 0.6, 0.3, new Vector3(0, 0.58, -0.04), -0.2),\n    box(0.22, 0.2, 0.44, new Vector3(0, 0.88, 0.08), 0.35),\n    box(0.16, 0.12, 0.08, new Vector3(0, 1.0, -0.1)),\n  )\n  return out\n}\n\nfunction Instanced({\n  geometry,\n  matrices,\n  color,\n}: {\n  geometry: BufferGeometry\n  matrices: Matrix4[]\n  color: string\n}) {\n  const ref = useRef<InstancedMesh>(null)\n  useLayoutEffect(() => {\n    const mesh = ref.current\n    if (!mesh) return\n    matrices.forEach((m, i) => {\n      mesh.setMatrixAt(i, m)\n    })\n    mesh.instanceMatrix.needsUpdate = true\n  }, [matrices])\n  return (\n    <instancedMesh\n      key={matrices.length}\n      ref={ref}\n      args={[geometry, undefined, matrices.length]}\n      castShadow\n      receiveShadow\n    >\n      <meshStandardMaterial color={color} roughness={0.35} />\n    </instancedMesh>\n  )\n}\n\nconst UP = new Vector3(0, 1, 0)\n\n/**\n * A chess board with a full set of turned pieces, set from a position in FEN or played out from\n * the seed into a mid-game. Files run along local X (a to h) and white sits at local +Z. It lies\n * on whatever it is placed on (its origin is the board's underside), or stands on its own\n * pedestal table with `table`. Squares and pieces are instanced: a few draw calls for the set.\n * With a table it has one cuboid collider; a bare board has none.\n */\nexport function ChessBoard({\n  position = [0, 0, 0],\n  rotation = [0, 0, 0],\n  seed = 1,\n  fen,\n  size = 0.45,\n  table = false,\n  lightSquare = '#e8d7b0',\n  darkSquare = '#6b4a2b',\n  frameColor,\n  whiteColor = '#efe6d2',\n  blackColor = '#2a2522',\n}: ChessBoardProps) {\n  const { unit, palette } = useWorld()\n  const wood = frameColor ?? palette.wood\n  const B = size * unit\n  const sq = (B * 0.9) / 8\n  const thick = 0.022 * unit\n  const tableH = 0.72 * unit\n  const base = table ? tableH : 0\n\n  const pieces = useMemo(() => chessPieces(fen, seed), [fen, seed])\n\n  const parts = useMemo(pieceParts, [])\n  const squareGeometry = useMemo(() => new BoxGeometry(sq, 0.001 * unit, sq), [sq, unit])\n\n  const squares = useMemo(() => {\n    const light: Matrix4[] = []\n    const dark: Matrix4[] = []\n    for (let f = 0; f < 8; f++) {\n      for (let r = 0; r < 8; r++) {\n        const m = new Matrix4().makeTranslation((f - 3.5) * sq, 0, (3.5 - r) * sq)\n        // a1 is a dark square\n        ;((f + r) % 2 === 0 ? dark : light).push(m)\n      }\n    }\n    return { light, dark }\n  }, [sq])\n\n  const sets = useMemo(() => {\n    const out: { key: string; geometry: BufferGeometry; matrices: Matrix4[]; color: string }[] = []\n    const scale = new Vector3(sq, sq, sq)\n    for (const t of TYPES) {\n      for (const white of [true, false]) {\n        const mine = pieces.filter((p) => p.type === t && p.white === white)\n        if (!mine.length) continue\n        const placed = mine.map((p) =>\n          new Matrix4().compose(\n            new Vector3((p.file - 3.5) * sq, 0, (3.5 - p.rank) * sq),\n            new Quaternion().setFromAxisAngle(UP, white ? Math.PI : 0),\n            scale,\n          ),\n        )\n        parts[t].forEach((part, i) => {\n          out.push({\n            key: `${t}${white ? 'w' : 'b'}${i}`,\n            geometry: part.geometry,\n            matrices: placed.map((m) => m.clone().multiply(part.local)),\n            color: white ? whiteColor : blackColor,\n          })\n        })\n      }\n    }\n    return out\n  }, [pieces, parts, sq, whiteColor, blackColor])\n\n  const board = (\n    <group position={[0, base, 0]}>\n      <mesh position={[0, thick / 2, 0]} castShadow receiveShadow>\n        <boxGeometry args={[B, thick, B]} />\n        <meshStandardMaterial color={wood} roughness={0.5} />\n      </mesh>\n      <group position={[0, thick + 0.0005 * unit, 0]}>\n        {[\n          { list: squares.light, color: lightSquare },\n          { list: squares.dark, color: darkSquare },\n        ].map(({ list, color }) => (\n          <Instanced key={color} geometry={squareGeometry} matrices={list} color={color} />\n        ))}\n      </group>\n      <group position={[0, thick + 0.001 * unit, 0]}>\n        {sets.map((s) => (\n          <Instanced key={s.key} geometry={s.geometry} matrices={s.matrices} color={s.color} />\n        ))}\n      </group>\n    </group>\n  )\n\n  if (!table) {\n    return (\n      <group position={position} rotation={rotation}>\n        {board}\n      </group>\n    )\n  }\n\n  return (\n    <RigidBody type=\"fixed\" colliders={false} position={position} rotation={rotation}>\n      <CuboidCollider args={[B * 0.6, tableH / 2, B * 0.6]} position={[0, tableH / 2, 0]} />\n      <mesh position={[0, tableH - 0.015 * unit, 0]} castShadow receiveShadow>\n        <boxGeometry args={[B * 1.2, 0.03 * unit, B * 1.2]} />\n        <meshStandardMaterial color={wood} roughness={0.55} />\n      </mesh>\n      <mesh position={[0, tableH / 2, 0]} castShadow>\n        <cylinderGeometry args={[0.04 * unit, 0.05 * unit, tableH - 0.03 * unit, 14]} />\n        <meshStandardMaterial color={wood} roughness={0.55} />\n      </mesh>\n      {[0, 1, 2].map((i) => (\n        <mesh\n          key={i}\n          position={[0, 0.03 * unit, 0]}\n          rotation={[0, (i * Math.PI * 2) / 3, 0]}\n          castShadow\n        >\n          <boxGeometry args={[0.05 * unit, 0.05 * unit, B * 0.85]} />\n          <meshStandardMaterial color={wood} roughness={0.55} />\n        </mesh>\n      ))}\n      {board}\n    </RigidBody>\n  )\n}\n",
      "type": "registry:component"
    }
  ]
}
