This commit is contained in:
2026-05-19 15:48:45 +08:00
parent 5567bf572f
commit 257cee6fb2

View File

@@ -27,6 +27,7 @@ const CELL_SIZE = 80;
const CELL_FONT_SIZE = 36;
const DRAFT_SIZE = CELL_SIZE / 2;
const DRAFT_FONT_SIZE = CELL_FONT_SIZE / 2;
const DIGITS = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const createSudokuRound = (difficulty: Difficulty, mode: GameMode): SudokuRound => {
const { puzzle, regionMap } = generatePuzzleForMode(difficulty, mode);
@@ -39,6 +40,50 @@ 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 getCellKey = (row: number, col: number): string => `${row}-${col}`;
const isFilledDigit = (value: number): boolean => value >= 1 && value <= 9;
const findConflictCells = (boardToCheck: GameBoard, regionMapToCheck: RegionMap): Set<string> => {
const conflictCells = new Set<string>();
const addGroupConflicts = (cells: Array<[number, number]>) => {
const cellsByValue = new Map<number, string[]>();
cells.forEach(([row, col]) => {
const value = boardToCheck[row][col];
if (!isFilledDigit(value)) return;
const keys = cellsByValue.get(value) ?? [];
keys.push(getCellKey(row, col));
cellsByValue.set(value, keys);
});
cellsByValue.forEach((keys) => {
if (keys.length <= 1) return;
keys.forEach((key) => conflictCells.add(key));
});
};
for (let row = 0; row < 9; row++) {
addGroupConflicts(Array.from({ length: 9 }, (_, col) => [row, col]));
}
for (let col = 0; col < 9; col++) {
addGroupConflicts(Array.from({ length: 9 }, (_, row) => [row, col]));
}
const cellsByRegion = Array.from({ length: 9 }, () => [] as Array<[number, number]>);
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
cellsByRegion[regionMapToCheck[row][col]].push([row, col]);
}
}
cellsByRegion.forEach(addGroupConflicts);
return conflictCells;
};
const SudokuGame: React.FC = () => {
// 游戏难度
@@ -429,13 +474,25 @@ const SudokuGame: React.FC = () => {
for (let r = 0; r < 9; r++) {
for (let c = 0; c < 9; c++) {
if (getCellHighlightType(r, c) !== 'none') {
highlighted.add(`${r}-${c}`);
highlighted.add(getCellKey(r, c));
}
}
}
return highlighted;
}, [board, selectedCell, config, regionMap]);
const conflictCells = useMemo(() => findConflictCells(board, regionMap), [board, regionMap]);
const availableNumbers = useMemo(() => {
return board.map((row, rowIndex) =>
row.map((cell, colIndex) => (
cell === 0
? DIGITS.filter((num) => isValidInBoard(rowIndex, colIndex, num, board))
: []
))
);
}, [board, regionMap]);
// 计算剩余未填写的数字数量
const remainingCount = useMemo(() => {
let count = 0;
@@ -453,11 +510,13 @@ const SudokuGame: React.FC = () => {
const isFixed = fixedCells[row][col];
const highlightType = getCellHighlightType(row, col);
const isSelected = selectedCell && selectedCell[0] === row && selectedCell[1] === col;
const displayValue = getCellValue(cell);
const isConflicting = conflictCells.has(getCellKey(row, col));
// 基础背景色 - 根据高亮类型设置不同颜色
let backgroundColor = 'white';
if (isSelected) {
if (isConflicting) {
backgroundColor = '#fff1f0'; // 冲突数字 - 警告红色
} else if (isSelected) {
backgroundColor = '#69b1ff'; // 选中时的深蓝色
} else if (highlightType === 'box') {
backgroundColor = '#d9f7be'; // 宫 - 浅绿色
@@ -495,9 +554,12 @@ const SudokuGame: React.FC = () => {
justifyContent: 'center',
fontSize: `${CELL_FONT_SIZE}px`,
cursor: 'pointer',
color: isFixed ? '#333' : '#1890ff',
color: isConflicting ? '#cf1322' : isFixed ? '#333' : '#1890ff',
position: 'relative' as const,
boxSizing: 'border-box',
boxShadow: isConflicting ? 'inset 0 0 0 3px #ff4d4f' : undefined,
outline: isSelected ? '3px solid #1677ff' : undefined,
outlineOffset: '-6px',
};
};
@@ -513,9 +575,25 @@ const SudokuGame: React.FC = () => {
justifyContent: 'center',
fontSize: `${isDraft ? DRAFT_FONT_SIZE : CELL_FONT_SIZE}px`,
lineHeight: 1,
position: 'relative' as const,
zIndex: 1,
};
};
const getAvailableNumbersStyle = (): React.CSSProperties => ({
position: 'absolute' as const,
top: '4px',
right: '5px',
maxWidth: '46px',
fontSize: '11px',
lineHeight: 1,
color: '#8c8c8c',
textAlign: 'right' as const,
pointerEvents: 'none' as const,
zIndex: 2,
fontVariantNumeric: 'tabular-nums',
});
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', padding: '20px' }}>
{/* 顶部控制和配置区 */}
@@ -666,13 +744,19 @@ const SudokuGame: React.FC = () => {
row.map((cell, colIndex) => {
const displayValue = getCellValue(cell);
const cellStyle = getCellStyle(rowIndex, colIndex);
const candidates = availableNumbers[rowIndex][colIndex];
const isConflicting = conflictCells.has(getCellKey(rowIndex, colIndex));
return (
<div
key={`${rowIndex}-${colIndex}`}
onClick={() => handleCellClick(rowIndex, colIndex)}
style={cellStyle}
title={isConflicting ? '该数字与同一行、列或区域中的数字冲突' : undefined}
>
{displayValue === null && candidates.length > 0 && (
<span style={getAvailableNumbersStyle()}>{candidates.join('')}</span>
)}
{displayValue !== null ? (
<span style={getCellValueStyle(rowIndex, colIndex)}>{displayValue}</span>
) : ''}