178 lines
6.5 KiB
JavaScript
178 lines
6.5 KiB
JavaScript
// Genius Square — pure game logic, no Phaser.
|
||
//
|
||
// Board is a 36-element flat array (6×6, row-major):
|
||
// null = empty
|
||
// 'blocked' = die/blocker occupying this cell
|
||
// pieceId = placed piece id
|
||
//
|
||
// The 9 pieces total 29 cells; 7 blockers = 36 = 6×6.
|
||
|
||
// ── Piece definitions ─────────────────────────────────────────────────────────
|
||
|
||
export const PIECES = [
|
||
{ id: 'dot', cells: [[0,0]] },
|
||
{ id: 'domino', cells: [[0,0],[0,1]] },
|
||
{ id: 'itromino', cells: [[0,0],[0,1],[0,2]] },
|
||
{ id: 'ltromino', cells: [[0,0],[1,0],[1,1]] },
|
||
{ id: 'square', cells: [[0,0],[0,1],[1,0],[1,1]] },
|
||
{ id: 'ltetromino', cells: [[0,0],[1,0],[2,0],[2,1]] },
|
||
{ id: 'stetromino', cells: [[0,1],[0,2],[1,0],[1,1]] },
|
||
{ id: 'ttetromino', cells: [[0,0],[0,1],[0,2],[1,1]] },
|
||
{ id: 'itetromino', cells: [[0,0],[0,1],[0,2],[0,3]] },
|
||
];
|
||
|
||
export const PIECE_IDS = PIECES.map(p => p.id);
|
||
|
||
// ── Orientation generation ────────────────────────────────────────────────────
|
||
// Enumerate all unique free-polyomino orientations (rotations + reflections)
|
||
// using the same normalize/deduplicate approach as BlokusBoard.js.
|
||
|
||
function normalize(cells) {
|
||
let minR = Infinity, minC = Infinity;
|
||
for (const [r, c] of cells) { if (r < minR) minR = r; if (c < minC) minC = c; }
|
||
return cells.map(([r, c]) => [r - minR, c - minC]).sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||
}
|
||
|
||
function keyOf(cells) {
|
||
return cells.map(([r, c]) => `${r},${c}`).join(';');
|
||
}
|
||
|
||
function computeOrientations(cells) {
|
||
const seen = new Map();
|
||
const base = cells.map(([r, c]) => [r, c]);
|
||
for (let flip = 0; flip < 2; flip++) {
|
||
let work = flip ? base.map(([r, c]) => [r, -c]) : base;
|
||
for (let rot = 0; rot < 4; rot++) {
|
||
const norm = normalize(work);
|
||
const k = keyOf(norm);
|
||
if (!seen.has(k)) seen.set(k, norm);
|
||
work = work.map(([r, c]) => [c, -r]); // rotate 90° clockwise
|
||
}
|
||
}
|
||
return [...seen.values()];
|
||
}
|
||
|
||
export const ORIENTATIONS = Object.fromEntries(
|
||
PIECES.map(p => [p.id, computeOrientations(p.cells)])
|
||
);
|
||
|
||
const ORI_INDEX = Object.fromEntries(
|
||
PIECES.map(p => [p.id, new Map(ORIENTATIONS[p.id].map((o, i) => [keyOf(o), i]))])
|
||
);
|
||
|
||
function transformedIndex(pieceId, oriIdx, fn) {
|
||
const cur = ORIENTATIONS[pieceId][oriIdx];
|
||
const next = normalize(cur.map(fn));
|
||
return ORI_INDEX[pieceId].get(keyOf(next)) ?? oriIdx;
|
||
}
|
||
|
||
/** Orientation index after a 90° clockwise rotation. */
|
||
export function rotateOri(pieceId, oriIdx) {
|
||
return transformedIndex(pieceId, oriIdx, ([r, c]) => [c, -r]);
|
||
}
|
||
|
||
/** Orientation index after a horizontal mirror. */
|
||
export function flipOri(pieceId, oriIdx) {
|
||
return transformedIndex(pieceId, oriIdx, ([r, c]) => [r, -c]);
|
||
}
|
||
|
||
// ── Dice zones ────────────────────────────────────────────────────────────────
|
||
// 7 zones partition all 36 cells. One cell picked randomly from each zone
|
||
// per round, ensuring blockers spread across the entire board.
|
||
|
||
const ZONES = [
|
||
[[0,0],[0,1],[0,2],[0,3],[1,3]],
|
||
[[0,4],[0,5],[1,4],[1,5],[2,5]],
|
||
[[1,0],[1,1],[1,2],[2,0],[2,1]],
|
||
[[2,2],[2,3],[2,4],[3,2],[3,3]],
|
||
[[3,0],[3,1],[4,0],[4,1],[5,0]],
|
||
[[3,4],[3,5],[4,4],[4,5],[5,5]],
|
||
[[4,2],[4,3],[5,1],[5,2],[5,3],[5,4]],
|
||
];
|
||
|
||
/** Roll 7 dice → array of 7 [row, col] blocker positions. */
|
||
export function rollDice() {
|
||
return ZONES.map(zone => zone[Math.floor(Math.random() * zone.length)]);
|
||
}
|
||
|
||
// ── Board helpers ─────────────────────────────────────────────────────────────
|
||
|
||
export function newBoard(blockers) {
|
||
const board = Array(36).fill(null);
|
||
for (const [r, c] of blockers) board[r * 6 + c] = 'blocked';
|
||
return board;
|
||
}
|
||
|
||
/** Translate orientation-relative [dr,dc] cells to absolute [r,c] at an anchor. */
|
||
export function absoluteCells(oriCells, anchorR, anchorC) {
|
||
return oriCells.map(([dr, dc]) => [anchorR + dr, anchorC + dc]);
|
||
}
|
||
|
||
export function canPlace(board, cells) {
|
||
for (const [r, c] of cells) {
|
||
if (r < 0 || r >= 6 || c < 0 || c >= 6) return false;
|
||
if (board[r * 6 + c] !== null) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
export function placePiece(board, cells, pieceId) {
|
||
const next = board.slice();
|
||
for (const [r, c] of cells) next[r * 6 + c] = pieceId;
|
||
return next;
|
||
}
|
||
|
||
export function removePiece(board, cells) {
|
||
const next = board.slice();
|
||
for (const [r, c] of cells) next[r * 6 + c] = null;
|
||
return next;
|
||
}
|
||
|
||
export function isSolved(board) {
|
||
return board.every(cell => cell !== null);
|
||
}
|
||
|
||
// ── Backtracking solver ───────────────────────────────────────────────────────
|
||
// Always targets the first null cell (left-to-right, top-to-bottom). For each
|
||
// remaining piece, tries every orientation offset that covers that cell, then
|
||
// recurses. This forces row-major filling and collapses the search space
|
||
// dramatically — runs in <10ms for typical Genius Square boards.
|
||
|
||
export function solve(board, remainingPieces) {
|
||
const emptyIdx = board.indexOf(null);
|
||
if (emptyIdx === -1) return remainingPieces.size === 0 ? [] : null;
|
||
if (remainingPieces.size === 0) return null;
|
||
|
||
const targetR = Math.floor(emptyIdx / 6);
|
||
const targetC = emptyIdx % 6;
|
||
|
||
for (const pieceId of remainingPieces) {
|
||
const oris = ORIENTATIONS[pieceId];
|
||
for (let oriIdx = 0; oriIdx < oris.length; oriIdx++) {
|
||
const oriCells = oris[oriIdx];
|
||
for (const [dr, dc] of oriCells) {
|
||
const anchorR = targetR - dr;
|
||
const anchorC = targetC - dc;
|
||
const cells = absoluteCells(oriCells, anchorR, anchorC);
|
||
if (!canPlace(board, cells)) continue;
|
||
|
||
const nextBoard = placePiece(board, cells, pieceId);
|
||
const nextRemaining = new Set(remainingPieces);
|
||
nextRemaining.delete(pieceId);
|
||
|
||
const sub = solve(nextBoard, nextRemaining);
|
||
if (sub !== null) {
|
||
return [{ pieceId, oriIdx, anchorR, anchorC, cells }, ...sub];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** Convenience: build fresh board from blockers and solve with all 9 pieces. */
|
||
export function solveFromBlockers(blockers) {
|
||
const board = newBoard(blockers);
|
||
return solve(board, new Set(PIECE_IDS));
|
||
}
|