diff --git a/src/game.css b/src/game.css index 09454f5..6eaedcb 100644 --- a/src/game.css +++ b/src/game.css @@ -254,3 +254,9 @@ 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); +} diff --git a/src/types.ts b/src/types.ts index 590f560..3a804c2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,11 @@ import type { RefObject } from 'react'; import type { GameStatus, GameStatusManager } from './useGameStatus/types'; +import type { CursorManager } from './useCursor/types'; export type Difficulty = 'easy' | 'medium' | 'hard'; export type { GameStatus, GameStatusManager }; +export type { CursorManager, Coord } from './useCursor/types'; export type GameOptions = { difficulty?: Difficulty; @@ -14,7 +16,7 @@ export type KeyLogEntry = { key: string; timestamp: number }; export type GameManager = { containerRef: RefObject; renderBoard: () => void; - cursor: any; + cursor: CursorManager; scoreManager: { timeValue: number; keystrokes: number; diff --git a/src/useBoard/types.ts b/src/useBoard/types.ts new file mode 100644 index 0000000..643fde9 --- /dev/null +++ b/src/useBoard/types.ts @@ -0,0 +1,8 @@ +import type { RefObject } from 'react'; +import type { Difficulty } from '../types'; + +export type BoardManager = { + containerRef: RefObject; + generateBoard: (difficulty: Difficulty) => void; + renderBoard: () => void; +}; diff --git a/src/useCursor/index.ts b/src/useCursor/index.ts new file mode 100644 index 0000000..71dcc07 --- /dev/null +++ b/src/useCursor/index.ts @@ -0,0 +1,2 @@ +export { useCursor } from './useCursor'; +export type { CursorManager, Coord } from './types'; diff --git a/src/useCursor/types.ts b/src/useCursor/types.ts new file mode 100644 index 0000000..b6ffee5 --- /dev/null +++ b/src/useCursor/types.ts @@ -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; +}; diff --git a/src/useCursor/useCursor.ts b/src/useCursor/useCursor.ts new file mode 100644 index 0000000..0c93a7d --- /dev/null +++ b/src/useCursor/useCursor.ts @@ -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({ 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; +} diff --git a/src/useCursor/useCursorRender.ts b/src/useCursor/useCursorRender.ts new file mode 100644 index 0000000..7b2ee46 --- /dev/null +++ b/src/useCursor/useCursorRender.ts @@ -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]); +} diff --git a/src/useGame.ts b/src/useGame.ts index 4cf0f01..dc372f2 100644 --- a/src/useGame.ts +++ b/src/useGame.ts @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'; import type { GameManager, GameOptions } from './types'; import { useBoard } from './useBoard/useBoard'; import { useGameStatus } from './useGameStatus'; +import { useCursor } from './useCursor'; +import { useCursorRender } from './useCursor/useCursorRender'; export function useGame(options: GameOptions = {}, platformHook?: unknown): GameManager { const { difficulty = 'easy' } = options; @@ -11,6 +13,11 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game const gameStatusManager = useGameStatus(); const { gameStatus } = gameStatusManager; + const cursor = useCursor(gameStatusManager); + const cursorPosition = cursor.position(); + + useCursorRender(containerRef, cursorPosition); + const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]); useEffect(() => { @@ -30,7 +37,7 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game containerRef, renderBoard, ...gameStatusManager, - cursor: {}, + cursor, scoreManager: { timeValue: 0, keystrokes: 0,