1import type {2 PlacedRectangle,3 PuzzleGiven,4 RectanglePlacement,5 ShikakuPuzzle,6} from "@/lib/shikaku/types.ts";78export function createRectangleFromCorners(9 start: { row: number; col: number },10 end: { row: number; col: number },11): RectanglePlacement {12 const row = Math.min(start.row, end.row);13 const col = Math.min(start.col, end.col);14 const height = Math.abs(end.row - start.row) + 1;15 const width = Math.abs(end.col - start.col) + 1;1617 return { row, col, width, height };18}1920export function getRectangleCells(rectangle: RectanglePlacement): string[] {21 const cells: string[] = [];2223 for (let row = rectangle.row; row < rectangle.row + rectangle.height; row++) {24 for (25 let col = rectangle.col;26 col < rectangle.col + rectangle.width;27 col++28 ) {29 cells.push(`${row},${col}`);30 }31 }3233 return cells;34}3536export function canonicalizeRectangle(rectangle: PlacedRectangle): string {37 return [38 rectangle.clueId,39 rectangle.row,40 rectangle.col,41 rectangle.width,42 rectangle.height,43 rectangle.value,44 ].join(":");45}4647export function validateRectanglePlacement(48 puzzle: ShikakuPuzzle,49 candidate: RectanglePlacement,50 existing: PlacedRectangle[],51):52 | { ok: true; given: PuzzleGiven }53 | {54 ok: false;55 reason:56 | "out-of-bounds"57 | "wrong-area"58 | "missing-given"59 | "multiple-givens"60 | "overlap";61 } {62 if (63 candidate.row < 0 || candidate.col < 0 ||64 candidate.row + candidate.height > puzzle.height ||65 candidate.col + candidate.width > puzzle.width66 ) {67 return { ok: false, reason: "out-of-bounds" };68 }6970 const area = candidate.width * candidate.height;71 const cells = new Set(getRectangleCells(candidate));72 const givens = puzzle.givens.filter((given) =>73 cells.has(`${given.row},${given.col}`)74 );7576 if (givens.length === 0) {77 return { ok: false, reason: "missing-given" };78 }7980 if (givens.length > 1) {81 return { ok: false, reason: "multiple-givens" };82 }8384 if (givens[0].value !== area) {85 return { ok: false, reason: "wrong-area" };86 }8788 const occupied = new Set(89 existing.flatMap((rectangle) => getRectangleCells(rectangle)),90 );91 for (const cell of cells) {92 if (occupied.has(cell)) {93 return { ok: false, reason: "overlap" };94 }95 }9697 return { ok: true, given: givens[0] };98}99100export function isPuzzleSolved(101 puzzle: ShikakuPuzzle,102 placed: PlacedRectangle[],103): boolean {104 if (placed.length !== puzzle.givens.length) return false;105106 const covered = new Set<string>();107 const accepted: PlacedRectangle[] = [];108109 for (const rectangle of placed) {110 const validation = validateRectanglePlacement(puzzle, rectangle, accepted);111 if (!validation.ok) return false;112 if (validation.given.id !== rectangle.clueId) return false;113114 const cells = getRectangleCells(rectangle);115 for (const cell of cells) {116 covered.add(cell);117 }118119 accepted.push(rectangle);120 }121122 return covered.size === puzzle.width * puzzle.height;123}