feat: implement Phase 40.1 - basic cursor with hjkl movement
Add cursor navigation with hjkl keys (vim-style): New Module Structure: - src/useCursor/types.ts: Coord and CursorManager types - src/useCursor/useCursor.ts: Cursor movement logic - src/useCursor/useCursorRender.ts: Visual cursor highlight - src/useCursor/index.ts: Public exports Cursor Implementation: - Position tracking with useRef (row/col from 0-8) - Basic movement: moveLeft, moveRight, moveUp, moveDown - Bounds checking (prevents moving outside 9x9 grid) - Keyboard listener for h/j/k/l keys - Only active when gameStatus === 'started' Cursor Rendering: - useCursorRender hook tracks cursor position - Adds 'cursor' CSS class to current cell - Removes from previous cell on movement - Blue highlight with border (rgba(59, 130, 246, 0.3)) Integration: - src/useGame.ts: Integrate useCursor and useCursorRender - src/types.ts: Export CursorManager and Coord types - src/useBoard/types.ts: NEW - BoardManager type - src/game.css: Add cursor highlight styles Game Flow: - Press Space → Game starts → Cursor appears at (0,0) - Press h/j/k/l → Cursor moves (bounded to grid) - Visual feedback with blue highlight - Press Q → Return to waiting state Phase 40.1 complete. Next: Add number prefixes (5j) and other vim motions.
This commit is contained in:
@@ -254,3 +254,9 @@
|
|||||||
inset 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent);
|
inset 0 0 12px color-mix(in srgb, var(--destructive, #c53b53) 40%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cursor highlight */
|
||||||
|
.sudoku-cell.cursor {
|
||||||
|
background-color: rgba(59, 130, 246, 0.3);
|
||||||
|
border: 2px solid rgb(59, 130, 246);
|
||||||
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,9 +1,11 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import type { GameStatus, GameStatusManager } from './useGameStatus/types';
|
import type { GameStatus, GameStatusManager } from './useGameStatus/types';
|
||||||
|
import type { CursorManager } from './useCursor/types';
|
||||||
|
|
||||||
export type Difficulty = 'easy' | 'medium' | 'hard';
|
export type Difficulty = 'easy' | 'medium' | 'hard';
|
||||||
|
|
||||||
export type { GameStatus, GameStatusManager };
|
export type { GameStatus, GameStatusManager };
|
||||||
|
export type { CursorManager, Coord } from './useCursor/types';
|
||||||
|
|
||||||
export type GameOptions = {
|
export type GameOptions = {
|
||||||
difficulty?: Difficulty;
|
difficulty?: Difficulty;
|
||||||
@@ -14,7 +16,7 @@ export type KeyLogEntry = { key: string; timestamp: number };
|
|||||||
export type GameManager = {
|
export type GameManager = {
|
||||||
containerRef: RefObject<HTMLDivElement | null>;
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
renderBoard: () => void;
|
renderBoard: () => void;
|
||||||
cursor: any;
|
cursor: CursorManager;
|
||||||
scoreManager: {
|
scoreManager: {
|
||||||
timeValue: number;
|
timeValue: number;
|
||||||
keystrokes: number;
|
keystrokes: number;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import type { RefObject } from 'react';
|
||||||
|
import type { Difficulty } from '../types';
|
||||||
|
|
||||||
|
export type BoardManager = {
|
||||||
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
generateBoard: (difficulty: Difficulty) => void;
|
||||||
|
renderBoard: () => void;
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useCursor } from './useCursor';
|
||||||
|
export type { CursorManager, Coord } from './types';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type Coord = { row: number; col: number };
|
||||||
|
|
||||||
|
export type CursorManager = {
|
||||||
|
position: () => Coord;
|
||||||
|
moveLeft: (count?: number) => void;
|
||||||
|
moveRight: (count?: number) => void;
|
||||||
|
moveUp: (count?: number) => void;
|
||||||
|
moveDown: (count?: number) => void;
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { useRef, useEffect } from 'react';
|
||||||
|
import type { Coord, CursorManager } from './types';
|
||||||
|
import type { GameStatusManager } from '../useGameStatus/types';
|
||||||
|
|
||||||
|
export function useCursor(gameStatusManager: GameStatusManager) {
|
||||||
|
const position = useRef<Coord>({ row: 0, col: 0 });
|
||||||
|
const { gameStatus } = gameStatusManager;
|
||||||
|
|
||||||
|
function move(dRows: number, dCols: number, count: number = 1): void {
|
||||||
|
const currentPos = position.current;
|
||||||
|
const newRow = currentPos.row + (dRows * count);
|
||||||
|
const newCol = currentPos.col + (dCols * count);
|
||||||
|
|
||||||
|
if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
position.current = { row: newRow, col: newCol };
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveLeft(count = 1): void {
|
||||||
|
move(0, -1, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveRight(count = 1): void {
|
||||||
|
move(0, 1, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveUp(count = 1): void {
|
||||||
|
move(-1, 0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveDown(count = 1): void {
|
||||||
|
move(1, 0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (gameStatus !== 'started') return;
|
||||||
|
|
||||||
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
|
const key = e.key;
|
||||||
|
|
||||||
|
if (key === 'h') {
|
||||||
|
moveLeft();
|
||||||
|
} else if (key === 'j') {
|
||||||
|
moveDown();
|
||||||
|
} else if (key === 'k') {
|
||||||
|
moveUp();
|
||||||
|
} else if (key === 'l') {
|
||||||
|
moveRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [gameStatus]);
|
||||||
|
|
||||||
|
const cursor: CursorManager = {
|
||||||
|
position: () => position.current,
|
||||||
|
moveLeft,
|
||||||
|
moveRight,
|
||||||
|
moveUp,
|
||||||
|
moveDown,
|
||||||
|
};
|
||||||
|
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import type { Coord } from './types';
|
||||||
|
import type { BoardManager } from '../useBoard/types';
|
||||||
|
|
||||||
|
export function useCursorRender(containerRef: BoardManager['containerRef'], position: Coord) {
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const cells = container.querySelectorAll('.sudoku-cell');
|
||||||
|
|
||||||
|
cells.forEach(cell => {
|
||||||
|
cell.classList.remove('cursor');
|
||||||
|
});
|
||||||
|
|
||||||
|
const index = position.row * 9 + position.col;
|
||||||
|
const targetCell = cells[index];
|
||||||
|
if (targetCell) {
|
||||||
|
targetCell.classList.add('cursor');
|
||||||
|
}
|
||||||
|
}, [position, containerRef]);
|
||||||
|
}
|
||||||
+8
-1
@@ -2,6 +2,8 @@ import { useState, useEffect } from 'react';
|
|||||||
import type { GameManager, GameOptions } from './types';
|
import type { GameManager, GameOptions } from './types';
|
||||||
import { useBoard } from './useBoard/useBoard';
|
import { useBoard } from './useBoard/useBoard';
|
||||||
import { useGameStatus } from './useGameStatus';
|
import { useGameStatus } from './useGameStatus';
|
||||||
|
import { useCursor } from './useCursor';
|
||||||
|
import { useCursorRender } from './useCursor/useCursorRender';
|
||||||
|
|
||||||
export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager {
|
export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager {
|
||||||
const { difficulty = 'easy' } = options;
|
const { difficulty = 'easy' } = options;
|
||||||
@@ -11,6 +13,11 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
|
|||||||
const gameStatusManager = useGameStatus();
|
const gameStatusManager = useGameStatus();
|
||||||
const { gameStatus } = gameStatusManager;
|
const { gameStatus } = gameStatusManager;
|
||||||
|
|
||||||
|
const cursor = useCursor(gameStatusManager);
|
||||||
|
const cursorPosition = cursor.position();
|
||||||
|
|
||||||
|
useCursorRender(containerRef, cursorPosition);
|
||||||
|
|
||||||
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -30,7 +37,7 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
|
|||||||
containerRef,
|
containerRef,
|
||||||
renderBoard,
|
renderBoard,
|
||||||
...gameStatusManager,
|
...gameStatusManager,
|
||||||
cursor: {},
|
cursor,
|
||||||
scoreManager: {
|
scoreManager: {
|
||||||
timeValue: 0,
|
timeValue: 0,
|
||||||
keystrokes: 0,
|
keystrokes: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user