diff --git a/src_failed/game.css b/src_failed/game.css deleted file mode 100644 index 09454f5..0000000 --- a/src_failed/game.css +++ /dev/null @@ -1,256 +0,0 @@ -/* ========================================================= - 🔧 Controls (tweak these) - ========================================================= */ -:root { - --cell-size: 96px; - --cell-font-size: 48px; - --grid-gap: 6px; - --box-separator: 9px; -} - -/* ========================================================= - Container & Grid Base - ========================================================= */ -#sudoku_container { - position: relative; - margin: 1em auto; - width: fit-content; -} - -#sudoku { - position: relative; - background-color: var(--card, #1e2030); - border-radius: 0.5rem; - box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px rgba(130, 170, 255, 0.05); - padding: 0.5rem; - width: fit-content; -} - -/* ========================================================= - Grid Layout - ========================================================= */ -#sudoku .sudoku-grid { - display: grid; - grid-template-columns: - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size); - grid-template-rows: - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size); - column-gap: var(--grid-gap); - row-gap: var(--grid-gap); - user-select: none; -} - -/* ========================================================= - Cell Base Styles - ========================================================= */ -#sudoku .sudoku-cell { - width: var(--cell-size); - height: var(--cell-size); - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - font-family: "Fira Code", monospace; - font-size: var(--cell-font-size); - background: var(--muted, #2f334d); - color: var(--foreground, #c8d3f5); - transition: all 0.15s ease; - box-shadow: inset 0 0 3px rgba(27, 29, 43, 0.4); -} - -/* ========================================================= - 3x3 Box Separators - ========================================================= */ -/* Right borders (end of each 3x3 box horizontally) */ -#sudoku .sudoku-cell[data-c="2"], -#sudoku .sudoku-cell[data-c="5"], -#sudoku .sudoku-cell[data-c="8"] { - border-right: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* Left borders (start of each 3x3 box horizontally) */ -#sudoku .sudoku-cell[data-c="0"], -#sudoku .sudoku-cell[data-c="3"], -#sudoku .sudoku-cell[data-c="6"] { - border-left: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* Bottom borders (end of each 3x3 box vertically) */ -#sudoku .sudoku-cell[data-r="2"], -#sudoku .sudoku-cell[data-r="5"], -#sudoku .sudoku-cell[data-r="8"] { - border-bottom: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* Top borders (start of each 3x3 box vertically) */ -#sudoku .sudoku-cell[data-r="0"], -#sudoku .sudoku-cell[data-r="3"], -#sudoku .sudoku-cell[data-r="6"] { - border-top: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* ========================================================= - User-Entered Cells - ========================================================= */ -#sudoku .sudoku-cell.user-entered { - color: var(--primary, #82aaff); - font-weight: 600; -} - -/* ========================================================= - Active Cell Highlight (Navigation Mode) - ========================================================= */ -#sudoku .sudoku-cell.active { - outline: 2px solid var(--primary, #82aaff); - outline-offset: -2px; - background: color-mix(in srgb, var(--primary, #82aaff) 20%, var(--muted, #2f334d)); - box-shadow: - 0 0 8px color-mix(in srgb, var(--primary, #82aaff) 40%, transparent), - 0 0 12px color-mix(in srgb, var(--primary, #82aaff) 20%, transparent), - inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 15%, transparent); -} - -/* ========================================================= - Editing Cell Highlight (Edit Mode) - ========================================================= */ -#sudoku .sudoku-cell.editing { - outline: 2px solid var(--accent, #ffc777); - outline-offset: -2px; - background: color-mix(in srgb, var(--accent, #ffc777) 15%, var(--muted, #2f334d)); - box-shadow: - 0 0 16px color-mix(in srgb, var(--accent, #ffc777) 70%, transparent), - 0 0 24px color-mix(in srgb, var(--accent, #ffc777) 40%, transparent), - inset 0 0 12px color-mix(in srgb, var(--accent, #ffc777) 30%, transparent); - animation: editing-cell-glow 1.5s ease-in-out infinite; -} - -@keyframes editing-cell-glow { - 0%, 100% { - box-shadow: - 0 0 16px color-mix(in srgb, var(--accent, #ffc777) 70%, transparent), - 0 0 24px color-mix(in srgb, var(--accent, #ffc777) 40%, transparent), - inset 0 0 12px color-mix(in srgb, var(--accent, #ffc777) 30%, transparent); - } - 50% { - box-shadow: - 0 0 20px color-mix(in srgb, var(--accent, #ffc777) 90%, transparent), - 0 0 32px color-mix(in srgb, var(--accent, #ffc777) 60%, transparent), - inset 0 0 16px color-mix(in srgb, var(--accent, #ffc777) 40%, transparent); - } -} - -/* ========================================================= - Visual Feedback Effects - ========================================================= */ -/* Invalid move effect */ -#sudoku .sudoku-cell.invalid-move { - animation: sudoku-invalid-pulse 180ms ease-out; - box-shadow: - 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 85%, transparent), - 0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 45%, transparent); -} - -@keyframes sudoku-invalid-pulse { - 0% { - transform: scale(1.05); - box-shadow: - 0 0 16px color-mix(in srgb, var(--destructive, #c53b53) 95%, transparent), - 0 0 28px color-mix(in srgb, var(--destructive, #c53b53) 55%, transparent); - } - 100% { - transform: none; - box-shadow: none; - } -} - -/* Locked cell effect (pre-filled clues) */ -#sudoku .sudoku-cell.locked-cell { - animation: sudoku-locked-pulse 300ms ease-out; -} - -@keyframes sudoku-locked-pulse { - 0% { - transform: scale(1.08); - background: rgba(255, 85, 85, 0.3); - box-shadow: - 0 0 20px rgba(255, 85, 85, 0.9), - 0 0 32px rgba(255, 85, 85, 0.6), - inset 0 0 16px rgba(255, 85, 85, 0.4); - } - 50% { - transform: scale(1.05); - background: rgba(255, 85, 85, 0.2); - box-shadow: - 0 0 16px rgba(255, 85, 85, 0.8), - 0 0 28px rgba(255, 85, 85, 0.5), - inset 0 0 12px rgba(255, 85, 85, 0.3); - } - 100% { - transform: none; - background: transparent; - box-shadow: none; - } -} - -/* Trail effect for moves */ -#sudoku .sudoku-cell.trail { - box-shadow: - inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 60%, transparent), - 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 35%, transparent); - animation: sudoku-trail-fade 220ms ease-out; -} - -@keyframes sudoku-trail-fade { - from { background-color: color-mix(in srgb, var(--primary, #82aaff) 25%, transparent); } - to { background-color: transparent; } -} - -/* Valid move flash */ -#sudoku .sudoku-cell.valid-move { - animation: sudoku-valid-flash 300ms ease-out; -} - -@keyframes sudoku-valid-flash { - 0% { - background: color-mix(in srgb, var(--success, #4fd6be) 40%, transparent); - box-shadow: - 0 0 12px color-mix(in srgb, var(--success, #4fd6be) 80%, transparent), - 0 0 20px color-mix(in srgb, var(--success, #4fd6be) 50%, transparent), - inset 0 0 8px color-mix(in srgb, var(--success, #4fd6be) 35%, transparent); - } - 100% { - background: transparent; - box-shadow: none; - } -} - -/* Conflict highlighting */ -#sudoku .sudoku-cell.conflict { - outline: 2px solid var(--destructive, #c53b53); - outline-offset: -2px; - background: color-mix(in srgb, var(--destructive, #c53b53) 25%, var(--muted, #2f334d)); - box-shadow: - 0 0 10px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent), - 0 0 18px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent), - inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 30%, transparent); - animation: sudoku-conflict-pulse 1s ease-in-out infinite; -} - -@keyframes sudoku-conflict-pulse { - 0%, 100% { - box-shadow: - 0 0 10px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent), - 0 0 18px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent), - inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 30%, transparent); - } - 50% { - box-shadow: - 0 0 14px color-mix(in srgb, var(--destructive, #c53b53) 85%, transparent), - 0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 55%, transparent), - inset 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent); - } -} diff --git a/src_failed/index.ts b/src_failed/index.ts deleted file mode 100644 index 5eb33b9..0000000 --- a/src_failed/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -export { useGame } from './useGame'; -export type { GameManager, GameOptions } from './types'; - -export { useBoard } from './useBoard'; -export { useCursor } from './useCursor'; -export { useGameStatus } from './useGameStatus'; -export { useScore } from './useScore'; - -export type { - Coord, - SudokuBoard, - SudokuCell, - Difficulty, - BoardManager -} from './useBoard/types'; - -export type { - CursorManager, - CursorMode, - GameKeyManager, - KeyLogEntry -} from './useCursor/types'; - -export type { - GameStatus, - GameStatusManager -} from './useGameStatus/types'; - -export type { - ScoreManager, - TimerManager -} from './useScore/types'; \ No newline at end of file diff --git a/src_failed/types.ts b/src_failed/types.ts deleted file mode 100644 index 91f9be7..0000000 --- a/src_failed/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { RefObject } from 'react'; - -export type { Coord, SudokuBoard, SudokuCell, Difficulty } from './useBoard/types'; -export type { CursorManager, CursorMode, GameKeyManager, KeyLogEntry } from './useCursor/types'; -export type { GameStatus, GameStatusManager } from './useGameStatus/types'; -export type { ScoreManager, TimerManager } from './useScore/types'; - -export type GameOptions = { - difficulty?: Difficulty; -}; - -export type GameManager = { - containerRef: RefObject; - renderBoard: () => void; - gameStatus: GameStatus; - setGameStatus: (status: GameStatus) => void; - startGame: () => void; - togglePause: (pause?: boolean) => void; - quitGame: () => void; - cursor: CursorManager; - scoreManager: ScoreManager; - keyLog: KeyLogEntry[]; - clearKeyLog: () => void; - getKeyLog: () => KeyLogEntry[]; - board: { - generateBoard: (difficulty: Difficulty) => void; - solveBoard: () => boolean; - resetBoard: () => void; - getCell: (row: number, col: number) => any; - setCell: (row: number, col: number, value: string) => boolean; - canEditCell: (row: number, col: number) => boolean; - isComplete: () => boolean; - isFullyValid: () => boolean; - }; -}; \ No newline at end of file diff --git a/src_failed/useBoard/SudokuGenerator.ts b/src_failed/useBoard/SudokuGenerator.ts deleted file mode 100644 index 585150a..0000000 --- a/src_failed/useBoard/SudokuGenerator.ts +++ /dev/null @@ -1,316 +0,0 @@ -export type DifficultyName = 'easy' | 'medium' | 'hard' | 'very-hard' | 'insane' | 'inhuman'; -export type Difficulty = DifficultyName | number; - -const DIGITS = '123456789'; -const ROWS = 'ABCDEFGHI'; -const COLS = DIGITS; -let SQUARES: string[] = []; -let UNITS: string[][] = []; -let SQUARE_UNITS_MAP: Record = {}; -let SQUARE_PEERS_MAP: Record = {}; - -const MIN_GIVENS = 17; -const NR_SQUARES = 81; - -const DIFFICULTY: Record = { - easy: 62, - medium: 53, - hard: 44, - 'very-hard': 35, - insane: 26, - inhuman: 17, -}; - -const BLANK_CHAR = '.'; -const BLANK_BOARD = BLANK_CHAR.repeat(NR_SQUARES); - -function initialize() { - SQUARES = cross(ROWS, COLS); - UNITS = getAllUnits(ROWS, COLS); - SQUARE_UNITS_MAP = getSquareUnitsMap(SQUARES, UNITS); - SQUARE_PEERS_MAP = getSquarePeersMap(SQUARES, SQUARE_UNITS_MAP); -} - -function cross(a: string, b: string) { - const res: string[] = []; - for (const ai of a) { - for (const bi of b) { - res.push(ai + bi); - } - } - return res; -} - -function inStr(v: string, seq: string) { - return seq.indexOf(v) !== -1; -} - -function shuffle(seq: T[]): T[] { - const arr = [...seq]; - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [arr[i], arr[j]] = [arr[j], arr[i]]; - } - return arr; -} - -function randRange(max: number, min = 0) { - return Math.floor(Math.random() * (max - min)) + min; -} - -function stripDups(seq: T[]): T[] { - return Array.from(new Set(seq)); -} - -function forceRange(nr: number, max: number, min = 0) { - return Math.min(Math.max(nr || 0, min), max); -} - -function getAllUnits(rows: string, cols: string) { - const units: string[][] = []; - for (const r of rows) units.push(cross(r, cols)); - for (const c of cols) units.push(cross(rows, c)); - const row_blocks = ['ABC', 'DEF', 'GHI']; - const col_blocks = ['123', '456', '789']; - for (const rb of row_blocks) { - for (const cb of col_blocks) { - units.push(cross(rb, cb)); - } - } - return units; -} - -function getSquareUnitsMap(squares: string[], units: string[][]) { - const map: Record = {}; - for (const s of squares) { - const list: string[][] = []; - for (const u of units) { - if (u.includes(s)) list.push(u); - } - map[s] = list; - } - return map; -} - -function getSquarePeersMap(squares: string[], units_map: Record) { - const map: Record = {}; - for (const s of squares) { - const peers: string[] = []; - for (const unit of units_map[s]) { - for (const sq of unit) { - if (sq !== s && !peers.includes(sq)) peers.push(sq); - } - } - map[s] = peers; - } - return map; -} - -function getSquareValsMap(board: string) { - const map: Record = {}; - if (board.length !== SQUARES.length) throw 'Board length mismatch'; - for (let i = 0; i < SQUARES.length; i++) { - map[SQUARES[i]] = board[i]; - } - return map; -} - -function validateBoard(board: string): true | string { - if (!board) return 'Empty board'; - if (board.length !== NR_SQUARES) return 'Invalid board size'; - for (let i = 0; i < board.length; i++) { - const c = board[i]; - if (!inStr(c, DIGITS) && c !== BLANK_CHAR) { - return `Invalid char at ${i}: ${c}`; - } - } - return true; -} - -function getCandidatesMap(board: string): Record | false { - const report = validateBoard(board); - if (report !== true) throw report; - - const map: Record = {}; - const squares_values = getSquareValsMap(board); - - for (const s of SQUARES) map[s] = DIGITS; - - for (const s in squares_values) { - const val = squares_values[s]; - if (inStr(val, DIGITS)) { - const new_candidates = assign(map, s, val); - if (!new_candidates) return false; - } - } - return map; -} - -function assign(candidates: Record, square: string, val: string) { - const others = candidates[square].replace(val, ''); - for (const v of others) { - const next = eliminate(candidates, square, v); - if (!next) return false; - } - return candidates; -} - -function eliminate(candidates: Record, square: string, val: string) { - if (!inStr(val, candidates[square])) return candidates; - - candidates[square] = candidates[square].replace(val, ''); - const n = candidates[square].length; - - if (n === 1) { - const target_val = candidates[square]; - for (const peer of SQUARE_PEERS_MAP[square]) { - const next = eliminate(candidates, peer, target_val); - if (!next) return false; - } - } else if (n === 0) return false; - - for (const unit of SQUARE_UNITS_MAP[square]) { - const places: string[] = []; - for (const sq of unit) { - if (inStr(val, candidates[sq])) places.push(sq); - } - if (places.length === 0) return false; - else if (places.length === 1) { - const next = assign(candidates, places[0], val); - if (!next) return false; - } - } - return candidates; -} - -function search(candidates: Record, reverse: boolean): any { - if (!candidates) return false; - reverse = reverse || false; - - let max_nr_candidates = 0; - for (const s of SQUARES) { - const n = candidates[s].length; - if (n > max_nr_candidates) { - max_nr_candidates = n; - } - } - if (max_nr_candidates === 1) return candidates; - - let min_nr_candidates = 10; - let min_square = ''; - for (const s of SQUARES) { - const n = candidates[s].length; - if (n < min_nr_candidates && n > 1) { - min_nr_candidates = n; - min_square = s; - } - } - - const min_candidates = candidates[min_square]; - if (!reverse) { - for (const v of min_candidates) { - const copy = JSON.parse(JSON.stringify(candidates)); - const next = search(assign(copy, min_square, v), reverse); - if (next) return next; - } - } else { - for (let i = min_candidates.length - 1; i >= 0; --i) { - const v = min_candidates[i]; - const copy = JSON.parse(JSON.stringify(candidates)); - const next = search(assign(copy, min_square, v), reverse); - if (next) return next; - } - } - return false; -} - -export class SudokuGenerator { - static generate(difficulty: Difficulty, unique?: boolean): string { - let numericDifficulty: number; - if (typeof difficulty === 'string' || typeof difficulty === 'undefined') { - numericDifficulty = DIFFICULTY[difficulty as DifficultyName] || DIFFICULTY.easy; - } else { - numericDifficulty = difficulty; - } - - numericDifficulty = forceRange(numericDifficulty, NR_SQUARES + 1, MIN_GIVENS); - unique = unique ?? true; - - const blank_board = BLANK_BOARD; - let candidates = getCandidatesMap(blank_board); - - if (!candidates) return SudokuGenerator.generate(numericDifficulty); - - const shuffled_squares = shuffle(SQUARES); - - for (const square of shuffled_squares) { - const rand_candidate_idx = randRange(candidates[square].length); - const rand_candidate = candidates[square][rand_candidate_idx]; - if (!assign(candidates, square, rand_candidate)) break; - - const single_candidates: string[] = []; - for (const s of SQUARES) { - if (candidates[s].length === 1) single_candidates.push(candidates[s]); - } - - if ( - single_candidates.length >= numericDifficulty && - stripDups(single_candidates).length >= 8 - ) { - let board = ''; - let givens_idxs: number[] = []; - - for (let i = 0; i < SQUARES.length; i++) { - const s = SQUARES[i]; - if (candidates[s].length === 1) { - board += candidates[s]; - givens_idxs.push(i); - } else { - board += BLANK_CHAR; - } - } - - if (givens_idxs.length > numericDifficulty) { - givens_idxs = shuffle(givens_idxs); - for (let i = 0; i < givens_idxs.length - numericDifficulty; ++i) { - const target = givens_idxs[i]; - board = - board.substring(0, target) + - BLANK_CHAR + - board.substring(target + 1); - } - } - - if (SudokuGenerator.solve(board)) return board; - } - } - - return SudokuGenerator.generate(numericDifficulty); - } - - static solve(board: string, reverse?: boolean): string | false { - const report = validateBoard(board); - if (report !== true) throw report; - - let nr_givens = 0; - for (const ch of board) { - if (ch !== BLANK_CHAR && inStr(ch, DIGITS)) nr_givens++; - } - if (nr_givens < MIN_GIVENS) throw 'Too few givens'; - - reverse = reverse || false; - const candidates = getCandidatesMap(board); - if (!candidates) return false; - - const result = search(candidates, reverse); - - if (result) { - let solution = ''; - for (const s of SQUARES) solution += result[s]; - return solution; - } - return false; - } -} - -initialize(); \ No newline at end of file diff --git a/src_failed/useBoard/SudokuRenderer.ts b/src_failed/useBoard/SudokuRenderer.ts deleted file mode 100644 index e99b483..0000000 --- a/src_failed/useBoard/SudokuRenderer.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { SudokuBoard, SudokuCell } from './types'; - -export class SudokuRenderer { - static renderBoard(container: HTMLElement, board: SudokuBoard): void { - const sudokuDiv = document.createElement('div'); - sudokuDiv.id = 'sudoku'; - sudokuDiv.className = 'sudoku-board'; - - for (let row = 0; row < 9; row++) { - const rowDiv = document.createElement('div'); - rowDiv.className = 'sudoku-row'; - - for (let col = 0; col < 9; col++) { - const cell = board[row][col]; - const cellDiv = document.createElement('div'); - cellDiv.className = 'sudoku-cell'; - cellDiv.dataset.row = row.toString(); - cellDiv.dataset.col = col.toString(); - cellDiv.textContent = cell.value || ''; - - if (cell.isGiven) { - cellDiv.classList.add('given'); - } - - if (cell.hasConflict) { - cellDiv.classList.add('conflict'); - } - - if (col === 2 || col === 5) { - cellDiv.classList.add('border-right'); - } - - if (row === 2 || row === 5) { - cellDiv.classList.add('border-bottom'); - } - - rowDiv.appendChild(cellDiv); - } - - sudokuDiv.appendChild(rowDiv); - } - - container.innerHTML = ''; - container.appendChild(sudokuDiv); - } - - static renderEmptyBoard(container: HTMLElement): void { - const emptyBoard: SudokuBoard = Array(9).fill(null).map(() => - Array(9).fill(null).map(() => ({ - value: '', - isGiven: false, - hasConflict: false - })) - ); - - SudokuRenderer.renderBoard(container, emptyBoard); - } - - static updateCell(container: HTMLElement, row: number, col: number, cell: SudokuCell): void { - const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; - if (!cellEl) return; - - cellEl.textContent = cell.value || ''; - cellEl.classList.toggle('given', cell.isGiven); - cellEl.classList.toggle('conflict', cell.hasConflict); - } - - static highlightCursor(container: HTMLElement, row: number, col: number, editMode: boolean): void { - const cells = container.querySelectorAll('.sudoku-cell'); - cells.forEach(cell => cell.classList.remove('cursor', 'edit-mode')); - - const cursorCell = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; - if (cursorCell) { - cursorCell.classList.add('cursor'); - if (editMode) { - cursorCell.classList.add('edit-mode'); - } - } - } - - static flashCell(container: HTMLElement, row: number, col: number, className: string, duration: number = 300): void { - const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; - if (!cellEl) return; - - cellEl.classList.add(className); - setTimeout(() => cellEl.classList.remove(className), duration); - } - - static highlightConflicts(container: HTMLElement, conflictIndices: number[], duration: number = 2000): void { - conflictIndices.forEach((idx) => { - const row = Math.floor(idx / 9); - const col = idx % 9; - const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.add('conflict'); - } - }); - - setTimeout(() => { - conflictIndices.forEach((idx) => { - const row = Math.floor(idx / 9); - const col = idx % 9; - const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.remove('conflict'); - } - }); - }, duration); - } -} \ No newline at end of file diff --git a/src_failed/useBoard/index.ts b/src_failed/useBoard/index.ts deleted file mode 100644 index 89a59d7..0000000 --- a/src_failed/useBoard/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { useBoard } from './useBoard'; -export type { BoardManager, SudokuBoard, SudokuCell, Coord, Difficulty } from './types'; -export { SudokuGenerator } from './SudokuGenerator'; -export { SudokuRenderer } from './SudokuRenderer'; \ No newline at end of file diff --git a/src_failed/useBoard/types.ts b/src_failed/useBoard/types.ts deleted file mode 100644 index b6d369a..0000000 --- a/src_failed/useBoard/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { RefObject } from 'react'; - -export type Coord = { row: number; col: number }; - -export type SudokuCell = { - value: string; - isGiven: boolean; - hasConflict: boolean; -}; - -export type SudokuBoard = SudokuCell[][]; - -export type Difficulty = 'easy' | 'medium' | 'hard' | 'very-hard' | 'insane' | 'inhuman'; - -export type BoardManager = { - containerRef: RefObject; - boardRef: RefObject; - initialBoardRef: RefObject; - solutionRef: RefObject; - renderBoard: () => void; - generateBoard: (difficulty: Difficulty) => void; - getCell: (row: number, col: number) => SudokuCell | null; - setCell: (row: number, col: number, value: string) => boolean; - canEditCell: (row: number, col: number) => boolean; - isValidPlacement: (row: number, col: number, digit: string) => boolean; - getConflicts: (row: number, col: number) => number[]; - isComplete: () => boolean; - isFullyValid: () => boolean; - solveBoard: () => boolean; - resetBoard: () => void; -}; \ No newline at end of file diff --git a/src_failed/useBoard/useBoard.ts b/src_failed/useBoard/useBoard.ts deleted file mode 100644 index 4f8006c..0000000 --- a/src_failed/useBoard/useBoard.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { useRef, useState, useCallback } from 'react'; -import type { RefObject } from 'react'; -import type { BoardManager, SudokuBoard, SudokuCell, Difficulty } from './types'; -import { SudokuGenerator } from './SudokuGenerator'; -import { SudokuRenderer } from './SudokuRenderer'; - -export function useBoard(): BoardManager { - const containerRef = useRef(null); - const boardRef = useRef([]); - const initialBoardRef = useRef(''); - const solutionRef = useRef(null); - - const initializeBoard = useCallback((boardString: string): SudokuBoard => { - const board: SudokuBoard = []; - for (let row = 0; row < 9; row++) { - const boardRow: SudokuCell[] = []; - for (let col = 0; col < 9; col++) { - const index = row * 9 + col; - const value = boardString[index]; - boardRow.push({ - value: value === '.' ? '' : value, - isGiven: value !== '.', - hasConflict: false - }); - } - board.push(boardRow); - } - return board; - }, []); - - const getCell = useCallback((row: number, col: number): SudokuCell | null => { - const board = boardRef.current; - if (row < 0 || row >= 9 || col < 0 || col >= 9) return null; - return board[row]?.[col] || null; - }, []); - - const canEditCell = useCallback((row: number, col: number): boolean => { - const cell = getCell(row, col); - return cell ? !cell.isGiven : false; - }, [getCell]); - - const isValidPlacement = useCallback((boardString: string, row: number, col: number, digit: string): boolean => { - if (digit === '.') return true; - - for (let c = 0; c < 9; c++) { - const checkIdx = row * 9 + c; - if (c !== col && boardString[checkIdx] === digit) { - return false; - } - } - - for (let r = 0; r < 9; r++) { - const checkIdx = r * 9 + col; - if (r !== row && boardString[checkIdx] === digit) { - return false; - } - } - - const boxRow = Math.floor(row / 3) * 3; - const boxCol = Math.floor(col / 3) * 3; - for (let r = boxRow; r < boxRow + 3; r++) { - for (let c = boxCol; c < boxCol + 3; c++) { - const checkIdx = r * 9 + c; - if ((r !== row || c !== col) && boardString[checkIdx] === digit) { - return false; - } - } - } - - return true; - }, []); - - const getConflicts = useCallback((boardString: string, row: number, col: number): number[] => { - const conflicts: number[] = []; - const digit = boardString[row * 9 + col]; - - if (digit === '.') return conflicts; - - for (let c = 0; c < 9; c++) { - if (c !== col && boardString[row * 9 + c] === digit) { - conflicts.push(row * 9 + c); - } - } - - for (let r = 0; r < 9; r++) { - if (r !== row && boardString[r * 9 + col] === digit) { - conflicts.push(r * 9 + col); - } - } - - const boxRow = Math.floor(row / 3) * 3; - const boxCol = Math.floor(col / 3) * 3; - for (let r = boxRow; r < boxRow + 3; r++) { - for (let c = boxCol; c < boxCol + 3; c++) { - const idx = r * 9 + c; - if ((r !== row || c !== col) && boardString[idx] === digit) { - if (!conflicts.includes(idx)) { - conflicts.push(idx); - } - } - } - } - - return conflicts; - }, []); - - const isFullyValid = useCallback((boardString: string): boolean => { - for (let r = 0; r < 9; r++) { - let rowSum = 0; - for (let c = 0; c < 9; c++) { - const digit = boardString[r * 9 + c]; - if (digit === '.') return false; - rowSum += parseInt(digit, 10); - } - if (rowSum !== 45) return false; - } - - for (let c = 0; c < 9; c++) { - let colSum = 0; - for (let r = 0; r < 9; r++) { - const digit = boardString[r * 9 + c]; - colSum += parseInt(digit, 10); - } - if (colSum !== 45) return false; - } - - for (let boxRow = 0; boxRow < 3; boxRow++) { - for (let boxCol = 0; boxCol < 3; boxCol++) { - let boxSum = 0; - for (let r = boxRow * 3; r < boxRow * 3 + 3; r++) { - for (let c = boxCol * 3; c < boxCol * 3 + 3; c++) { - const digit = boardString[r * 9 + c]; - boxSum += parseInt(digit, 10); - } - } - if (boxSum !== 45) return false; - } - } - - return true; - }, []); - - const isComplete = useCallback((boardString: string): boolean => { - return !boardString.includes('.'); - }, []); - - const boardToString = useCallback((board: SudokuBoard): string => { - return board.map(row => - row.map(cell => cell.value || '.').join('') - ).join(''); - }, []); - - const setCell = useCallback((row: number, col: number, value: string): boolean => { - if (!canEditCell(row, col)) return false; - - const board = boardRef.current; - const boardString = boardToString(board); - const newBoardString = boardString.substring(0, row * 9 + col) + value + boardString.substring(row * 9 + col + 1); - - if (!isValidPlacement(newBoardString, row, col, value)) { - return false; - } - - board[row][col] = { - value: value === '.' ? '' : value, - isGiven: board[row][col].isGiven, - hasConflict: false - }; - - return true; - }, [canEditCell, isValidPlacement, boardToString]); - - const generateBoard = useCallback((difficulty: Difficulty): void => { - const newBoardString = SudokuGenerator.generate(difficulty); - const solution = SudokuGenerator.solve(newBoardString); - - initialBoardRef.current = newBoardString; - solutionRef.current = solution; - boardRef.current = initializeBoard(newBoardString); - }, [initializeBoard]); - - const solveBoard = useCallback((): boolean => { - const solution = solutionRef.current; - if (!solution) return false; - - boardRef.current = initializeBoard(solution); - return true; - }, [initializeBoard]); - - const resetBoard = useCallback((): void => { - const initialBoard = initialBoardRef.current; - if (initialBoard) { - boardRef.current = initializeBoard(initialBoard); - } - }, [initializeBoard]); - - const renderBoard = useCallback((): void => { - const container = containerRef.current; - const board = boardRef.current; - - if (!container || board.length === 0) { - if (container) { - SudokuRenderer.renderEmptyBoard(container); - } - return; - } - - SudokuRenderer.renderBoard(container, board); - }, []); - - return { - containerRef, - boardRef, - initialBoardRef, - solutionRef, - renderBoard, - generateBoard, - getCell, - setCell, - canEditCell, - isValidPlacement: (row, col, digit) => { - const boardString = boardToString(boardRef.current); - return isValidPlacement(boardString, row, col, digit); - }, - getConflicts: (row, col) => { - const boardString = boardToString(boardRef.current); - return getConflicts(boardString, row, col); - }, - isComplete: () => { - const boardString = boardToString(boardRef.current); - return isComplete(boardString); - }, - isFullyValid: () => { - const boardString = boardToString(boardRef.current); - return isFullyValid(boardString); - }, - solveBoard, - resetBoard - }; -} \ No newline at end of file diff --git a/src_failed/useCursor/index.ts b/src_failed/useCursor/index.ts deleted file mode 100644 index 2a6a4c6..0000000 --- a/src_failed/useCursor/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { useCursor } from './useCursor'; -export { useVimMotions } from './useVimMotions'; -export type { - CursorManager, - CursorMode, - Coord, - Motion, - VimMotionSystem, - GameKeyManager, - KeyLogEntry, - CursorRenderer -} from './types'; \ No newline at end of file diff --git a/src_failed/useCursor/types.ts b/src_failed/useCursor/types.ts deleted file mode 100644 index b4d3fcd..0000000 --- a/src_failed/useCursor/types.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { RefObject } from 'react'; -import type { BoardManager } from '../useBoard/types'; - -export type Coord = { row: number; col: number }; - -export type CursorMode = 'normal' | 'edit'; - -export type Motion = { - dr: number; - dc: number; - steps: number; -}; - -export type CursorManager = { - position: () => Coord; - mode: () => CursorMode; - move: (dRows: number, dCols: number, count: number) => void; - moveLeft: (count?: number) => void; - moveRight: (count?: number) => void; - moveUp: (count?: number) => void; - moveDown: (count?: number) => void; - moveToStart: () => void; - moveToEnd: () => void; - moveToTop: () => void; - moveToBottom: () => void; - repeatLastMotion: () => void; - resetCount: () => void; - getCount: () => string; - hasCount: () => boolean; - setCount: (digit: string) => void; - setLastKey: (key: string) => void; - getLastKey: () => string; - setMode: (mode: CursorMode) => void; - flashInvalid: () => void; - flashValid: () => void; - flashLocked: () => void; - highlightConflicts: (conflictIndices: number[]) => void; -}; - -export type VimMotionSystem = { - processKey: (key: string) => { type: 'move' | 'anchor' | 'repeat' | 'count' | 'digit' | 'erase' | 'mode', data: any } | null; - resetCount: () => void; - getCount: () => number; - hasCount: () => boolean; - setLastMotion: (motion: Motion) => void; - repeatLastMotion: () => Motion | null; -}; - -export type GameKeyManager = { - keyLog: KeyLogEntry[]; - clearKeyLog: () => void; - getKeyLog: () => KeyLogEntry[]; - editMode: CursorMode; - setEditMode: (mode: CursorMode) => void; -}; - -export type KeyLogEntry = { key: string; timestamp: number }; - -export type CursorRenderer = { - highlightCursor: (row: number, col: number, editMode: boolean) => void; - flashCell: (row: number, col: number, className: string, duration?: number) => void; - highlightConflicts: (conflictIndices: number[], duration?: number) => void; -}; \ No newline at end of file diff --git a/src_failed/useCursor/useCursor.ts b/src_failed/useCursor/useCursor.ts deleted file mode 100644 index 451a72d..0000000 --- a/src_failed/useCursor/useCursor.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { useState, useCallback, useEffect } from 'react'; -import type { BoardManager } from '../useBoard/types'; -import type { CursorManager, CursorMode, Coord, VimMotionSystem, GameKeyManager, KeyLogEntry } from './types'; -import { useVimMotions } from './useVimMotions'; - -export function useCursor(board: BoardManager): CursorManager & { keyManager: GameKeyManager } { - const [position, setPosition] = useState({ row: 0, col: 0 }); - const [mode, setMode] = useState('normal'); - const [keyLog, setKeyLog] = useState([]); - - const vimMotions = useVimMotions(); - - const addKeyToLog = useCallback((key: string) => { - setKeyLog(prev => [...prev, { key, timestamp: Date.now() }]); - }, []); - - const move = useCallback((dRows: number, dCols: number, count: number) => { - setPosition(prev => { - const newRow = prev.row + (dRows * count); - const newCol = prev.col + (dCols * count); - - if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) { - flashInvalid(); - return prev; - } - - return { row: newRow, col: newCol }; - }); - }, []); - - const moveLeft = useCallback((count = 1) => { - move(0, -1, count); - }, [move]); - - const moveRight = useCallback((count = 1) => { - move(0, 1, count); - }, [move]); - - const moveUp = useCallback((count = 1) => { - move(-1, 0, count); - }, [move]); - - const moveDown = useCallback((count = 1) => { - move(1, 0, count); - }, [move]); - - const moveToStart = useCallback(() => { - setPosition(prev => ({ ...prev, col: 0 })); - }, []); - - const moveToEnd = useCallback(() => { - setPosition(prev => ({ ...prev, col: 8 })); - }, []); - - const moveToTop = useCallback(() => { - setPosition({ row: 0, col: 0 }); - }, []); - - const moveToBottom = useCallback(() => { - setPosition({ row: 8, col: 0 }); - }, []); - - const repeatLastMotion = useCallback(() => { - const lastMotion = vimMotions.repeatLastMotion(); - if (lastMotion) { - move(lastMotion.dr, lastMotion.dc, lastMotion.steps); - } - }, [vimMotions, move]); - - const resetCount = useCallback(() => { - vimMotions.resetCount(); - }, [vimMotions]); - - const getCount = useCallback(() => { - return vimMotions.getCount().toString(); - }, [vimMotions]); - - const hasCount = useCallback(() => { - return vimMotions.hasCount(); - }, [vimMotions]); - - const setCount = useCallback((digit: string) => { - vimMotions.processKey(digit); - }, [vimMotions]); - - const setLastKey = useCallback((key: string) => { - addKeyToLog(key); - }, [addKeyToLog]); - - const getLastKey = useCallback(() => { - return keyLog[keyLog.length - 1]?.key || ''; - }, [keyLog]); - - const flashInvalid = useCallback(() => { - const container = board.containerRef.current; - if (!container) return; - - const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.add('invalid-move'); - setTimeout(() => cellEl.classList.remove('invalid-move'), 180); - } - }, [board, position]); - - const flashValid = useCallback(() => { - const container = board.containerRef.current; - if (!container) return; - - const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.add('valid-move'); - setTimeout(() => cellEl.classList.remove('valid-move'), 300); - } - }, [board, position]); - - const flashLocked = useCallback(() => { - const container = board.containerRef.current; - if (!container) return; - - const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.add('locked-cell'); - setTimeout(() => cellEl.classList.remove('locked-cell'), 300); - } - }, [board, position]); - - const highlightConflicts = useCallback((conflictIndices: number[]) => { - const container = board.containerRef.current; - if (!container) return; - - conflictIndices.forEach((idx) => { - const row = Math.floor(idx / 9); - const col = idx % 9; - const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.add('conflict'); - } - }); - - setTimeout(() => { - conflictIndices.forEach((idx) => { - const row = Math.floor(idx / 9); - const col = idx % 9; - const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.remove('conflict'); - } - }); - }, 2000); - }, [board]); - - const clearKeyLog = useCallback(() => { - setKeyLog([]); - }, []); - - const getKeyLog = useCallback(() => { - return [...keyLog]; - }, [keyLog]); - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - const key = e.key; - const result = vimMotions.processKey(key); - - if (!result) return; - - addKeyToLog(key); - - switch (result.type) { - case 'move': - move(result.data.dr, result.data.dc, result.data.steps); - break; - - case 'anchor': - switch (result.data) { - case 'start': - moveToStart(); - break; - case 'end': - moveToEnd(); - break; - case 'top': - moveToTop(); - break; - case 'bottom': - moveToBottom(); - break; - } - break; - - case 'repeat': - if (result.data) { - move(result.data.dr, result.data.dc, result.data.steps); - } - break; - - case 'mode': - setMode(result.data); - break; - - case 'digit': - if (mode === 'edit') { - const success = board.setCell(position.row, position.col, result.data); - if (success) { - flashValid(); - } else if (!board.canEditCell(position.row, position.col)) { - flashLocked(); - } else { - const conflicts = board.getConflicts(position.row, position.col); - if (conflicts.length > 0) { - highlightConflicts(conflicts); - } - flashInvalid(); - } - } - break; - - case 'erase': - if (mode === 'edit') { - const success = board.setCell(position.row, position.col, '.'); - if (success) { - flashValid(); - } else if (!board.canEditCell(position.row, position.col)) { - flashLocked(); - } else { - flashInvalid(); - } - } - break; - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [vimMotions, addKeyToLog, move, moveToStart, moveToEnd, moveToTop, moveToBottom, mode, board, position, flashValid, flashLocked, flashInvalid, highlightConflicts]); - - const cursorManager: CursorManager = { - position: () => position, - mode: () => mode, - move, - moveLeft, - moveRight, - moveUp, - moveDown, - moveToStart, - moveToEnd, - moveToTop, - moveToBottom, - repeatLastMotion, - resetCount, - getCount, - hasCount, - setCount, - setLastKey, - getLastKey, - setMode, - flashInvalid, - flashValid, - flashLocked, - highlightConflicts - }; - - const keyManager: GameKeyManager = { - keyLog, - clearKeyLog, - getKeyLog, - editMode: mode, - setEditMode: setMode - }; - - return { - ...cursorManager, - keyManager - }; -} \ No newline at end of file diff --git a/src_failed/useCursor/useVimMotions.ts b/src_failed/useCursor/useVimMotions.ts deleted file mode 100644 index a79e26a..0000000 --- a/src_failed/useCursor/useVimMotions.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { useRef } from 'react'; -import type { VimMotionSystem, Motion } from './types'; - -export function useVimMotions(): VimMotionSystem { - const countRef = useRef(''); - const lastMotionRef = useRef(null); - - const processKey = (key: string) => { - if (key >= '0' && key <= '9') { - if (key === '0' && countRef.current === '') { - return { type: 'anchor', data: 'start' }; - } - countRef.current += key; - return { type: 'count', data: key }; - } - - const count = countRef.current ? parseInt(countRef.current, 10) : 1; - - switch (key) { - case 'h': - const motionH = { dr: 0, dc: -1, steps: count }; - lastMotionRef.current = motionH; - countRef.current = ''; - return { type: 'move', data: motionH }; - - case 'j': - const motionJ = { dr: 1, dc: 0, steps: count }; - lastMotionRef.current = motionJ; - countRef.current = ''; - return { type: 'move', data: motionJ }; - - case 'k': - const motionK = { dr: -1, dc: 0, steps: count }; - lastMotionRef.current = motionK; - countRef.current = ''; - return { type: 'move', data: motionK }; - - case 'l': - const motionL = { dr: 0, dc: 1, steps: count }; - lastMotionRef.current = motionL; - countRef.current = ''; - return { type: 'move', data: motionL }; - - case '^': - countRef.current = ''; - return { type: 'anchor', data: 'start' }; - - case '$': - countRef.current = ''; - return { type: 'anchor', data: 'end' }; - - case 'g': - countRef.current = ''; - return { type: 'anchor', data: 'top' }; - - case 'G': - countRef.current = ''; - return { type: 'anchor', data: 'bottom' }; - - case '.': - const lastMotion = lastMotionRef.current; - if (lastMotion) { - return { type: 'repeat', data: lastMotion }; - } - return null; - - case 'i': - countRef.current = ''; - return { type: 'mode', data: 'edit' }; - - case 'Escape': - countRef.current = ''; - return { type: 'mode', data: 'normal' }; - - case 'x': - case 'Delete': - case 'Backspace': - countRef.current = ''; - return { type: 'erase', data: null }; - - default: - if (key >= '1' && key <= '9') { - countRef.current = ''; - return { type: 'digit', data: key }; - } - return null; - } - }; - - const resetCount = () => { - countRef.current = ''; - }; - - const getCount = () => { - return countRef.current ? parseInt(countRef.current, 10) : 1; - }; - - const hasCount = () => { - return countRef.current !== ''; - }; - - const setLastMotion = (motion: Motion) => { - lastMotionRef.current = motion; - }; - - const repeatLastMotion = () => { - return lastMotionRef.current; - }; - - return { - processKey, - resetCount, - getCount, - hasCount, - setLastMotion, - repeatLastMotion - }; -} \ No newline at end of file diff --git a/src_failed/useGame.ts b/src_failed/useGame.ts deleted file mode 100644 index a3a1709..0000000 --- a/src_failed/useGame.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { useEffect } from 'react'; -import { useBoard } from './useBoard'; -import { useCursor } from './useCursor'; -import { useGameStatus } from './useGameStatus'; -import { useScore } from './useScore'; -import type { GameManager, GameOptions } from './types'; - -export function useGame(options: GameOptions, platformHook?: unknown): GameManager { - const board = useBoard(); - const gameStatusManager = useGameStatus(); - const { cursor, keyManager } = useCursor(board); - const scoreManager = useScore({ - board, - gameStatusManager, - keystrokeCount: keyManager.keyLog.length - }); - - useEffect(() => { - board.generateBoard('medium'); - }, [board]); - - useEffect(() => { - const { gameStatus } = gameStatusManager; - - if (gameStatus === 'waiting') { - board.renderBoard(); - } else { - board.renderBoard(); - const container = board.containerRef.current; - if (container) { - const pos = cursor.position(); - const cellEl = container.querySelector(`.sudoku-cell[data-row="${pos.row}"][data-col="${pos.col}"]`) as HTMLElement; - if (cellEl) { - cellEl.classList.add('cursor'); - if (cursor.mode() === 'edit') { - cellEl.classList.add('edit-mode'); - } - } - } - } - }, [gameStatusManager.gameStatus, board, cursor]); - - useEffect(() => { - const container = board.containerRef.current; - if (!container) return; - - const handleCellClick = (e: MouseEvent) => { - const target = e.target as HTMLElement; - const cellEl = target.closest('.sudoku-cell') as HTMLElement; - if (!cellEl) return; - - const row = parseInt(cellEl.dataset.row || '0'); - const col = parseInt(cellEl.dataset.col || '0'); - - const currentPos = cursor.position(); - if (currentPos.row !== row || currentPos.col !== col) { - cursor.move(row - currentPos.row, col - currentPos.col, 1); - } - }; - - container.addEventListener('click', handleCellClick); - return () => container.removeEventListener('click', handleCellClick); - }, [board, cursor]); - - const startGame = () => { - board.generateBoard('medium'); - gameStatusManager.startGame(); - }; - - const togglePause = (pause?: boolean) => { - gameStatusManager.togglePause(pause); - }; - - const quitGame = () => { - gameStatusManager.quitGame(); - }; - - const gameManager: GameManager = { - containerRef: board.containerRef, - renderBoard: board.renderBoard, - gameStatus: gameStatusManager.gameStatus, - setGameStatus: gameStatusManager.setGameStatus, - startGame, - togglePause, - quitGame, - cursor, - scoreManager, - keyLog: keyManager.keyLog, - clearKeyLog: keyManager.clearKeyLog, - getKeyLog: keyManager.getKeyLog, - board, - solveBoard: board.solveBoard, - resetBoard: board.resetBoard - }; - - if (typeof platformHook === 'function') { - platformHook(gameManager); - } - - return gameManager; -} \ No newline at end of file diff --git a/src_failed/useGameStatus/index.ts b/src_failed/useGameStatus/index.ts deleted file mode 100644 index c72f736..0000000 --- a/src_failed/useGameStatus/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { useGameStatus } from './useGameStatus'; -export type { GameStatus, GameStatusManager } from './types'; \ No newline at end of file diff --git a/src_failed/useGameStatus/types.ts b/src_failed/useGameStatus/types.ts deleted file mode 100644 index d764da1..0000000 --- a/src_failed/useGameStatus/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type GameStatus = 'waiting' | 'started' | 'paused' | 'game-over' | 'game-won'; - -export type GameStatusManager = { - gameStatus: GameStatus; - setGameStatus: (status: GameStatus) => void; - startGame: () => void; - togglePause: (pause?: boolean) => void; - quitGame: () => void; -}; \ No newline at end of file diff --git a/src_failed/useGameStatus/useGameStatus.ts b/src_failed/useGameStatus/useGameStatus.ts deleted file mode 100644 index 56ecde4..0000000 --- a/src_failed/useGameStatus/useGameStatus.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useState, useCallback } from 'react'; -import type { GameStatusManager, GameStatus } from './types'; - -export function useGameStatus(): GameStatusManager { - const [gameStatus, setGameStatus] = useState('waiting'); - - const startGame = useCallback(() => { - setGameStatus('started'); - }, []); - - const togglePause = useCallback((pause?: boolean) => { - setGameStatus(current => { - if (current === 'started' || current === 'paused') { - return pause !== undefined ? (pause ? 'paused' : 'started') : - (current === 'started' ? 'paused' : 'started'); - } - return current; - }); - }, []); - - const quitGame = useCallback(() => { - setGameStatus('waiting'); - }, []); - - return { - gameStatus, - setGameStatus, - startGame, - togglePause, - quitGame - }; -} \ No newline at end of file diff --git a/src_failed/useScore/index.ts b/src_failed/useScore/index.ts deleted file mode 100644 index 1cd87d9..0000000 --- a/src_failed/useScore/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { useScore } from './useScore'; -export { useTimer } from './useTimer'; -export type { ScoreManager, TimerManager, UseScoreProps } from './types'; \ No newline at end of file diff --git a/src_failed/useScore/types.ts b/src_failed/useScore/types.ts deleted file mode 100644 index 128174b..0000000 --- a/src_failed/useScore/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { BoardManager } from '../useBoard/types'; -import type { GameStatusManager } from '../useGameStatus/types'; - -export type TimerManager = { - timeValue: number; - startTimer: () => void; - stopTimer: () => void; - resetTimer: () => void; -}; - -export type ScoreManager = { - timeValue: number; - keystrokes: number; - mistakes: number; - hints: number; - efficiency: number; - finalScore: number | null; - startTimer: () => void; - stopTimer: () => void; - resetTimer: () => void; - addMistake: () => void; - addHint: () => void; - calculateEfficiency: () => number; - calculateFinalScore: () => number | null; -}; - -export type UseScoreProps = { - board: BoardManager; - gameStatusManager: GameStatusManager; - keystrokeCount: number; -}; \ No newline at end of file diff --git a/src_failed/useScore/useScore.ts b/src_failed/useScore/useScore.ts deleted file mode 100644 index eb4179e..0000000 --- a/src_failed/useScore/useScore.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import type { BoardManager } from '../useBoard/types'; -import type { GameStatusManager } from '../useGameStatus/types'; -import type { ScoreManager, UseScoreProps } from './types'; -import { useTimer } from './useTimer'; - -export function useScore({ board, gameStatusManager, keystrokeCount }: UseScoreProps): ScoreManager { - const timer = useTimer(); - const [mistakes, setMistakes] = useState(0); - const [hints, setHints] = useState(0); - - const addMistake = useCallback(() => { - setMistakes(prev => prev + 1); - }, []); - - const addHint = useCallback(() => { - setHints(prev => prev + 1); - }, []); - - const calculateEfficiency = useCallback((): number => { - const boardString = board.boardRef.current.map(row => - row.map(cell => cell.value || '.').join('') - ).join(''); - - const filledCells = boardString.replace(/\./g, '').length; - const totalCells = 81; - const givenCells = board.initialBoardRef.current.replace(/\./g, '').length; - const userFilledCells = filledCells - givenCells; - - if (userFilledCells === 0) return 100; - - const optimalMoves = userFilledCells; - const actualMoves = keystrokeCount; - - const efficiency = Math.round((optimalMoves / actualMoves) * 100); - return Math.max(0, Math.min(100, efficiency)); - }, [board, keystrokeCount]); - - const calculateFinalScore = useCallback((): number | null => { - const { gameStatus } = gameStatusManager; - - if (gameStatus !== 'game-won') return null; - - const efficiency = calculateEfficiency(); - const timeInSeconds = timer.timeValue / 1000; - const timeBonus = Math.max(0, 3600 - timeInSeconds) * 10; - const mistakePenalty = mistakes * 100; - const hintPenalty = hints * 200; - - const baseScore = 10000; - const efficiencyBonus = efficiency * 50; - - const finalScore = baseScore + efficiencyBonus + timeBonus - mistakePenalty - hintPenalty; - return Math.max(0, Math.round(finalScore)); - }, [gameStatusManager, calculateEfficiency, timer.timeValue, mistakes, hints]); - - useEffect(() => { - const { gameStatus } = gameStatusManager; - - if (gameStatus === 'started') { - timer.startTimer(); - } else if (gameStatus === 'paused' || gameStatus === 'game-over' || gameStatus === 'game-won') { - timer.stopTimer(); - } - - if (gameStatus === 'waiting') { - timer.resetTimer(); - setMistakes(0); - setHints(0); - } - }, [gameStatusManager.gameStatus, timer]); - - useEffect(() => { - const { gameStatus, setGameStatus } = gameStatusManager; - - if (gameStatus === 'started' && board.isComplete() && board.isFullyValid()) { - setGameStatus('game-won'); - } - }, [board, gameStatusManager]); - - return { - timeValue: timer.timeValue, - keystrokes: keystrokeCount, - mistakes, - hints, - efficiency: calculateEfficiency(), - finalScore: calculateFinalScore(), - startTimer: timer.startTimer, - stopTimer: timer.stopTimer, - resetTimer: timer.resetTimer, - addMistake, - addHint, - calculateEfficiency, - calculateFinalScore - }; -} \ No newline at end of file diff --git a/src_failed/useScore/useTimer.ts b/src_failed/useScore/useTimer.ts deleted file mode 100644 index 0412bbf..0000000 --- a/src_failed/useScore/useTimer.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useState, useEffect, useRef } from 'react'; -import type { TimerManager } from './types'; - -export function useTimer(): TimerManager { - const [timeValue, setTimeValue] = useState(0); - const intervalRef = useRef(null); - const startTimeRef = useRef(0); - - const startTimer = () => { - if (intervalRef.current) return; - - startTimeRef.current = Date.now() - timeValue; - intervalRef.current = setInterval(() => { - setTimeValue(Date.now() - startTimeRef.current); - }, 100); - }; - - const stopTimer = () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - }; - - const resetTimer = () => { - stopTimer(); - setTimeValue(0); - startTimeRef.current = 0; - }; - - useEffect(() => { - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, []); - - return { - timeValue, - startTimer, - stopTimer, - resetTimer - }; -} \ No newline at end of file diff --git a/src_old/game.css b/src_old/game.css deleted file mode 100644 index 09454f5..0000000 --- a/src_old/game.css +++ /dev/null @@ -1,256 +0,0 @@ -/* ========================================================= - 🔧 Controls (tweak these) - ========================================================= */ -:root { - --cell-size: 96px; - --cell-font-size: 48px; - --grid-gap: 6px; - --box-separator: 9px; -} - -/* ========================================================= - Container & Grid Base - ========================================================= */ -#sudoku_container { - position: relative; - margin: 1em auto; - width: fit-content; -} - -#sudoku { - position: relative; - background-color: var(--card, #1e2030); - border-radius: 0.5rem; - box-shadow: 0 0 10px rgba(27, 29, 43, 0.6), inset 0 0 6px rgba(130, 170, 255, 0.05); - padding: 0.5rem; - width: fit-content; -} - -/* ========================================================= - Grid Layout - ========================================================= */ -#sudoku .sudoku-grid { - display: grid; - grid-template-columns: - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size); - grid-template-rows: - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size) - var(--cell-size) var(--cell-size) var(--cell-size); - column-gap: var(--grid-gap); - row-gap: var(--grid-gap); - user-select: none; -} - -/* ========================================================= - Cell Base Styles - ========================================================= */ -#sudoku .sudoku-cell { - width: var(--cell-size); - height: var(--cell-size); - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - font-family: "Fira Code", monospace; - font-size: var(--cell-font-size); - background: var(--muted, #2f334d); - color: var(--foreground, #c8d3f5); - transition: all 0.15s ease; - box-shadow: inset 0 0 3px rgba(27, 29, 43, 0.4); -} - -/* ========================================================= - 3x3 Box Separators - ========================================================= */ -/* Right borders (end of each 3x3 box horizontally) */ -#sudoku .sudoku-cell[data-c="2"], -#sudoku .sudoku-cell[data-c="5"], -#sudoku .sudoku-cell[data-c="8"] { - border-right: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* Left borders (start of each 3x3 box horizontally) */ -#sudoku .sudoku-cell[data-c="0"], -#sudoku .sudoku-cell[data-c="3"], -#sudoku .sudoku-cell[data-c="6"] { - border-left: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* Bottom borders (end of each 3x3 box vertically) */ -#sudoku .sudoku-cell[data-r="2"], -#sudoku .sudoku-cell[data-r="5"], -#sudoku .sudoku-cell[data-r="8"] { - border-bottom: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* Top borders (start of each 3x3 box vertically) */ -#sudoku .sudoku-cell[data-r="0"], -#sudoku .sudoku-cell[data-r="3"], -#sudoku .sudoku-cell[data-r="6"] { - border-top: 2px solid color-mix(in srgb, var(--primary, #82aaff) 30%, transparent); -} - -/* ========================================================= - User-Entered Cells - ========================================================= */ -#sudoku .sudoku-cell.user-entered { - color: var(--primary, #82aaff); - font-weight: 600; -} - -/* ========================================================= - Active Cell Highlight (Navigation Mode) - ========================================================= */ -#sudoku .sudoku-cell.active { - outline: 2px solid var(--primary, #82aaff); - outline-offset: -2px; - background: color-mix(in srgb, var(--primary, #82aaff) 20%, var(--muted, #2f334d)); - box-shadow: - 0 0 8px color-mix(in srgb, var(--primary, #82aaff) 40%, transparent), - 0 0 12px color-mix(in srgb, var(--primary, #82aaff) 20%, transparent), - inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 15%, transparent); -} - -/* ========================================================= - Editing Cell Highlight (Edit Mode) - ========================================================= */ -#sudoku .sudoku-cell.editing { - outline: 2px solid var(--accent, #ffc777); - outline-offset: -2px; - background: color-mix(in srgb, var(--accent, #ffc777) 15%, var(--muted, #2f334d)); - box-shadow: - 0 0 16px color-mix(in srgb, var(--accent, #ffc777) 70%, transparent), - 0 0 24px color-mix(in srgb, var(--accent, #ffc777) 40%, transparent), - inset 0 0 12px color-mix(in srgb, var(--accent, #ffc777) 30%, transparent); - animation: editing-cell-glow 1.5s ease-in-out infinite; -} - -@keyframes editing-cell-glow { - 0%, 100% { - box-shadow: - 0 0 16px color-mix(in srgb, var(--accent, #ffc777) 70%, transparent), - 0 0 24px color-mix(in srgb, var(--accent, #ffc777) 40%, transparent), - inset 0 0 12px color-mix(in srgb, var(--accent, #ffc777) 30%, transparent); - } - 50% { - box-shadow: - 0 0 20px color-mix(in srgb, var(--accent, #ffc777) 90%, transparent), - 0 0 32px color-mix(in srgb, var(--accent, #ffc777) 60%, transparent), - inset 0 0 16px color-mix(in srgb, var(--accent, #ffc777) 40%, transparent); - } -} - -/* ========================================================= - Visual Feedback Effects - ========================================================= */ -/* Invalid move effect */ -#sudoku .sudoku-cell.invalid-move { - animation: sudoku-invalid-pulse 180ms ease-out; - box-shadow: - 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 85%, transparent), - 0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 45%, transparent); -} - -@keyframes sudoku-invalid-pulse { - 0% { - transform: scale(1.05); - box-shadow: - 0 0 16px color-mix(in srgb, var(--destructive, #c53b53) 95%, transparent), - 0 0 28px color-mix(in srgb, var(--destructive, #c53b53) 55%, transparent); - } - 100% { - transform: none; - box-shadow: none; - } -} - -/* Locked cell effect (pre-filled clues) */ -#sudoku .sudoku-cell.locked-cell { - animation: sudoku-locked-pulse 300ms ease-out; -} - -@keyframes sudoku-locked-pulse { - 0% { - transform: scale(1.08); - background: rgba(255, 85, 85, 0.3); - box-shadow: - 0 0 20px rgba(255, 85, 85, 0.9), - 0 0 32px rgba(255, 85, 85, 0.6), - inset 0 0 16px rgba(255, 85, 85, 0.4); - } - 50% { - transform: scale(1.05); - background: rgba(255, 85, 85, 0.2); - box-shadow: - 0 0 16px rgba(255, 85, 85, 0.8), - 0 0 28px rgba(255, 85, 85, 0.5), - inset 0 0 12px rgba(255, 85, 85, 0.3); - } - 100% { - transform: none; - background: transparent; - box-shadow: none; - } -} - -/* Trail effect for moves */ -#sudoku .sudoku-cell.trail { - box-shadow: - inset 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 60%, transparent), - 0 0 6px color-mix(in srgb, var(--primary, #82aaff) 35%, transparent); - animation: sudoku-trail-fade 220ms ease-out; -} - -@keyframes sudoku-trail-fade { - from { background-color: color-mix(in srgb, var(--primary, #82aaff) 25%, transparent); } - to { background-color: transparent; } -} - -/* Valid move flash */ -#sudoku .sudoku-cell.valid-move { - animation: sudoku-valid-flash 300ms ease-out; -} - -@keyframes sudoku-valid-flash { - 0% { - background: color-mix(in srgb, var(--success, #4fd6be) 40%, transparent); - box-shadow: - 0 0 12px color-mix(in srgb, var(--success, #4fd6be) 80%, transparent), - 0 0 20px color-mix(in srgb, var(--success, #4fd6be) 50%, transparent), - inset 0 0 8px color-mix(in srgb, var(--success, #4fd6be) 35%, transparent); - } - 100% { - background: transparent; - box-shadow: none; - } -} - -/* Conflict highlighting */ -#sudoku .sudoku-cell.conflict { - outline: 2px solid var(--destructive, #c53b53); - outline-offset: -2px; - background: color-mix(in srgb, var(--destructive, #c53b53) 25%, var(--muted, #2f334d)); - box-shadow: - 0 0 10px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent), - 0 0 18px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent), - inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 30%, transparent); - animation: sudoku-conflict-pulse 1s ease-in-out infinite; -} - -@keyframes sudoku-conflict-pulse { - 0%, 100% { - box-shadow: - 0 0 10px color-mix(in srgb, var(--destructive, #c53b53) 70%, transparent), - 0 0 18px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent), - inset 0 0 8px color-mix(in srgb, var(--destructive, #c53b53) 30%, transparent); - } - 50% { - box-shadow: - 0 0 14px color-mix(in srgb, var(--destructive, #c53b53) 85%, transparent), - 0 0 24px color-mix(in srgb, var(--destructive, #c53b53) 55%, transparent), - inset 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent); - } -} diff --git a/src_old/index.ts b/src_old/index.ts deleted file mode 100644 index 891c6f4..0000000 --- a/src_old/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './types'; -export * from "./useGame"; diff --git a/src_old/lib/Sudoku.ts b/src_old/lib/Sudoku.ts deleted file mode 100644 index 8c1f7ee..0000000 --- a/src_old/lib/Sudoku.ts +++ /dev/null @@ -1,372 +0,0 @@ -// src/lib/Sudoku.ts -// Direct TypeScript + ES module version of Robatron's sudoku.js -// https://github.com/robatron/sudoku.js -// No algorithmic changes – only syntax, structure, and typing updates. - -export type DifficultyName = - | "easy" - | "medium" - | "hard" - | "very-hard" - | "insane" - | "inhuman"; - -export type Difficulty = DifficultyName | number; - -const sudoku: any = {}; - -sudoku.DIGITS = "123456789"; -const ROWS = "ABCDEFGHI"; -const COLS = sudoku.DIGITS; -let SQUARES: string[] = []; - -let UNITS: string[][] = []; -let SQUARE_UNITS_MAP: Record = {}; -let SQUARE_PEERS_MAP: Record = {}; - -const MIN_GIVENS = 17; -const NR_SQUARES = 81; - -const DIFFICULTY: Record = { - easy: 62, - medium: 53, - hard: 44, - "very-hard": 35, - insane: 26, - inhuman: 17, -}; - -sudoku.BLANK_CHAR = "."; -sudoku.BLANK_BOARD = sudoku.BLANK_CHAR.repeat(NR_SQUARES); - -function initialize() { - SQUARES = sudoku._cross(ROWS, COLS); - UNITS = sudoku._get_all_units(ROWS, COLS); - SQUARE_UNITS_MAP = sudoku._get_square_units_map(SQUARES, UNITS); - SQUARE_PEERS_MAP = sudoku._get_square_peers_map(SQUARES, SQUARE_UNITS_MAP); -} - -// --------------------------------------------------------------------------- -// Generate -// --------------------------------------------------------------------------- - -sudoku.generate = function (difficulty: Difficulty, unique?: boolean): string { - let numericDifficulty: number; - if (typeof difficulty === "string" || typeof difficulty === "undefined") { - numericDifficulty = DIFFICULTY[difficulty as DifficultyName] || DIFFICULTY.easy; - } else { - numericDifficulty = difficulty; - } - - numericDifficulty = sudoku._force_range(numericDifficulty, NR_SQUARES + 1, MIN_GIVENS); - unique = unique ?? true; - - const blank_board = sudoku.BLANK_BOARD; - let candidates = sudoku._get_candidates_map(blank_board); - - const shuffled_squares = sudoku._shuffle(SQUARES); - - for (const square of shuffled_squares) { - const rand_candidate_idx = sudoku._rand_range(candidates[square].length); - const rand_candidate = candidates[square][rand_candidate_idx]; - if (!sudoku._assign(candidates, square, rand_candidate)) break; - - const single_candidates: string[] = []; - for (const s of SQUARES) { - if (candidates[s].length === 1) single_candidates.push(candidates[s]); - } - - if ( - single_candidates.length >= numericDifficulty && - sudoku._strip_dups(single_candidates).length >= 8 - ) { - let board = ""; - let givens_idxs: number[] = []; - - for (let i = 0; i < SQUARES.length; i++) { - const s = SQUARES[i]; - if (candidates[s].length === 1) { - board += candidates[s]; - givens_idxs.push(i); - } else { - board += sudoku.BLANK_CHAR; - } - } - - if (givens_idxs.length > numericDifficulty) { - givens_idxs = sudoku._shuffle(givens_idxs); - for (let i = 0; i < givens_idxs.length - numericDifficulty; ++i) { - const target = givens_idxs[i]; - board = - board.substring(0, target) + - sudoku.BLANK_CHAR + - board.substring(target + 1); - } - } - - if (sudoku.solve(board)) return board; - } - } - - return sudoku.generate(numericDifficulty); -}; - -// --------------------------------------------------------------------------- -// Solve -// --------------------------------------------------------------------------- - -sudoku.solve = function (board: string, reverse?: boolean): string | false { - const report = sudoku.validate_board(board); - if (report !== true) throw report; - - let nr_givens = 0; - for (const ch of board) { - if (ch !== sudoku.BLANK_CHAR && sudoku._in(ch, sudoku.DIGITS)) nr_givens++; - } - if (nr_givens < MIN_GIVENS) throw "Too few givens"; - - reverse = reverse || false; - const candidates = sudoku._get_candidates_map(board); - const result = sudoku._search(candidates, reverse); - - if (result) { - let solution = ""; - for (const s of SQUARES) solution += result[s]; - return solution; - } - return false; -}; - -// --------------------------------------------------------------------------- -// Get candidates -// --------------------------------------------------------------------------- - -sudoku.get_candidates = function (board: string): string[][] | false { - const report = sudoku.validate_board(board); - if (report !== true) throw report; - - const map = sudoku._get_candidates_map(board); - if (!map) return false; - - const rows: string[][] = []; - let cur_row: string[] = []; - let i = 0; - for (const square in map) { - cur_row.push(map[square]); - if (i % 9 === 8) { - rows.push(cur_row); - cur_row = []; - } - ++i; - } - return rows; -}; - -sudoku._get_candidates_map = function (board: string): Record | false { - const report = sudoku.validate_board(board); - if (report !== true) throw report; - - const map: Record = {}; - const squares_values = sudoku._get_square_vals_map(board); - - for (const s of SQUARES) map[s] = sudoku.DIGITS; - - for (const s in squares_values) { - const val = squares_values[s]; - if (sudoku._in(val, sudoku.DIGITS)) { - const new_candidates = sudoku._assign(map, s, val); - if (!new_candidates) return false; - } - } - return map; -}; - -// --------------------------------------------------------------------------- -// Search -// --------------------------------------------------------------------------- - -sudoku._search = function (candidates: Record, reverse: boolean): any { - if (!candidates) return false; - reverse = reverse || false; - - let max_nr_candidates = 0; - for (const s of SQUARES) { - const n = candidates[s].length; - if (n > max_nr_candidates) { - max_nr_candidates = n; - } - } - if (max_nr_candidates === 1) return candidates; - - let min_nr_candidates = 10; - let min_square = ""; - for (const s of SQUARES) { - const n = candidates[s].length; - if (n < min_nr_candidates && n > 1) { - min_nr_candidates = n; - min_square = s; - } - } - - const min_candidates = candidates[min_square]; - if (!reverse) { - for (const v of min_candidates) { - const copy = JSON.parse(JSON.stringify(candidates)); - const next = sudoku._search(sudoku._assign(copy, min_square, v), reverse); - if (next) return next; - } - } else { - for (let i = min_candidates.length - 1; i >= 0; --i) { - const v = min_candidates[i]; - const copy = JSON.parse(JSON.stringify(candidates)); - const next = sudoku._search(sudoku._assign(copy, min_square, v), reverse); - if (next) return next; - } - } - return false; -}; - -// --------------------------------------------------------------------------- -// Assignment / elimination logic -// --------------------------------------------------------------------------- - -sudoku._assign = function (candidates: Record, square: string, val: string) { - const others = candidates[square].replace(val, ""); - for (const v of others) { - const next = sudoku._eliminate(candidates, square, v); - if (!next) return false; - } - return candidates; -}; - -sudoku._eliminate = function (candidates: Record, square: string, val: string) { - if (!sudoku._in(val, candidates[square])) return candidates; - - candidates[square] = candidates[square].replace(val, ""); - const n = candidates[square].length; - - if (n === 1) { - const target_val = candidates[square]; - for (const peer of SQUARE_PEERS_MAP[square]) { - const next = sudoku._eliminate(candidates, peer, target_val); - if (!next) return false; - } - } else if (n === 0) return false; - - for (const unit of SQUARE_UNITS_MAP[square]) { - const places: string[] = []; - for (const sq of unit) { - if (sudoku._in(val, candidates[sq])) places.push(sq); - } - if (places.length === 0) return false; - else if (places.length === 1) { - const next = sudoku._assign(candidates, places[0], val); - if (!next) return false; - } - } - return candidates; -}; - -// --------------------------------------------------------------------------- -// Data structures -// --------------------------------------------------------------------------- - -sudoku._get_square_vals_map = function (board: string) { - const map: Record = {}; - if (board.length !== SQUARES.length) throw "Board length mismatch"; - for (let i = 0; i < SQUARES.length; i++) map[SQUARES[i]] = board[i]; - return map; -}; - -sudoku._get_square_units_map = function (squares: string[], units: string[][]) { - const map: Record = {}; - for (const s of squares) { - const list: string[][] = []; - for (const u of units) if (u.includes(s)) list.push(u); - map[s] = list; - } - return map; -}; - -sudoku._get_square_peers_map = function ( - squares: string[], - units_map: Record -) { - const map: Record = {}; - for (const s of squares) { - const peers: string[] = []; - for (const unit of units_map[s]) { - for (const sq of unit) { - if (sq !== s && !peers.includes(sq)) peers.push(sq); - } - } - map[s] = peers; - } - return map; -}; - -sudoku._get_all_units = function (rows: string, cols: string) { - const units: string[][] = []; - for (const r of rows) units.push(sudoku._cross(r, cols)); - for (const c of cols) units.push(sudoku._cross(rows, c)); - const row_blocks = ["ABC", "DEF", "GHI"]; - const col_blocks = ["123", "456", "789"]; - for (const rb of row_blocks) - for (const cb of col_blocks) - units.push(sudoku._cross(rb, cb)); - return units; -}; - -// --------------------------------------------------------------------------- -// Utils -// --------------------------------------------------------------------------- - -sudoku.validate_board = function (board: string): true | string { - if (!board) return "Empty board"; - if (board.length !== NR_SQUARES) return "Invalid board size"; - for (let i = 0; i < board.length; i++) { - const c = board[i]; - if (!sudoku._in(c, sudoku.DIGITS) && c !== sudoku.BLANK_CHAR) - return `Invalid char at ${i}: ${c}`; - } - return true; -}; - -sudoku._cross = function (a: string, b: string) { - const res: string[] = []; - for (const ai of a) for (const bi of b) res.push(ai + bi); - return res; -}; - -sudoku._in = function (v: string, seq: string) { - return seq.indexOf(v) !== -1; -}; - -sudoku._shuffle = function (seq: T[]): T[] { - const arr = [...seq]; - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [arr[i], arr[j]] = [arr[j], arr[i]]; - } - return arr; -}; - -sudoku._rand_range = function (max: number, min = 0) { - return Math.floor(Math.random() * (max - min)) + min; -}; - -sudoku._strip_dups = function (seq: T[]): T[] { - return Array.from(new Set(seq)); -}; - -sudoku._force_range = function (nr: number, max: number, min = 0) { - return Math.min(Math.max(nr || 0, min), max); -}; - -// --------------------------------------------------------------------------- -// Init -// --------------------------------------------------------------------------- -initialize(); - -export { sudoku }; - diff --git a/src_old/sudoku/index.ts b/src_old/sudoku/index.ts deleted file mode 100644 index 947c3b1..0000000 --- a/src_old/sudoku/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './sudoku'; -export * from './validate-board'; diff --git a/src_old/sudoku/make-puzzle.ts b/src_old/sudoku/make-puzzle.ts deleted file mode 100644 index 63a4248..0000000 --- a/src_old/sudoku/make-puzzle.ts +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Create a playable Sudoku puzzle by removing random digits - * from a fully solved board string. - * - * Difficulty only determines how many cells are removed. - * - "easy": removes ~40 cells - * - "medium": removes ~50 cells - * - "hard": removes ~60 cells - * - * The returned string uses '.' for blanks. - */ -export function makePuzzle( - solution: string, - difficulty: "easy" | "medium" | "hard" = "medium" -): string { - if (solution.length !== 81 || !/^[1-9]+$/.test(solution)) { - throw new Error("Invalid solved board: must be 81 digits 1–9"); - } - - // --- removal count based on difficulty --- - const removeCount = { - easy: 40, - medium: 50, - hard: 60, - }[difficulty]; - - // --- create a mutable copy --- - const board = solution.split(""); - - // --- build a shuffled list of indices 0–80 --- - const indices = Array.from({ length: 81 }, (_, i) => i); - for (let i = indices.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [indices[i], indices[j]] = [indices[j], indices[i]]; - } - - // --- remove the desired number of cells --- - for (let i = 0; i < removeCount; i++) { - const idx = indices[i]; - board[idx] = "."; - } - - return board.join(""); -} - diff --git a/src_old/sudoku/sudoku.test.ts b/src_old/sudoku/sudoku.test.ts deleted file mode 100644 index 4c86286..0000000 --- a/src_old/sudoku/sudoku.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, test, expect } from "bun:test"; -import { generateSolvedBoard } from "./sudoku"; -import { assertValidFullBoard } from "./validate-board"; - - -const valids = [ - "534678912672195348198342567859761423426853791713924856961537284287419635345286179", - "456789123123456789789123456567891234234567891891234567678912345345678912912345678", - "234567891567891234891234567345678912678912345912345678456789123789123456123456789", - "987654321654321987321987654876543219543219876219876543765432198432198765198765432" -]; - -const invalids = [ - "534678912672195348198342567859761423426853791713924856961537284287419635345286171", - "534678911672195348198342567859761423426853791713924856961537284287419635345286179", - "534678912572195348198342567859761423426853791713924856961537284287419635345286179", - "534678912672195348198342567859761423426863791713924856961537284287419635345286179", -]; - -describe("Testing the Asserter with static boards", () => { - test("valid boards", () => { - for (let board of valids) { - expect(() => assertValidFullBoard(board)).not.toThrow(); - } - }) - - test("invalid boards", () => { - for (let board of invalids) { - expect(() => assertValidFullBoard(board)).toThrow() - } - }) -}); - -describe("Testing the generateSolvedBoard", () => { - test("generates 10 valid boards", () => { - for (let i = 0; i < 10; i++) { - const generated = generateSolvedBoard(); - assertValidFullBoard(generated); - }; - }); - test("generates 100 valid boards", () => { - for (let i = 0; i < 100; i++) { - const generated = generateSolvedBoard(); - assertValidFullBoard(generated); - }; - }); - - test("generates 1000 valid boards", () => { - for (let i = 0; i < 1000; i++) { - const generated = generateSolvedBoard(); - assertValidFullBoard(generated); - }; - }); -}); - - - diff --git a/src_old/sudoku/sudoku.ts b/src_old/sudoku/sudoku.ts deleted file mode 100644 index cf776b3..0000000 --- a/src_old/sudoku/sudoku.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Generate a fully valid Sudoku solution using a base pattern and structural shuffles. - * Returns a string of 81 digits (row-major order). - */ -export function generateSolvedBoard(): string { - const size = 9; - const box = 3; - - // --- 1️⃣ Base pattern function --- - const pattern = (r: number, c: number) => - (box * (r % box) + Math.floor(r / box) + c) % size; - - // --- 2️⃣ Create base board --- - const baseBoard: number[][] = Array.from({ length: size }, (_, r) => - Array.from({ length: size }, (_, c) => pattern(r, c) + 1) - ); - - // --- 3️⃣ Helper to shuffle an array in place --- - const shuffle = (arr: T[]): T[] => { - const copy = [...arr]; - for (let i = copy.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [copy[i], copy[j]] = [copy[j], copy[i]]; - } - return copy; - }; - - // --- 4️⃣ Build shuffled row/col indices preserving 3×3 structure --- - const rows = shuffle([0, 1, 2]) - .concat(shuffle([3, 4, 5])) - .concat(shuffle([6, 7, 8])); - const cols = shuffle([0, 1, 2]) - .concat(shuffle([3, 4, 5])) - .concat(shuffle([6, 7, 8])); - - // --- 5️⃣ Shuffle entire bands and stacks (groups of 3) --- - const rowBands = shuffle([0, 1, 2]); - const colStacks = shuffle([0, 1, 2]); - - const shuffledRows = rowBands.flatMap(band => rows.slice(band * 3, band * 3 + 3)); - const shuffledCols = colStacks.flatMap(stack => cols.slice(stack * 3, stack * 3 + 3)); - - // --- 6️⃣ Digit permutation (1–9) --- - const digits = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9]); - - // --- 7️⃣ Build the final shuffled board --- - const board: number[][] = shuffledRows.map(r => - shuffledCols.map(c => digits[baseBoard[r][c] - 1]) - ); - - // --- 8️⃣ Flatten to 81-char string --- - return board.flat().join(''); -} - diff --git a/src_old/sudoku/validate-board.ts b/src_old/sudoku/validate-board.ts deleted file mode 100644 index 3d638ce..0000000 --- a/src_old/sudoku/validate-board.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Validate a fully filled Sudoku board (81-char string) - * Returns true if: - * - 81 characters long - * - all characters are digits 1–9 - * - all rows, columns, and 3×3 boxes sum to 45 - */ -export function validateFullBoard(board: string): boolean { - // --- Basic sanity --- - if (board.length !== 81) return false; - if (!/^[1-9]+$/.test(board)) return false; - - const getCell = (r: number, c: number) => Number(board[r * 9 + c]); - - // --- Validate rows --- - for (let r = 0; r < 9; r++) { - const sum = Array.from({ length: 9 }, (_, c) => getCell(r, c)) - .reduce((a, b) => a + b, 0); - if (sum !== 45) return false; - } - - // --- Validate columns --- - for (let c = 0; c < 9; c++) { - const sum = Array.from({ length: 9 }, (_, r) => getCell(r, c)) - .reduce((a, b) => a + b, 0); - if (sum !== 45) return false; - } - - // --- Validate 3×3 boxes --- - for (let br = 0; br < 3; br++) { - for (let bc = 0; bc < 3; bc++) { - const sum = Array.from({ length: 9 }, (_, i) => { - const r = br * 3 + Math.floor(i / 3); - const c = bc * 3 + (i % 3); - return getCell(r, c); - }).reduce((a, b) => a + b, 0); - if (sum !== 45) return false; - } - } - - return true; -} -/** - * 🔍 Assert that a Sudoku board string represents a valid, fully filled grid. - * - * Format: - * - 81 characters - * - Only digits 1–9 - * - Each row, column, and 3×3 box contains unique digits that sum to 45 - * - * Throws descriptive errors on failure. - */ - -export function assertValidFullBoard(board: string): void { - // --- Basic integrity checks --- - console.assert(board.length === 81, `❌ Board length must be 81, got ${board.length}`); - if (board.length !== 81) throw new Error(`Invalid board length: ${board.length}`); - - // All characters must be digits 1–9 - console.assert(/^[1-9]+$/.test(board), "❌ Board must contain only digits 1–9 (no 0s or dots)"); - if (!/^[1-9]+$/.test(board)) throw new Error("Board contains invalid characters"); - - const get = (r: number, c: number) => Number(board[r * 9 + c]); - - // --- Helper to assert uniqueness and sum --- - const assertGroup = (label: string, cells: number[]) => { - const sum = cells.reduce((a, b) => a + b, 0); - const unique = new Set(cells); - console.assert(sum === 45, `❌ ${label} sum=${sum} (should be 45)`); - console.assert(unique.size === 9, `❌ ${label} has duplicates: [${cells.join(",")}]`); - - if (sum !== 45) throw new Error(`${label} does not sum to 45`); - if (unique.size !== 9) throw new Error(`${label} contains duplicates`); - }; - - // --- Validate rows --- - for (let r = 0; r < 9; r++) { - const row = Array.from({ length: 9 }, (_, c) => get(r, c)); - assertGroup(`Row ${r + 1}`, row); - } - - // --- Validate columns --- - for (let c = 0; c < 9; c++) { - const col = Array.from({ length: 9 }, (_, r) => get(r, c)); - assertGroup(`Column ${c + 1}`, col); - } - - // --- Validate 3×3 boxes --- - for (let br = 0; br < 3; br++) { - for (let bc = 0; bc < 3; bc++) { - const box: number[] = []; - for (let r = 0; r < 3; r++) { - for (let c = 0; c < 3; c++) { - box.push(get(br * 3 + r, bc * 3 + c)); - } - } - const idx = br * 3 + bc + 1; - assertGroup(`Box ${idx}`, box); - } - } -} - - - diff --git a/src_old/types.ts b/src_old/types.ts deleted file mode 100644 index d09f67c..0000000 --- a/src_old/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { useGame } from "./useGame"; -import { useScore } from "./useScore"; - -export type UseGameType = ReturnType; -export type UseScoreType = ReturnType; - -export type { - Difficulty, - DifficultyName, -} from "./lib/Sudoku"; - -export type GamePhase = "idle" | "playing"; -export type PlayStatus = "started" | "game-over" | "game-won"; -export type GameStatus = "waiting" | "started" | "game-over" | "game-won"; - -export const isPlaying = (status: GameStatus): status is PlayStatus => - status === "started" || status === "game-over" || status === "game-won"; - -export const getGamePhase = (status: GameStatus): GamePhase => - status === "waiting" ? "idle" : "playing"; diff --git a/src_old/useGame/index.ts b/src_old/useGame/index.ts deleted file mode 100644 index 1163755..0000000 --- a/src_old/useGame/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useGame'; diff --git a/src_old/useGame/useBoardState.ts b/src_old/useGame/useBoardState.ts deleted file mode 100644 index eecce25..0000000 --- a/src_old/useGame/useBoardState.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { useState } from "react"; -import { sudoku } from "../lib/Sudoku"; -import type { UseCursorType } from "./useCursor"; - -type UseBoardStateOptions = { - validateMoves?: boolean; -}; - -export function useBoardState(cursorManager: UseCursorType, options?: UseBoardStateOptions) { - const { cursor, setCursor } = cursorManager; - const { validateMoves = false } = options || {}; - const [board, setBoard] = useState(sudoku.generate("medium")); - const [initialBoard, setInitialBoard] = useState(board); - const [solution, setSolution] = useState(null); - - const givenCells = () => { - const cells = new Set(); - for (let i = 0; i < initialBoard.length; i++) { - if (initialBoard[i] !== ".") { - cells.add(i); - } - } - return cells; - }; - - const canEditCell = (row: number, col: number): boolean => { - const index = row * 9 + col; - return !givenCells().has(index); - }; - - const isValidPlacement = (testBoard: string, row: number, col: number, digit: string): boolean => { - if (digit === ".") return true; - - for (let c = 0; c < 9; c++) { - const checkIdx = row * 9 + c; - if (c !== col && testBoard[checkIdx] === digit) { - return false; - } - } - - for (let r = 0; r < 9; r++) { - const checkIdx = r * 9 + col; - if (r !== row && testBoard[checkIdx] === digit) { - return false; - } - } - - const boxRow = Math.floor(row / 3) * 3; - const boxCol = Math.floor(col / 3) * 3; - for (let r = boxRow; r < boxRow + 3; r++) { - for (let c = boxCol; c < boxCol + 3; c++) { - const checkIdx = r * 9 + c; - if ((r !== row || c !== col) && testBoard[checkIdx] === digit) { - return false; - } - } - } - - return true; - }; - - const getConflicts = (testBoard: string, row: number, col: number): number[] => { - const conflicts: number[] = []; - const digit = testBoard[row * 9 + col]; - - if (digit === ".") return conflicts; - - for (let c = 0; c < 9; c++) { - if (c !== col && testBoard[row * 9 + c] === digit) { - conflicts.push(row * 9 + c); - } - } - - for (let r = 0; r < 9; r++) { - if (r !== row && testBoard[r * 9 + col] === digit) { - conflicts.push(r * 9 + col); - } - } - - const boxRow = Math.floor(row / 3) * 3; - const boxCol = Math.floor(col / 3) * 3; - for (let r = boxRow; r < boxRow + 3; r++) { - for (let c = boxCol; c < boxCol + 3; c++) { - if ((r !== row || c !== col) && testBoard[r * 9 + c] === digit) { - const idx = r * 9 + c; - if (!conflicts.includes(idx)) { - conflicts.push(idx); - } - } - } - } - - return conflicts; - }; - - const isFullyValid = (testBoard: string): boolean => { - for (let r = 0; r < 9; r++) { - let rowSum = 0; - for (let c = 0; c < 9; c++) { - const digit = testBoard[r * 9 + c]; - if (digit === ".") return false; - rowSum += parseInt(digit, 10); - } - if (rowSum !== 45) return false; - } - - for (let c = 0; c < 9; c++) { - let colSum = 0; - for (let r = 0; r < 9; r++) { - const digit = testBoard[r * 9 + c]; - colSum += parseInt(digit, 10); - } - if (colSum !== 45) return false; - } - - for (let boxRow = 0; boxRow < 3; boxRow++) { - for (let boxCol = 0; boxCol < 3; boxCol++) { - let boxSum = 0; - for (let r = boxRow * 3; r < boxRow * 3 + 3; r++) { - for (let c = boxCol * 3; c < boxCol * 3 + 3; c++) { - const digit = testBoard[r * 9 + c]; - boxSum += parseInt(digit, 10); - } - } - if (boxSum !== 45) return false; - } - } - - return true; - }; - - const isComplete = (testBoard: string): boolean => { - return !testBoard.includes("."); - }; - - const setDigit = (d: string) => { - const cellIndex = cursor.r * 9 + cursor.c; - - if (!canEditCell(cursor.r, cursor.c)) { - window.dispatchEvent(new CustomEvent("sudoku-locked-cell")); - return false; - } - - const arr = board.split(""); - arr[cellIndex] = d; - const newBoard = arr.join(""); - - if (validateMoves && !isValidPlacement(newBoard, cursor.r, cursor.c, d)) { - const conflicts = getConflicts(newBoard, cursor.r, cursor.c); - window.dispatchEvent( - new CustomEvent("sudoku-invalid-move", { - detail: { row: cursor.r, col: cursor.c, digit: d, conflicts }, - }) - ); - return false; - } - - setBoard(newBoard); - if (validateMoves) { - window.dispatchEvent( - new CustomEvent("sudoku-valid-move", { - detail: { row: cursor.r, col: cursor.c, digit: d }, - }) - ); - } - return true; - }; - - const erase = () => { - if (!canEditCell(cursor.r, cursor.c)) { - window.dispatchEvent(new CustomEvent("sudoku-locked-cell")); - return false; - } - - const i = cursor.r * 9 + cursor.c; - const arr = board.split(""); - arr[i] = "."; - setBoard(arr.join("")); - return true; - }; - - const reset = () => { - const newBoard = sudoku.generate("easy"); - setBoard(newBoard); - setInitialBoard(newBoard); - setSolution(null); - setCursor({ r: 0, c: 0 }); - }; - - const solveBoard = () => { - const solved = sudoku.solve(initialBoard); - if (solved) { - setSolution(solved); - setBoard(solved); - window.dispatchEvent(new CustomEvent("sudoku-game-won")); - } - }; - - const boardToString = (human: boolean = false) => { - // Computer-optimized: compact 81-character string - if (!human) { - return board; - } - - // Human-readable: formatted grid with 3x3 box separators - let result = ""; - for (let r = 0; r < 9; r++) { - for (let c = 0; c < 9; c++) { - const idx = r * 9 + c; - result += board[idx] === "." ? "." : board[idx]; - if (c === 2 || c === 5) result += " | "; - else if (c < 8) result += " "; - } - result += "\n"; - if (r === 2 || r === 5) result += "------+-------+------\n"; - } - return result; - }; - - return { - board, - initialBoard, - solution, - setDigit, - erase, - reset, - solveBoard, - boardToString, - canEditCell, - isValidPlacement, - getConflicts, - isFullyValid, - isComplete, - givenCells, - }; -} - -export type UseBoardStateType = ReturnType; diff --git a/src_old/useGame/useCursor.ts b/src_old/useGame/useCursor.ts deleted file mode 100644 index 230e508..0000000 --- a/src_old/useGame/useCursor.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { useRef, useState, useCallback, useEffect } from "react"; -import type { UseRenderer } from "./useSudokuRenderer"; - -export function useCursor(rendererManager: UseRenderer) { - const { containerRef } = rendererManager; - const [cursor, setCursor] = useState<{ r: number; c: number }>({ r: 0, c: 0 }); - - const animatingRef = useRef(false); - - const getCellEl = useCallback((r: number, c: number): HTMLElement | null => { - const container = containerRef.current; - if (!container) return null; - const sudokuDiv = container.querySelector("#sudoku"); - if (!sudokuDiv) return null; - const selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`; - return sudokuDiv.querySelector(selector) as HTMLElement | null; - }, [containerRef]); - - const flashInvalid = useCallback(() => { - const el = getCellEl(cursor.r, cursor.c); - if (!el) return; - el.classList.add("invalid-move"); - setTimeout(() => el.classList.remove("invalid-move"), 180); - }, [cursor, getCellEl]); - - const flashLocked = useCallback(() => { - const el = getCellEl(cursor.r, cursor.c); - if (!el) return; - el.classList.add("locked-cell"); - setTimeout(() => el.classList.remove("locked-cell"), 300); - }, [cursor, getCellEl]); - - const flashValid = useCallback(() => { - const el = getCellEl(cursor.r, cursor.c); - if (!el) return; - el.classList.add("valid-move"); - setTimeout(() => el.classList.remove("valid-move"), 300); - }, [cursor, getCellEl]); - - const highlightConflicts = useCallback((conflictIndices: number[]) => { - const container = containerRef.current; - if (!container) return; - const sudokuDiv = container.querySelector("#sudoku"); - if (!sudokuDiv) return; - - conflictIndices.forEach((idx) => { - const r = Math.floor(idx / 9); - const c = idx % 9; - const el = getCellEl(r, c); - if (el) { - el.classList.add("conflict"); - } - }); - - setTimeout(() => { - conflictIndices.forEach((idx) => { - const r = Math.floor(idx / 9); - const c = idx % 9; - const el = getCellEl(r, c); - if (el) { - el.classList.remove("conflict"); - } - }); - }, 2000); - }, [containerRef, getCellEl]); - - const moveCursor = useCallback( - (dr: number, dc: number, steps = 1) => { - if (animatingRef.current) return; - - setCursor((prev) => { - const targetR = prev.r + (dr * steps); - const targetC = prev.c + (dc * steps); - - // Check if the full counted move would go out of bounds - if (targetR < 0 || targetR > 8 || targetC < 0 || targetC > 8) { - // Flash invalid and don't move - setTimeout(() => flashInvalid(), 0); - return prev; - } - - // Valid move - apply it - return { r: targetR, c: targetC }; - }); - }, - [flashInvalid] - ); - - useEffect(() => { - const onLockedCell = () => flashLocked(); - const onValidMove = () => flashValid(); - const onInvalidMove = (e: Event) => { - const detail = (e as CustomEvent).detail; - if (detail?.conflicts) { - highlightConflicts(detail.conflicts); - } - flashInvalid(); - }; - - window.addEventListener("sudoku-locked-cell", onLockedCell); - window.addEventListener("sudoku-valid-move", onValidMove); - window.addEventListener("sudoku-invalid-move", onInvalidMove as EventListener); - - return () => { - window.removeEventListener("sudoku-locked-cell", onLockedCell); - window.removeEventListener("sudoku-valid-move", onValidMove); - window.removeEventListener("sudoku-invalid-move", onInvalidMove as EventListener); - }; - }, [flashLocked, flashValid, flashInvalid, highlightConflicts]); - - return { - cursor, - setCursor, - moveCursor, - getCellEl, - flashInvalid, - flashValid, - highlightConflicts, - }; -} - -export type UseCursorType = ReturnType; diff --git a/src_old/useGame/useGame.ts b/src_old/useGame/useGame.ts deleted file mode 100644 index 4150bb8..0000000 --- a/src_old/useGame/useGame.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { useEffect } from "react"; -import { useRenderer } from "./useSudokuRenderer"; -import { useCursor } from "./useCursor"; -import { useGameStatus } from "./useGameStatus"; -import { useBoardState } from "./useBoardState"; -import { useKeyBindings } from "./useKeyBindings"; -import { useScore } from "../useScore"; - -export type UseGameOptions = { - validateMoves?: boolean; -}; - -export function useGame(platformHook?: unknown, options?: UseGameOptions) { - const { validateMoves = false } = options || {}; - const rendererManager = useRenderer(); - const { containerRef, renderEmptyBoard, renderBoard, highlightCursor } = rendererManager; - const cursorManager = useCursor(rendererManager); - const { cursor, moveCursor } = cursorManager; - const gameManager = useGameStatus(rendererManager, cursorManager); - const { gameStatus, setGameStatus, startGame, stopGame } = gameManager; - const boardManager = useBoardState(cursorManager, { validateMoves }); - const { - board, - initialBoard, - solution, - setDigit, - erase, - reset, - solveBoard, - isComplete, - isFullyValid, - } = boardManager; - - // --------------------------- - // Keybindings - // --------------------------- - const keyBindings = useKeyBindings({ - moveCursor, - setDigit, - erase, - stopGame, - cursor, - board, - initialBoard, - }); - - const { editMode, keyLog } = keyBindings; - - // --------------------------- - // Scoring (needs keyLog from keyBindings) - // --------------------------- - const scoreManager = useScore({ - gameContext: { gameStatus, setGameStatus }, - keystrokeCount: keyLog.length, - board, - isComplete, - isFullyValid, - initialBoard, - }); - - // --------------------------- - // Rendering - // --------------------------- - useEffect(() => { - renderEmptyBoard(); - }, [renderEmptyBoard]); - - useEffect(() => { - if (gameStatus === "waiting") { - renderEmptyBoard(); - } else { - renderBoard(board, initialBoard); - highlightCursor(cursor.r, cursor.c, editMode === "edit"); - } - }, [gameStatus, board, initialBoard, cursor, editMode, renderEmptyBoard, renderBoard, highlightCursor]); - - // --------------------------- - // Game Control - // --------------------------- - const initGame = () => { - reset(); - startGame(); - }; - - // --------------------------- - // Full Game Manager - // --------------------------- - const fullGameManager = { - // core - containerRef, - gameManager, - gameStatus, - setGameStatus, - startGame, - stopGame, - initGame, - - // cursor - cursorManager, - cursor, - moveCursor, - - // board state - boardManager, - board, - solution, - setDigit, - erase, - reset, - solveBoard, - - // scoring - scoreManager, - - // keybindings (vim-maze compatible) - ...keyBindings, - }; - - // allow platform-level injection - if (typeof platformHook === "function") { - platformHook(fullGameManager); - } - - return fullGameManager; -} diff --git a/src_old/useGame/useGameStatus.ts b/src_old/useGame/useGameStatus.ts deleted file mode 100644 index b74720f..0000000 --- a/src_old/useGame/useGameStatus.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useCallback, useState, useMemo } from "react"; -import type { UseRenderer } from "./useSudokuRenderer"; -import type { UseCursorType } from "./useCursor"; -import type { GameStatus } from "../types"; -import { getGamePhase, isPlaying } from "../types"; - -export type { GameStatus } from "../types"; - -export function useGameStatus( - rendererManager: UseRenderer, - cursorManager: UseCursorType -) { - const { containerRef } = rendererManager; - const { setCursor } = cursorManager; - const [gameStatus, setGameStatus] = useState("waiting"); - - const gamePhase = useMemo(() => getGamePhase(gameStatus), [gameStatus]); - const playStatus = useMemo( - () => (isPlaying(gameStatus) ? gameStatus : null), - [gameStatus] - ); - - const startGame = useCallback(() => { - setGameStatus("started"); - }, []); - - const stopGame = useCallback(() => { - setCursor({ r: 0, c: 0 }); - setGameStatus("waiting"); - const container = containerRef.current; - container?.querySelectorAll(".active").forEach((el) => el.classList.remove("active")); - }, [setCursor, containerRef]); - - return { - gameStatus, - gamePhase, - playStatus, - setGameStatus, - startGame, - stopGame, - }; -} - -export type UseGameStatusType = ReturnType; diff --git a/src_old/useGame/useKeyBindings.ts b/src_old/useGame/useKeyBindings.ts deleted file mode 100644 index 09bdba4..0000000 --- a/src_old/useGame/useKeyBindings.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { useEffect, useRef, useState } from "react"; - -export type KeyLogEntry = { key: string; timestamp: number }; -export type EditMode = "navigation" | "edit"; - -type Ctx = { - moveCursor: (dr: number, dc: number, steps?: number) => void; - setDigit: (d: string) => void; - erase: () => void; - stopGame: () => void; - cursor: { r: number; c: number }; - board: string; - initialBoard: string; -}; - -export function useKeyBindings(ctx: Ctx) { - const [keyLog, setKeyLog] = useState([]); - const [editMode, setEditMode] = useState("navigation"); - const logRef = useRef([]); - const countRef = useRef(""); // Vim-style numeric prefix - const lastMotionRef = useRef<{ dr: number; dc: number; steps: number } | null>(null); - - // Record keypresses for score/time analytics - const recordKey = (key: string) => { - const entry = { key, timestamp: performance.now() }; - logRef.current.push(entry); - setKeyLog([...logRef.current]); - }; - - const clearLog = () => { - logRef.current = []; - setKeyLog([]); - }; - - const getLog = () => logRef.current; - - const resetCount = () => { - countRef.current = ""; - }; - - useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - const k = e.key; - - // ========== EDIT MODE ========== - if (editMode === "edit") { - // Exit edit mode with Escape or Enter - if (k === "Escape" || k === "Enter") { - recordKey(k); - setEditMode("navigation"); - return; - } - - // Digits 1-9 set the cell value - if (/^[1-9]$/.test(k)) { - recordKey(k); - ctx.setDigit(k); - return; - } - - // Erase with x or Backspace - if (k === "x" || k === "Backspace") { - recordKey(k); - ctx.erase(); - return; - } - - // Ignore all other keys in edit mode - return; - } - - // ========== NAVIGATION MODE ========== - - // Q to quit game - if (k === "q" || k === "Q") { - recordKey(k); - ctx.stopGame(); - return; - } - - // R to replace (only works on user-filled cells) - if (k === "r" || k === "R") { - recordKey(k); - - const cellIndex = ctx.cursor.r * 9 + ctx.cursor.c; - const isClue = ctx.initialBoard[cellIndex] !== "."; - const isEmpty = ctx.board[cellIndex] === "."; - - if (isClue) { - // Flash red - cell is locked (pre-filled clue) - window.dispatchEvent(new CustomEvent("sudoku-locked-cell")); - return; - } - - if (isEmpty) { - // Flash red - cell is empty (use I to insert) - window.dispatchEvent(new CustomEvent("sudoku-locked-cell")); - return; - } - - setEditMode("edit"); - return; - } - - // I to insert (only works on empty cells) - if (k === "i" || k === "I") { - recordKey(k); - - const cellIndex = ctx.cursor.r * 9 + ctx.cursor.c; - const isClue = ctx.initialBoard[cellIndex] !== "."; - const isEmpty = ctx.board[cellIndex] === "."; - - if (isClue) { - // Flash red - cell is locked (pre-filled clue) - window.dispatchEvent(new CustomEvent("sudoku-locked-cell")); - return; - } - - if (!isEmpty) { - // Flash red - cell already has a value (use R to replace) - window.dispatchEvent(new CustomEvent("sudoku-locked-cell")); - return; - } - - setEditMode("edit"); - return; - } - - // D or X to delete cell value in navigation mode - if (k === "d" || k === "D" || k === "x" || k === "X") { - recordKey(k); - - const cellIndex = ctx.cursor.r * 9 + ctx.cursor.c; - const isClue = ctx.initialBoard[cellIndex] !== "."; - - if (isClue) { - // Flash red - can't delete clues - window.dispatchEvent(new CustomEvent("sudoku-locked-cell")); - return; - } - - ctx.erase(); - return; - } - - // Escape cancels count prefix - if (k === "Escape") { - recordKey(k); - resetCount(); - return; - } - - // Numeric prefixes (for counted motions) - if (/^[0-9]$/.test(k) && k !== "0") { - recordKey(k); - countRef.current += k; - return; - } - - // Vim-style navigation - let dr = 0; - let dc = 0; - if (k === "h") dc = -1; - else if (k === "l") dc = 1; - else if (k === "j") dr = 1; - else if (k === "k") dr = -1; - else if (k === "0" || k === "^") dc = -999; // go to col 0 - else if (k === "$") dc = 999; // go to col 8 - else if (k === ".") { - recordKey("."); - const last = lastMotionRef.current; - if (last) ctx.moveCursor(last.dr, last.dc, last.steps); - return; - } else return; - - recordKey(k); - - const steps = Math.max(1, parseInt(countRef.current || "1", 10)); - resetCount(); - - if (dc === -999) { - ctx.moveCursor(0, -1, 99); // move far left - lastMotionRef.current = { dr: 0, dc: -1, steps: 99 }; - return; - } - - if (dc === 999) { - ctx.moveCursor(0, 1, 99); // move far right - lastMotionRef.current = { dr: 0, dc: 1, steps: 99 }; - return; - } - - ctx.moveCursor(dr, dc, steps); - lastMotionRef.current = { dr, dc, steps }; - }; - - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [ctx, editMode]); - - // ✅ same interface as vim-maze useKeyBindings - return { - keyLog, - getLog, - clearLog, - resetCount, - countRef, - lastMotionRef, - editMode, - }; -} - -export type UseKeyBindingsType = ReturnType; diff --git a/src_old/useGame/useSudokuRenderer.ts b/src_old/useGame/useSudokuRenderer.ts deleted file mode 100644 index d0c312d..0000000 --- a/src_old/useGame/useSudokuRenderer.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { useRef, useCallback } from "react"; - -export function useRenderer() { - const containerRef = useRef(null); - - const renderEmptyBoard = useCallback(() => { - const container = containerRef.current; - if (!container) return; - - container.innerHTML = ""; - const sudokuDiv = document.createElement("div"); - sudokuDiv.id = "sudoku"; - - const grid = document.createElement("div"); - grid.className = "sudoku-grid"; - - for (let i = 0; i < 81; i++) { - const r = Math.floor(i / 9); - const c = i % 9; - const cell = document.createElement("div"); - cell.className = "sudoku-cell"; - cell.dataset.r = String(r); - cell.dataset.c = String(c); - cell.textContent = ""; - grid.appendChild(cell); - } - - sudokuDiv.appendChild(grid); - container.appendChild(sudokuDiv); - }, []); - - const renderBoard = useCallback((board: string, initialBoard?: string) => { - const container = containerRef.current; - if (!container) return; - - container.innerHTML = ""; - const sudokuDiv = document.createElement("div"); - sudokuDiv.id = "sudoku"; - - const grid = document.createElement("div"); - grid.className = "sudoku-grid"; - - for (let i = 0; i < 81; i++) { - const r = Math.floor(i / 9); - const c = i % 9; - const cell = document.createElement("div"); - cell.className = "sudoku-cell"; - cell.dataset.r = String(r); - cell.dataset.c = String(c); - cell.textContent = board[i] === "." ? "" : board[i]; - - if (initialBoard && board[i] !== "." && board[i] !== initialBoard[i]) { - cell.classList.add("user-entered"); - } - - grid.appendChild(cell); - } - - sudokuDiv.appendChild(grid); - container.appendChild(sudokuDiv); - }, []); - - const highlightCursor = useCallback((r: number, c: number, isEditing = false) => { - const container = containerRef.current; - if (!container) return; - - const sudokuDiv = container.querySelector("#sudoku"); - if (!sudokuDiv) return; - - sudokuDiv.querySelectorAll(".active").forEach((el) => el.classList.remove("active")); - sudokuDiv.querySelectorAll(".editing").forEach((el) => el.classList.remove("editing")); - - const selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`; - const cell = sudokuDiv.querySelector(selector); - if (cell) { - cell.classList.add("active"); - if (isEditing) { - cell.classList.add("editing"); - } - } - }, []); - - return { containerRef, renderEmptyBoard, renderBoard, highlightCursor }; -} - -export type UseRenderer = ReturnType; - diff --git a/src_old/useScore/hooks/useTimer.ts b/src_old/useScore/hooks/useTimer.ts deleted file mode 100644 index 16e6e10..0000000 --- a/src_old/useScore/hooks/useTimer.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; - -export type UseTimerReturn = { - timeValue: number; - startTimer: () => void; - stopTimer: () => void; - resetTimer: () => void; -}; - -export type UseTimerOptions = { - interval?: number; -}; - -export function useTimer(options: UseTimerOptions = {}): UseTimerReturn { - const { interval = 100 } = options; - const [timeValue, setTimeValue] = useState(0); - - const intervalIdRef = useRef | null>(null); - const offsetRef = useRef(0); - const startTimeRef = useRef(null); - - const update = useCallback(() => { - if (startTimeRef.current === null) { - return; - } - - const now = performance.now(); - setTimeValue(offsetRef.current + (now - startTimeRef.current)); - }, []); - - const startTimer = useCallback(() => { - if (intervalIdRef.current !== null) { - return; - } - - if (startTimeRef.current === null) { - startTimeRef.current = performance.now(); - } - - update(); - intervalIdRef.current = setInterval(update, Math.max(16, interval)); - }, [interval, update]); - - const stopTimer = useCallback(() => { - if (intervalIdRef.current !== null) { - clearInterval(intervalIdRef.current); - intervalIdRef.current = null; - } - - if (startTimeRef.current !== null) { - update(); - offsetRef.current += performance.now() - startTimeRef.current; - startTimeRef.current = null; - } - }, [update]); - - const resetTimer = useCallback(() => { - if (intervalIdRef.current !== null) { - clearInterval(intervalIdRef.current); - intervalIdRef.current = null; - } - - startTimeRef.current = null; - offsetRef.current = 0; - setTimeValue(0); - }, []); - - useEffect(() => { - return () => { - if (intervalIdRef.current !== null) { - clearInterval(intervalIdRef.current); - } - }; - }, []); - - return { - timeValue, - startTimer, - stopTimer, - resetTimer, - }; -} diff --git a/src_old/useScore/index.ts b/src_old/useScore/index.ts deleted file mode 100644 index 525ce75..0000000 --- a/src_old/useScore/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useScore'; diff --git a/src_old/useScore/useScore.ts b/src_old/useScore/useScore.ts deleted file mode 100644 index a85eae0..0000000 --- a/src_old/useScore/useScore.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { useEffect, useState } from "react"; -import { useTimer } from "./hooks/useTimer"; -import { useScoreTime } from "./useScoreTime"; -import type { GameStatus } from "../types"; - -const TIME_LIMIT_MS = Infinity; // No time limit by default -const KEYSTROKE_LIMIT = Infinity; // No keystroke limit by default - -type GameScoreContext = { - gameStatus: GameStatus; - setGameStatus: (status: GameStatus) => void; -}; - -type UseScoreParams = { - gameContext: GameScoreContext; - keystrokeCount: number; - board: string; - isComplete: (board: string) => boolean; - isFullyValid: (board: string) => boolean; - initialBoard: string; -}; - -export function useScore({ - gameContext, - keystrokeCount, - board, - isComplete, - isFullyValid, - initialBoard -}: UseScoreParams) { - const timer = useTimer(); - const { gameStatus, setGameStatus } = gameContext; - const { timeValue, startTimer, stopTimer, resetTimer } = timer; - - const [finalScore, setFinalScore] = useState(null); - - const minMovesRequired = initialBoard.split("").filter(c => c === ".").length; - const actualMoves = keystrokeCount; - const efficiency = minMovesRequired > 0 - ? Math.round((actualMoves / minMovesRequired) * 100) - : 0; - - useScoreTime({ gameStatus, timer }); - - useEffect(() => { - if (gameStatus !== "started") return; - - if (isComplete(board) && isFullyValid(board)) { - setGameStatus("game-won"); - return; - } - - if (timeValue >= TIME_LIMIT_MS) { - setGameStatus("game-over"); - return; - } - - if (keystrokeCount >= KEYSTROKE_LIMIT) { - setGameStatus("game-over"); - return; - } - - // TODO: Enable efficiency-based game-over in future - // if (efficiency > 200) { - // setGameStatus("game-over"); - // return; - // } - }, [gameStatus, timeValue, keystrokeCount, board, setGameStatus, isComplete, isFullyValid, efficiency]); - - useEffect(() => { - const handleWin = () => { - if (gameStatus === "started") { - setGameStatus("game-won"); - } - }; - window.addEventListener("sudoku-game-won", handleWin); - return () => window.removeEventListener("sudoku-game-won", handleWin); - }, [gameStatus, setGameStatus]); - - useEffect(() => { - if (gameStatus !== "game-won") return; - - const timeInSeconds = timeValue / 1000; - const baseScore = 100000; - const timePenalty = Math.floor(timeInSeconds * 10); - const extraMoves = Math.max(0, actualMoves - minMovesRequired); - const movePenalty = extraMoves * 50; - const score = Math.max(0, baseScore - timePenalty - movePenalty); - - setFinalScore(score); - }, [gameStatus, timeValue, actualMoves, minMovesRequired]); - - return { - timeValue, - startTimer, - stopTimer, - resetTimer, - finalScore, - timeLimit: TIME_LIMIT_MS, - keystrokeLimit: KEYSTROKE_LIMIT, - minMovesRequired, - actualMoves, - efficiency, - }; -} - -export type UseScoreType = ReturnType; diff --git a/src_old/useScore/useScoreTime.ts b/src_old/useScore/useScoreTime.ts deleted file mode 100644 index 4f76b85..0000000 --- a/src_old/useScore/useScoreTime.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useRef } from "react"; -import type { UseTimerReturn } from "./hooks/useTimer"; -import type { GameStatus } from "../types"; - -type UseScoreTimeParams = { - gameStatus: GameStatus; - timer: UseTimerReturn; -}; - -export function useScoreTime({ gameStatus, timer }: UseScoreTimeParams) { - const prevStatusRef = useRef(null); - const { startTimer, stopTimer, resetTimer } = timer; - - useEffect(() => { - prevStatusRef.current = gameStatus; - - switch (gameStatus) { - case "started": - resetTimer(); - startTimer(); - break; - - case "game-over": - case "game-won": - case "waiting": - stopTimer(); - break; - } - }, [gameStatus, startTimer, stopTimer, resetTimer]); -}