starting new structure
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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<KeyLogEntry[]>([]);
|
||||
const [editMode, setEditMode] = useState<EditMode>("navigation");
|
||||
const logRef = useRef<KeyLogEntry[]>([]);
|
||||
const countRef = useRef<string>(""); // 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<typeof useKeyBindings>;
|
||||
Reference in New Issue
Block a user