add hint
This commit is contained in:
@@ -27,6 +27,7 @@ const CELL_SIZE = 80;
|
|||||||
const CELL_FONT_SIZE = 36;
|
const CELL_FONT_SIZE = 36;
|
||||||
const DRAFT_SIZE = CELL_SIZE / 2;
|
const DRAFT_SIZE = CELL_SIZE / 2;
|
||||||
const DRAFT_FONT_SIZE = CELL_FONT_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 createSudokuRound = (difficulty: Difficulty, mode: GameMode): SudokuRound => {
|
||||||
const { puzzle, regionMap } = generatePuzzleForMode(difficulty, mode);
|
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 cloneGameBoard = (board: GameBoard): GameBoard => board.map(row => [...row]);
|
||||||
const cloneDraftCells = (draftCells: DraftCells): DraftCells => draftCells.map(row => [...row]);
|
const cloneDraftCells = (draftCells: DraftCells): DraftCells => draftCells.map(row => [...row]);
|
||||||
const createEmptyDraftCells = (): DraftCells => Array.from({ length: 9 }, () => Array(9).fill(false));
|
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 = () => {
|
const SudokuGame: React.FC = () => {
|
||||||
// 游戏难度
|
// 游戏难度
|
||||||
@@ -429,13 +474,25 @@ const SudokuGame: React.FC = () => {
|
|||||||
for (let r = 0; r < 9; r++) {
|
for (let r = 0; r < 9; r++) {
|
||||||
for (let c = 0; c < 9; c++) {
|
for (let c = 0; c < 9; c++) {
|
||||||
if (getCellHighlightType(r, c) !== 'none') {
|
if (getCellHighlightType(r, c) !== 'none') {
|
||||||
highlighted.add(`${r}-${c}`);
|
highlighted.add(getCellKey(r, c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return highlighted;
|
return highlighted;
|
||||||
}, [board, selectedCell, config, regionMap]);
|
}, [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(() => {
|
const remainingCount = useMemo(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
@@ -453,11 +510,13 @@ const SudokuGame: React.FC = () => {
|
|||||||
const isFixed = fixedCells[row][col];
|
const isFixed = fixedCells[row][col];
|
||||||
const highlightType = getCellHighlightType(row, col);
|
const highlightType = getCellHighlightType(row, col);
|
||||||
const isSelected = selectedCell && selectedCell[0] === row && selectedCell[1] === col;
|
const isSelected = selectedCell && selectedCell[0] === row && selectedCell[1] === col;
|
||||||
const displayValue = getCellValue(cell);
|
const isConflicting = conflictCells.has(getCellKey(row, col));
|
||||||
|
|
||||||
// 基础背景色 - 根据高亮类型设置不同颜色
|
// 基础背景色 - 根据高亮类型设置不同颜色
|
||||||
let backgroundColor = 'white';
|
let backgroundColor = 'white';
|
||||||
if (isSelected) {
|
if (isConflicting) {
|
||||||
|
backgroundColor = '#fff1f0'; // 冲突数字 - 警告红色
|
||||||
|
} else if (isSelected) {
|
||||||
backgroundColor = '#69b1ff'; // 选中时的深蓝色
|
backgroundColor = '#69b1ff'; // 选中时的深蓝色
|
||||||
} else if (highlightType === 'box') {
|
} else if (highlightType === 'box') {
|
||||||
backgroundColor = '#d9f7be'; // 宫 - 浅绿色
|
backgroundColor = '#d9f7be'; // 宫 - 浅绿色
|
||||||
@@ -495,9 +554,12 @@ const SudokuGame: React.FC = () => {
|
|||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
fontSize: `${CELL_FONT_SIZE}px`,
|
fontSize: `${CELL_FONT_SIZE}px`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
color: isFixed ? '#333' : '#1890ff',
|
color: isConflicting ? '#cf1322' : isFixed ? '#333' : '#1890ff',
|
||||||
position: 'relative' as const,
|
position: 'relative' as const,
|
||||||
boxSizing: 'border-box',
|
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',
|
justifyContent: 'center',
|
||||||
fontSize: `${isDraft ? DRAFT_FONT_SIZE : CELL_FONT_SIZE}px`,
|
fontSize: `${isDraft ? DRAFT_FONT_SIZE : CELL_FONT_SIZE}px`,
|
||||||
lineHeight: 1,
|
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 (
|
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' }}>
|
||||||
{/* 顶部控制和配置区 */}
|
{/* 顶部控制和配置区 */}
|
||||||
@@ -666,13 +744,19 @@ const SudokuGame: React.FC = () => {
|
|||||||
row.map((cell, colIndex) => {
|
row.map((cell, colIndex) => {
|
||||||
const displayValue = getCellValue(cell);
|
const displayValue = getCellValue(cell);
|
||||||
const cellStyle = getCellStyle(rowIndex, colIndex);
|
const cellStyle = getCellStyle(rowIndex, colIndex);
|
||||||
|
const candidates = availableNumbers[rowIndex][colIndex];
|
||||||
|
const isConflicting = conflictCells.has(getCellKey(rowIndex, colIndex));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${rowIndex}-${colIndex}`}
|
key={`${rowIndex}-${colIndex}`}
|
||||||
onClick={() => handleCellClick(rowIndex, colIndex)}
|
onClick={() => handleCellClick(rowIndex, colIndex)}
|
||||||
style={cellStyle}
|
style={cellStyle}
|
||||||
|
title={isConflicting ? '该数字与同一行、列或区域中的数字冲突' : undefined}
|
||||||
>
|
>
|
||||||
|
{displayValue === null && candidates.length > 0 && (
|
||||||
|
<span style={getAvailableNumbersStyle()}>{candidates.join('')}</span>
|
||||||
|
)}
|
||||||
{displayValue !== null ? (
|
{displayValue !== null ? (
|
||||||
<span style={getCellValueStyle(rowIndex, colIndex)}>{displayValue}</span>
|
<span style={getCellValueStyle(rowIndex, colIndex)}>{displayValue}</span>
|
||||||
) : ''}
|
) : ''}
|
||||||
|
|||||||
Reference in New Issue
Block a user