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