fertig-classic-games/public/src/games/geniussquare/GeniusSquareLogic.js

178 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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));
}