added draft mode

This commit is contained in:
2026-05-15 22:21:23 +08:00
parent 8af733a274
commit f1f906be55

View File

@@ -17,9 +17,16 @@ type SudokuRound = {
board: GameBoard; board: GameBoard;
regionMap: RegionMap; regionMap: RegionMap;
}; };
type DraftCells = boolean[][];
type BoardSnapshot = {
board: GameBoard;
draftCells: DraftCells;
};
const CELL_SIZE = 80; const CELL_SIZE = 80;
const CELL_FONT_SIZE = 36; const CELL_FONT_SIZE = 36;
const DRAFT_SIZE = CELL_SIZE / 2;
const DRAFT_FONT_SIZE = CELL_FONT_SIZE / 2;
const createSudokuRound = (difficulty: Difficulty, mode: GameMode): SudokuRound => { const createSudokuRound = (difficulty: Difficulty, mode: GameMode): SudokuRound => {
const { puzzle, regionMap } = generatePuzzleForMode(difficulty, mode); const { puzzle, regionMap } = generatePuzzleForMode(difficulty, mode);
@@ -30,6 +37,8 @@ const createSudokuRound = (difficulty: Difficulty, mode: GameMode): SudokuRound
}; };
const cloneGameBoard = (board: GameBoard): GameBoard => board.map(row => [...row]); const cloneGameBoard = (board: GameBoard): GameBoard => board.map(row => [...row]);
const cloneDraftCells = (draftCells: DraftCells): DraftCells => draftCells.map(row => [...row]);
const createEmptyDraftCells = (): DraftCells => Array.from({ length: 9 }, () => Array(9).fill(false));
const SudokuGame: React.FC = () => { const SudokuGame: React.FC = () => {
// 游戏难度 // 游戏难度
@@ -40,8 +49,10 @@ const SudokuGame: React.FC = () => {
const [round, setRound] = useState<SudokuRound>(() => createSudokuRound('hard', 'standard')); const [round, setRound] = useState<SudokuRound>(() => createSudokuRound('hard', 'standard'));
const { board, regionMap } = round; const { board, regionMap } = round;
const [undoStack, setUndoStack] = useState<GameBoard[]>([]); const [draftCells, setDraftCells] = useState<DraftCells>(() => createEmptyDraftCells());
const [redoStack, setRedoStack] = useState<GameBoard[]>([]); const [draftMode, setDraftMode] = useState(false);
const [undoStack, setUndoStack] = useState<BoardSnapshot[]>([]);
const [redoStack, setRedoStack] = useState<BoardSnapshot[]>([]);
const setBoard = (nextBoard: GameBoard) => { const setBoard = (nextBoard: GameBoard) => {
setRound((currentRound) => ({ setRound((currentRound) => ({
@@ -50,10 +61,17 @@ const SudokuGame: React.FC = () => {
})); }));
}; };
const applyBoardChange = (nextBoard: GameBoard) => { const applyBoardChange = (nextBoard: GameBoard, nextDraftCells: DraftCells) => {
setUndoStack((previousStack) => [...previousStack, cloneGameBoard(board)]); setUndoStack((previousStack) => [
...previousStack,
{
board: cloneGameBoard(board),
draftCells: cloneDraftCells(draftCells),
},
]);
setRedoStack([]); setRedoStack([]);
setBoard(nextBoard); setBoard(nextBoard);
setDraftCells(nextDraftCells);
}; };
const [fixedCells, setFixedCells] = useState<boolean[][]>(() => const [fixedCells, setFixedCells] = useState<boolean[][]>(() =>
@@ -159,15 +177,18 @@ const SudokuGame: React.FC = () => {
if (!selectedCell || isGameComplete) return; if (!selectedCell || isGameComplete) return;
const [row, col] = selectedCell; const [row, col] = selectedCell;
if (fixedCells[row][col]) return; if (fixedCells[row][col]) return;
if (board[row][col] === num) return; if (board[row][col] === num && draftCells[row][col] === draftMode) return;
const newBoard = board.map(row => [...row]); const newBoard = board.map(row => [...row]);
const newDraftCells = cloneDraftCells(draftCells);
newBoard[row][col] = num; newBoard[row][col] = num;
applyBoardChange(newBoard); newDraftCells[row][col] = draftMode;
applyBoardChange(newBoard, newDraftCells);
// 检查是否完成 // 检查是否完成
setTimeout(() => { setTimeout(() => {
if (checkIsComplete(newBoard)) { if (checkIsComplete(newBoard)) {
setDraftCells(createEmptyDraftCells());
setIsGameComplete(true); setIsGameComplete(true);
setIsTimerRunning(false); setIsTimerRunning(false);
setShowCompleteDialog(true); setShowCompleteDialog(true);
@@ -210,26 +231,42 @@ const SudokuGame: React.FC = () => {
if (fixedCells[row][col] || board[row][col] === 0) return; if (fixedCells[row][col] || board[row][col] === 0) return;
const newBoard = board.map(row => [...row]); const newBoard = board.map(row => [...row]);
const newDraftCells = cloneDraftCells(draftCells);
newBoard[row][col] = 0; newBoard[row][col] = 0;
applyBoardChange(newBoard); newDraftCells[row][col] = false;
applyBoardChange(newBoard, newDraftCells);
}; };
const handleUndo = () => { const handleUndo = () => {
if (isGameComplete || undoStack.length === 0) return; if (isGameComplete || undoStack.length === 0) return;
const previousBoard = undoStack[undoStack.length - 1]; const previousSnapshot = undoStack[undoStack.length - 1];
setUndoStack(undoStack.slice(0, -1)); setUndoStack(undoStack.slice(0, -1));
setRedoStack([cloneGameBoard(board), ...redoStack]); setRedoStack([
setBoard(cloneGameBoard(previousBoard)); {
board: cloneGameBoard(board),
draftCells: cloneDraftCells(draftCells),
},
...redoStack,
]);
setBoard(cloneGameBoard(previousSnapshot.board));
setDraftCells(cloneDraftCells(previousSnapshot.draftCells));
}; };
const handleRedo = () => { const handleRedo = () => {
if (isGameComplete || redoStack.length === 0) return; if (isGameComplete || redoStack.length === 0) return;
const nextBoard = redoStack[0]; const nextSnapshot = redoStack[0];
setRedoStack(redoStack.slice(1)); setRedoStack(redoStack.slice(1));
setUndoStack([...undoStack, cloneGameBoard(board)]); setUndoStack([
setBoard(cloneGameBoard(nextBoard)); ...undoStack,
{
board: cloneGameBoard(board),
draftCells: cloneDraftCells(draftCells),
},
]);
setBoard(cloneGameBoard(nextSnapshot.board));
setDraftCells(cloneDraftCells(nextSnapshot.draftCells));
}; };
const canUndo = !isGameComplete && undoStack.length > 0; const canUndo = !isGameComplete && undoStack.length > 0;
@@ -253,6 +290,7 @@ const SudokuGame: React.FC = () => {
const nextRound = createSudokuRound(level, mode); const nextRound = createSudokuRound(level, mode);
setRound(nextRound); setRound(nextRound);
setFixedCells(nextRound.board.map(row => row.map(cell => cell !== 0))); setFixedCells(nextRound.board.map(row => row.map(cell => cell !== 0)));
setDraftCells(createEmptyDraftCells());
setUndoStack([]); setUndoStack([]);
setRedoStack([]); setRedoStack([]);
setSelectedCell(null); setSelectedCell(null);
@@ -345,7 +383,7 @@ const SudokuGame: React.FC = () => {
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedCell, board, undoStack, redoStack, isGameComplete, fixedCells, timeElapsed, regionMap, difficulty]); }, [selectedCell, board, draftCells, draftMode, undoStack, redoStack, isGameComplete, fixedCells, timeElapsed, regionMap, difficulty]);
useEffect(() => { useEffect(() => {
if (!isTimerRunning) return; if (!isTimerRunning) return;
@@ -462,6 +500,21 @@ const SudokuGame: React.FC = () => {
}; };
}; };
const getCellValueStyle = (row: number, col: number): React.CSSProperties => {
const isDraft = draftCells[row][col] && !fixedCells[row][col];
const size = isDraft ? DRAFT_SIZE : CELL_SIZE;
return {
width: `${size}px`,
height: `${size}px`,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: `${isDraft ? DRAFT_FONT_SIZE : CELL_FONT_SIZE}px`,
lineHeight: 1,
};
};
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', padding: '20px' }}> <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', padding: '20px' }}>
{/* 顶部控制和配置区 */} {/* 顶部控制和配置区 */}
@@ -586,6 +639,14 @@ const SudokuGame: React.FC = () => {
/> />
</label> </label>
<label style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
<input
type="checkbox"
checked={draftMode}
onChange={(e) => setDraftMode(e.target.checked)}
/>
稿
</label>
</div> </div>
{/* 数独棋盘 */} {/* 数独棋盘 */}
@@ -611,7 +672,9 @@ const SudokuGame: React.FC = () => {
onClick={() => handleCellClick(rowIndex, colIndex)} onClick={() => handleCellClick(rowIndex, colIndex)}
style={cellStyle} style={cellStyle}
> >
{displayValue !== null ? displayValue : ''} {displayValue !== null ? (
<span style={getCellValueStyle(rowIndex, colIndex)}>{displayValue}</span>
) : ''}
</div> </div>
); );
}) })
@@ -622,7 +685,7 @@ const SudokuGame: React.FC = () => {
<div style={{ display: 'flex', gap: '30px', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'center' }}> <div style={{ display: 'flex', gap: '30px', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'center' }}>
<div style={{ padding: '15px 25px', backgroundColor: '#f5f5f5', borderRadius: '8px', textAlign: 'center' }}> <div style={{ padding: '15px 25px', backgroundColor: '#f5f5f5', borderRadius: '8px', textAlign: 'center' }}>
<span style={{ fontSize: '14px', color: '#666', display: 'block', marginBottom: '8px' }}> <span style={{ fontSize: '14px', color: '#666', display: 'block', marginBottom: '8px' }}>
{selectedCell ? '按键盘 1-9 输入数字' : '选择一个格子后输入数字'} {selectedCell ? (draftMode ? '草稿模式:按键盘 1-9 试填数字' : '按键盘 1-9 输入数字') : '选择一个格子后输入数字'}
</span> </span>
<span style={{ fontSize: '16px', color: '#666', display: 'block', marginBottom: '8px' }}> <span style={{ fontSize: '16px', color: '#666', display: 'block', marginBottom: '8px' }}>
<strong style={{ color: '#1890ff', fontSize: '20px' }}>{formatTime(timeElapsed)}</strong> <strong style={{ color: '#1890ff', fontSize: '20px' }}>{formatTime(timeElapsed)}</strong>
@@ -637,7 +700,7 @@ const SudokuGame: React.FC = () => {
{/* 快捷键提示 */} {/* 快捷键提示 */}
<div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f9f9f9', borderRadius: '4px', fontSize: '12px', color: '#666' }}> <div style={{ marginTop: '20px', padding: '10px', backgroundColor: '#f9f9f9', borderRadius: '4px', fontSize: '12px', color: '#666' }}>
<strong>:</strong> 1-9 | /Backspace | Ctrl/Cmd+Z | Ctrl/Cmd+Y | <strong>:</strong> 1-9 /稿 | /Backspace | Ctrl/Cmd+Z | Ctrl/Cmd+Y |
</div> </div>
{/* 完成成功对话框 */} {/* 完成成功对话框 */}