working on useGame
This commit is contained in:
+50
-10
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 (/^[1-9]$/.test(k)) {
|
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 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);
|
recordKey(k);
|
||||||
ctx.setDigit(k);
|
|
||||||
resetCount();
|
resetCount();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Erase ---
|
// Numeric prefixes (for counted motions)
|
||||||
if (k === "x" || k === "Backspace") {
|
if (/^[0-9]$/.test(k) && k !== "0") {
|
||||||
recordKey(k);
|
|
||||||
ctx.erase();
|
|
||||||
resetCount();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Numeric prefixes ---
|
|
||||||
if (/^[0-9]$/.test(k)) {
|
|
||||||
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user