feat: implement digit entry in edit mode (Phase 40.3.3)
- Add setCell method to useBoard to update board state - Handle digit keys 1-9 in edit mode in useCursor - Move boardRendered counter to useBoard for proper cursor re-rendering - Fix golden glow disappearing after digit entry - Digits display with .user-entered class via renderer
This commit is contained in:
+124
-1
@@ -762,6 +762,129 @@ Red flash → Stay in Normal
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Iteration 9: [Pending]
|
## Iteration 9: Phase 40.3.3 - Digit Entry
|
||||||
|
|
||||||
|
**Date**: 2025-01-22
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
- Allow user to type digits 1-9 when in edit mode
|
||||||
|
- Update board state with entered digits
|
||||||
|
- Cells show digits with `.user-entered` class
|
||||||
|
- Stay in edit mode after entering digit
|
||||||
|
|
||||||
|
### Tasks Completed
|
||||||
|
|
||||||
|
#### 1. Added setCell Method to BoardManager
|
||||||
|
**Updated useBoard.ts:**
|
||||||
|
- Added `setCell(row, col, value)` function
|
||||||
|
- Updates board string at calculated index
|
||||||
|
- Triggers re-render to display change
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
```typescript
|
||||||
|
function setCell(row: number, col: number, value: string): void {
|
||||||
|
const board = boardRef.current.split('');
|
||||||
|
const index = row * 9 + col;
|
||||||
|
board[index] = value;
|
||||||
|
boardRef.current = board.join('');
|
||||||
|
renderBoard();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Updated BoardManager Type
|
||||||
|
- Added `setCell: (row: number, col: number, value: string) => void;` to type definition
|
||||||
|
|
||||||
|
#### 3. Enhanced useCursor for Digit Entry
|
||||||
|
**Updated useCursor.ts:**
|
||||||
|
- Added `boardManager` parameter (BoardManager)
|
||||||
|
- In edit mode, detect digit keys (1-9)
|
||||||
|
- Call `boardManager.setCell()` to update board
|
||||||
|
- Stay in edit mode (no auto-exit)
|
||||||
|
|
||||||
|
**Key Handling:**
|
||||||
|
```typescript
|
||||||
|
if (mode === 'edit') {
|
||||||
|
if (key === 'Escape') {
|
||||||
|
setMode('normal');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key >= '1' && key <= '9') {
|
||||||
|
boardManager.setCell(position.row, position.col, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Updated useGame Integration
|
||||||
|
- Pass `board` (BoardManager) to `useCursor()`
|
||||||
|
- Three-parameter call: `useCursor(gameStatusManager, containerRef, board)`
|
||||||
|
|
||||||
|
### Technical Details
|
||||||
|
|
||||||
|
**Board State Flow:**
|
||||||
|
```
|
||||||
|
User presses '5' in edit mode
|
||||||
|
↓
|
||||||
|
useCursor detects digit key
|
||||||
|
↓
|
||||||
|
Calls boardManager.setCell(row, col, '5')
|
||||||
|
↓
|
||||||
|
Updates boardRef.current string
|
||||||
|
↓
|
||||||
|
Calls renderBoard()
|
||||||
|
↓
|
||||||
|
SudokuRenderer.render() displays digit with .user-entered class
|
||||||
|
↓
|
||||||
|
User sees '5' in the cell
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cell Classification:**
|
||||||
|
- Given cells: Have `.given` class (from initial puzzle)
|
||||||
|
- User-entered cells: Have `.user-entered` class (from renderer logic)
|
||||||
|
- Empty cells: No value displayed
|
||||||
|
|
||||||
|
**Mode Behavior:**
|
||||||
|
- Edit mode entered with `i` on empty/user-entered cells
|
||||||
|
- Typing 1-9 places digit, stays in edit mode
|
||||||
|
- `Escape` exits to normal mode
|
||||||
|
- Can navigate to other cells and repeat
|
||||||
|
|
||||||
|
### Build Status
|
||||||
|
- ✅ TypeScript compiles cleanly
|
||||||
|
- ✅ No type errors
|
||||||
|
- ✅ Integration complete
|
||||||
|
|
||||||
|
### Issue 1: Golden Glow Disappearing After Digit Entry
|
||||||
|
|
||||||
|
**Problem**: When typing a digit in edit mode, the `.editing` class (golden glow) disappeared even though we remained in edit mode.
|
||||||
|
|
||||||
|
**Root Cause**: `setCell()` calls `renderBoard()` which rebuilds the entire DOM, wiping out the cursor highlight classes.
|
||||||
|
|
||||||
|
**Solution**: Moved `boardRendered` counter from `useGame` to `useBoard`
|
||||||
|
- Added `boardRendered` state to `useBoard` hook
|
||||||
|
- Increment counter at end of `renderBoard()` function
|
||||||
|
- Export `boardRendered` from `useBoard`
|
||||||
|
- `useCursorRender` depends on `boardRendered` and re-applies cursor classes after DOM rebuild
|
||||||
|
|
||||||
|
**Changes**:
|
||||||
|
- `useBoard.ts`: Added `useState` for `boardRendered`, increment in `renderBoard()`
|
||||||
|
- `useBoard/types.ts`: Added `boardRendered: number` to `BoardManager`
|
||||||
|
- `useGame.ts`: Removed local `boardRendered` state, use board's `boardRendered` instead
|
||||||
|
- Removed manual `setBoardRendered` calls from `useGame` (automatic now)
|
||||||
|
|
||||||
|
**Result**: ✅ Golden glow persists when typing digits in edit mode
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Phase 40.3.4: Add erase functionality (x, Backspace, Delete)
|
||||||
|
- Phase 40.3.5: Add replace mode (r)
|
||||||
|
- Validation and conflict detection
|
||||||
|
- Win condition checking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Iteration 10: [Pending]
|
||||||
|
|
||||||
*To be filled as implementation progresses...*
|
*To be filled as implementation progresses...*
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { Difficulty } from '../types';
|
|||||||
|
|
||||||
export type BoardManager = {
|
export type BoardManager = {
|
||||||
containerRef: RefObject<HTMLDivElement | null>;
|
containerRef: RefObject<HTMLDivElement | null>;
|
||||||
|
boardRendered: number;
|
||||||
generateBoard: (difficulty: Difficulty) => void;
|
generateBoard: (difficulty: Difficulty) => void;
|
||||||
renderBoard: () => void;
|
renderBoard: () => void;
|
||||||
|
setCell: (row: number, col: number, value: string) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { SudokuGenerator } from './SudokuGenerator';
|
import { SudokuGenerator } from './SudokuGenerator';
|
||||||
import { SudokuRenderer } from './SudokuRenderer';
|
import { SudokuRenderer } from './SudokuRenderer';
|
||||||
import type { Difficulty } from './SudokuGenerator';
|
import type { Difficulty } from './SudokuGenerator';
|
||||||
@@ -10,6 +10,7 @@ export function useBoard() {
|
|||||||
const boardRef = useRef<string>('');
|
const boardRef = useRef<string>('');
|
||||||
const solutionRef = useRef<string>('');
|
const solutionRef = useRef<string>('');
|
||||||
const initialBoardRef = useRef<string>('');
|
const initialBoardRef = useRef<string>('');
|
||||||
|
const [boardRendered, setBoardRendered] = useState(0);
|
||||||
|
|
||||||
function generateBoard(difficulty: Difficulty = 'easy') {
|
function generateBoard(difficulty: Difficulty = 'easy') {
|
||||||
if (!generatorRef.current) {
|
if (!generatorRef.current) {
|
||||||
@@ -41,6 +42,16 @@ export function useBoard() {
|
|||||||
} else {
|
} else {
|
||||||
rendererRef.current.renderEmpty();
|
rendererRef.current.renderEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setBoardRendered(prev => prev + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCell(row: number, col: number, value: string): void {
|
||||||
|
const board = boardRef.current.split('');
|
||||||
|
const index = row * 9 + col;
|
||||||
|
board[index] = value;
|
||||||
|
boardRef.current = board.join('');
|
||||||
|
renderBoard();
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -50,7 +61,9 @@ export function useBoard() {
|
|||||||
boardRef,
|
boardRef,
|
||||||
solutionRef,
|
solutionRef,
|
||||||
initialBoardRef,
|
initialBoardRef,
|
||||||
|
boardRendered,
|
||||||
generateBoard,
|
generateBoard,
|
||||||
renderBoard,
|
renderBoard,
|
||||||
|
setCell,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import type { Coord, CursorManager, CursorMode } from './types';
|
import type { Coord, CursorManager, CursorMode } from './types';
|
||||||
import type { GameStatusManager } from '../useGameStatus/types';
|
import type { GameStatusManager } from '../useGameStatus/types';
|
||||||
|
import type { BoardManager } from '../useBoard/types';
|
||||||
import { useVimMotions } from './useVimMotions';
|
import { useVimMotions } from './useVimMotions';
|
||||||
|
|
||||||
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>) {
|
export function useCursor(gameStatusManager: GameStatusManager, containerRef: React.RefObject<HTMLDivElement | null>, boardManager: BoardManager) {
|
||||||
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
const [position, setPosition] = useState<Coord>({ row: 0, col: 0 });
|
||||||
const [mode, setMode] = useState<CursorMode>('normal');
|
const [mode, setMode] = useState<CursorMode>('normal');
|
||||||
const { gameStatus } = gameStatusManager;
|
const { gameStatus } = gameStatusManager;
|
||||||
@@ -97,7 +98,14 @@ export function useCursor(gameStatusManager: GameStatusManager, containerRef: Re
|
|||||||
if (mode === 'edit') {
|
if (mode === 'edit') {
|
||||||
if (key === 'Escape') {
|
if (key === 'Escape') {
|
||||||
setMode('normal');
|
setMode('normal');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (key >= '1' && key <= '9') {
|
||||||
|
boardManager.setCell(position.row, position.col, key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-6
@@ -8,15 +8,14 @@ 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;
|
||||||
const board = useBoard();
|
const board = useBoard();
|
||||||
const { containerRef, renderBoard, generateBoard } = board;
|
const { containerRef, renderBoard, generateBoard, boardRendered } = board;
|
||||||
|
|
||||||
const gameStatusManager = useGameStatus();
|
const gameStatusManager = useGameStatus();
|
||||||
const { gameStatus } = gameStatusManager;
|
const { gameStatus } = gameStatusManager;
|
||||||
|
|
||||||
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
const [keyLog, setKeyLog] = useState<{ key: string; timestamp: number }[]>([]);
|
||||||
const [boardRendered, setBoardRendered] = useState(0);
|
|
||||||
|
|
||||||
const cursor = useCursor(gameStatusManager, containerRef);
|
const cursor = useCursor(gameStatusManager, containerRef, board);
|
||||||
const cursorPosition = cursor.position();
|
const cursorPosition = cursor.position();
|
||||||
const cursorMode = cursor.mode();
|
const cursorMode = cursor.mode();
|
||||||
|
|
||||||
@@ -24,17 +23,14 @@ export function useGame(options: GameOptions = {}, platformHook?: unknown): Game
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
renderBoard();
|
renderBoard();
|
||||||
setBoardRendered(prev => prev + 1);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameStatus === 'started') {
|
if (gameStatus === 'started') {
|
||||||
generateBoard(difficulty);
|
generateBoard(difficulty);
|
||||||
renderBoard();
|
renderBoard();
|
||||||
setBoardRendered(prev => prev + 1);
|
|
||||||
} else if (gameStatus === 'waiting') {
|
} else if (gameStatus === 'waiting') {
|
||||||
renderBoard();
|
renderBoard();
|
||||||
setBoardRendered(prev => prev + 1);
|
|
||||||
}
|
}
|
||||||
}, [gameStatus, difficulty]);
|
}, [gameStatus, difficulty]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user