added draft mode
This commit is contained in:
@@ -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>
|
||||||
|
|
||||||
{/* 完成成功对话框 */}
|
{/* 完成成功对话框 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user