working on useGame

This commit is contained in:
2025-10-15 17:47:41 +01:00
parent 8acc16fc1c
commit da8ca2c289
5 changed files with 153 additions and 38 deletions
+50 -10
View File
@@ -57,18 +57,16 @@
/* ========================================================= /* =========================================================
3x3 Box Separators 3x3 Box Separators
========================================================= */ ========================================================= */
/* Add thicker border to right of columns 2 and 5 (0-indexed) */ /* Add extra margin to right of columns 2 and 5 (0-indexed) */
#sudoku .sudoku-cell[data-c="2"], #sudoku .sudoku-cell[data-c="2"],
#sudoku .sudoku-cell[data-c="5"] { #sudoku .sudoku-cell[data-c="5"] {
border-right: var(--box-separator) solid #1e2030; margin-right: var(--box-separator);
margin-right: 1px;
} }
/* Add thicker border to bottom of rows 2 and 5 (0-indexed) */ /* Add extra margin to bottom of rows 2 and 5 (0-indexed) */
#sudoku .sudoku-cell[data-r="2"], #sudoku .sudoku-cell[data-r="2"],
#sudoku .sudoku-cell[data-r="5"] { #sudoku .sudoku-cell[data-r="5"] {
border-bottom: var(--box-separator) solid #1e2030; margin-bottom: var(--box-separator);
margin-bottom: 1px;
} }
/* ========================================================= /* =========================================================
@@ -80,20 +78,33 @@
} }
/* ========================================================= /* =========================================================
Active Cell Highlight Active Cell Highlight (Navigation Mode)
========================================================= */ ========================================================= */
#sudoku .sudoku-cell.active { #sudoku .sudoku-cell.active {
outline: 2px solid #ffc777; /* Tokyo Night yellow/warning */ outline: 2px solid #82aaff; /* Tokyo Night blue */
outline-offset: -2px;
background: #3d4269;
box-shadow:
0 0 8px rgba(130, 170, 255, 0.4),
0 0 12px rgba(130, 170, 255, 0.2),
inset 0 0 6px rgba(130, 170, 255, 0.15);
}
/* =========================================================
Editing Cell Highlight (Edit Mode)
========================================================= */
#sudoku .sudoku-cell.editing {
outline: 2px solid #ffc777; /* Tokyo Night yellow */
outline-offset: -2px; outline-offset: -2px;
background: #3d3836; background: #3d3836;
box-shadow: box-shadow:
0 0 16px rgba(255, 199, 119, 0.7), 0 0 16px rgba(255, 199, 119, 0.7),
0 0 24px rgba(255, 199, 119, 0.4), 0 0 24px rgba(255, 199, 119, 0.4),
inset 0 0 12px rgba(255, 199, 119, 0.3); inset 0 0 12px rgba(255, 199, 119, 0.3);
animation: active-cell-glow 1.5s ease-in-out infinite; animation: editing-cell-glow 1.5s ease-in-out infinite;
} }
@keyframes active-cell-glow { @keyframes editing-cell-glow {
0%, 100% { 0%, 100% {
box-shadow: box-shadow:
0 0 16px rgba(255, 199, 119, 0.7), 0 0 16px rgba(255, 199, 119, 0.7),
@@ -132,6 +143,35 @@
} }
} }
/* 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 */ /* Trail effect for moves */
#sudoku .sudoku-cell.trail { #sudoku .sudoku-cell.trail {
box-shadow: inset 0 0 6px rgba(130, 170, 255, 0.6), 0 0 6px rgba(130, 170, 255, 0.35); box-shadow: inset 0 0 6px rgba(130, 170, 255, 0.6), 0 0 6px rgba(130, 170, 255, 0.35);
+14 -1
View File
@@ -1,4 +1,4 @@
import { useRef, useState, useCallback } from "react"; import { useRef, useState, useCallback, useEffect } from "react";
import type { UseRenderer } from "./useSudokuRenderer"; import type { UseRenderer } from "./useSudokuRenderer";
export function useCursor(rendererManager: UseRenderer) { export function useCursor(rendererManager: UseRenderer) {
@@ -23,6 +23,13 @@ export function useCursor(rendererManager: UseRenderer) {
setTimeout(() => el.classList.remove("invalid-move"), 180); setTimeout(() => el.classList.remove("invalid-move"), 180);
}, [cursor, getCellEl]); }, [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 moveCursor = useCallback( const moveCursor = useCallback(
(dr: number, dc: number, steps = 1) => { (dr: number, dc: number, steps = 1) => {
if (animatingRef.current) return; if (animatingRef.current) return;
@@ -40,6 +47,12 @@ export function useCursor(rendererManager: UseRenderer) {
[] []
); );
useEffect(() => {
const onLockedCell = () => flashLocked();
window.addEventListener("sudoku-locked-cell", onLockedCell);
return () => window.removeEventListener("sudoku-locked-cell", onLockedCell);
}, [flashLocked]);
return { return {
cursor, cursor,
setCursor, setCursor,
+17 -11
View File
@@ -15,6 +15,21 @@ export function useGame(platformHook?: unknown) {
const boardManager = useBoardState(cursorManager); const boardManager = useBoardState(cursorManager);
const { board, initialBoard, solution, setDigit, erase, reset, solveBoard } = boardManager; const { board, initialBoard, solution, setDigit, erase, reset, solveBoard } = boardManager;
// ---------------------------
// Keybindings
// ---------------------------
const keyBindings = useKeyBindings({
moveCursor,
setDigit,
erase,
stopGame,
cursor,
board,
initialBoard,
});
const { editMode } = keyBindings;
// --------------------------- // ---------------------------
// Rendering // Rendering
// --------------------------- // ---------------------------
@@ -27,18 +42,9 @@ export function useGame(platformHook?: unknown) {
renderEmptyBoard(); renderEmptyBoard();
} else { } else {
renderBoard(board, initialBoard); renderBoard(board, initialBoard);
highlightCursor(cursor.r, cursor.c); highlightCursor(cursor.r, cursor.c, editMode === "edit");
} }
}, [gameStatus, board, initialBoard, cursor, renderEmptyBoard, renderBoard, highlightCursor]); }, [gameStatus, board, initialBoard, cursor, editMode, renderEmptyBoard, renderBoard, highlightCursor]);
// ---------------------------
// Keybindings
// ---------------------------
const keyBindings = useKeyBindings({
moveCursor,
setDigit,
erase,
});
// --------------------------- // ---------------------------
// Game Control // Game Control
+59 -7
View File
@@ -1,15 +1,21 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
export type KeyLogEntry = { key: string; timestamp: number }; export type KeyLogEntry = { key: string; timestamp: number };
export type EditMode = "navigation" | "edit";
type Ctx = { type Ctx = {
moveCursor: (dr: number, dc: number, steps?: number) => void; moveCursor: (dr: number, dc: number, steps?: number) => void;
setDigit: (d: string) => void; setDigit: (d: string) => void;
erase: () => void; erase: () => void;
stopGame: () => void;
cursor: { r: number; c: number };
board: string;
initialBoard: string;
}; };
export function useKeyBindings(ctx: Ctx) { export function useKeyBindings(ctx: Ctx) {
const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]); const [keyLog, setKeyLog] = useState<KeyLogEntry[]>([]);
const [editMode, setEditMode] = useState<EditMode>("navigation");
const logRef = useRef<KeyLogEntry[]>([]); const logRef = useRef<KeyLogEntry[]>([]);
const countRef = useRef<string>(""); // Vim-style numeric prefix const countRef = useRef<string>(""); // Vim-style numeric prefix
const lastMotionRef = useRef<{ dr: number; dc: number; steps: number } | null>(null); const lastMotionRef = useRef<{ dr: number; dc: number; steps: number } | null>(null);
@@ -36,30 +42,75 @@ export function useKeyBindings(ctx: Ctx) {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
const k = e.key; const k = e.key;
// --- Digits 1–9 fill a cell --- // ========== 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)) { if (/^[1-9]$/.test(k)) {
recordKey(k); recordKey(k);
ctx.setDigit(k); ctx.setDigit(k);
resetCount();
return; return;
} }
// --- Erase --- // Erase with x or Backspace
if (k === "x" || k === "Backspace") { if (k === "x" || k === "Backspace") {
recordKey(k); recordKey(k);
ctx.erase(); 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 or C to enter edit mode
if (k === "r" || k === "R" || k === "c" || k === "C") {
recordKey(k);
// Check if current cell is a pre-filled clue (not editable)
const cellIndex = ctx.cursor.r * 9 + ctx.cursor.c;
const isClue = ctx.initialBoard[cellIndex] !== ".";
if (isClue) {
// Flash red - cell is locked
window.dispatchEvent(new CustomEvent("sudoku-locked-cell"));
return;
}
setEditMode("edit");
return;
}
// Escape cancels count prefix
if (k === "Escape") {
recordKey(k);
resetCount(); resetCount();
return; return;
} }
// --- Numeric prefixes --- // Numeric prefixes (for counted motions)
if (/^[0-9]$/.test(k)) { if (/^[0-9]$/.test(k) && k !== "0") {
recordKey(k); recordKey(k);
countRef.current += k; countRef.current += k;
return; return;
} }
// --- Vim-style navigation --- // Vim-style navigation
let dr = 0; let dr = 0;
let dc = 0; let dc = 0;
if (k === "h") dc = -1; if (k === "h") dc = -1;
@@ -98,7 +149,7 @@ export function useKeyBindings(ctx: Ctx) {
window.addEventListener("keydown", onKeyDown); window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown);
}, [ctx]); }, [ctx, editMode]);
// ✅ same interface as vim-maze useKeyBindings // ✅ same interface as vim-maze useKeyBindings
return { return {
@@ -108,6 +159,7 @@ export function useKeyBindings(ctx: Ctx) {
resetCount, resetCount,
countRef, countRef,
lastMotionRef, lastMotionRef,
editMode,
}; };
} }
+5 -1
View File
@@ -60,7 +60,7 @@ export function useRenderer() {
container.appendChild(sudokuDiv); container.appendChild(sudokuDiv);
}, []); }, []);
const highlightCursor = useCallback((r: number, c: number) => { const highlightCursor = useCallback((r: number, c: number, isEditing = false) => {
const container = containerRef.current; const container = containerRef.current;
if (!container) return; if (!container) return;
@@ -68,11 +68,15 @@ export function useRenderer() {
if (!sudokuDiv) return; if (!sudokuDiv) return;
sudokuDiv.querySelectorAll(".active").forEach((el) => el.classList.remove("active")); 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 selector = `.sudoku-cell[data-r="${r}"][data-c="${c}"]`;
const cell = sudokuDiv.querySelector(selector); const cell = sudokuDiv.querySelector(selector);
if (cell) { if (cell) {
cell.classList.add("active"); cell.classList.add("active");
if (isEditing) {
cell.classList.add("editing");
}
} }
}, []); }, []);