Sudoku / script.js
nayohan's picture
Update space
596f7cb
document.addEventListener('DOMContentLoaded', () => {
const BASE_PUZZLE = '530070000600195000098000060800060003400803001700020006060000280000419005000080079';
const TECHNIQUE_LABELS = {
'naked-single': '๋‹จ์ผ ํ›„๋ณด',
'hidden-single-row': 'ํ–‰ ์ˆจ์€ ์‹ฑ๊ธ€',
'hidden-single-col': '์—ด ์ˆจ์€ ์‹ฑ๊ธ€',
'hidden-single-box': '๋ฐ•์Šค ์ˆจ์€ ์‹ฑ๊ธ€'
};
const dom = {
board: document.getElementById('sudoku-board'),
newGameBtn: document.getElementById('new-game-btn'),
hintBtn: document.getElementById('hint-btn'),
applyHintBtn: document.getElementById('apply-hint-btn'),
checkBtn: document.getElementById('check-btn'),
resetBtn: document.getElementById('reset-btn'),
hintMessage: document.getElementById('hint-message'),
gameStatus: document.getElementById('game-status'),
puzzleMeta: document.getElementById('puzzle-meta'),
difficultyBadge: document.getElementById('difficulty-badge'),
filledCount: document.getElementById('filled-count'),
remainingCount: document.getElementById('remaining-count'),
nextTechnique: document.getElementById('next-technique'),
analysisSummary: document.getElementById('analysis-summary'),
logList: document.getElementById('log-list')
};
const state = {
board: [],
initial: [],
solution: [],
analysis: null,
pendingHint: null,
selected: null,
showMistakes: false,
hintMessage: 'ํžŒํŠธ ๋ณด๊ธฐ๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋‹ค์Œ ๋…ผ๋ฆฌ ๋‹จ๊ณ„๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.',
gameCount: 0,
learningLog: [],
cells: []
};
createBoard();
bindEvents();
startNewGame();
function bindEvents() {
dom.newGameBtn.addEventListener('click', startNewGame);
dom.hintBtn.addEventListener('click', requestHint);
dom.applyHintBtn.addEventListener('click', applyHint);
dom.checkBtn.addEventListener('click', toggleMistakes);
dom.resetBtn.addEventListener('click', resetBoard);
}
function createBoard() {
const fragment = document.createDocumentFragment();
for (let row = 0; row < 9; row += 1) {
const rowCells = [];
for (let col = 0; col < 9; col += 1) {
const input = document.createElement('input');
input.type = 'text';
input.className = 'cell';
input.maxLength = 1;
input.inputMode = 'numeric';
input.autocomplete = 'off';
input.pattern = '[1-9]';
input.spellcheck = false;
input.setAttribute('aria-describedby', 'sudoku-help');
input.dataset.row = String(row);
input.dataset.col = String(col);
input.setAttribute('role', 'gridcell');
input.setAttribute('aria-rowindex', String(row + 1));
input.setAttribute('aria-colindex', String(col + 1));
if (col === 2 || col === 5) {
input.classList.add('edge-right');
}
if (row === 2 || row === 5) {
input.classList.add('edge-bottom');
}
input.addEventListener('focus', onCellFocus);
input.addEventListener('click', onCellFocus);
input.addEventListener('input', onCellInput);
input.addEventListener('keydown', onCellKeyDown);
rowCells.push(input);
fragment.appendChild(input);
}
state.cells.push(rowCells);
}
dom.board.appendChild(fragment);
}
function startNewGame() {
const initial = generatePuzzle();
const solution = solveBoard(initial);
if (!solution) {
setStatus('ํผ์ฆ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ๊ณ ์นจ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•˜์„ธ์š”.');
return;
}
state.initial = cloneBoard(initial);
state.board = cloneBoard(initial);
state.solution = solution;
state.analysis = analyzePuzzle(initial);
state.pendingHint = null;
state.selected = findFirstEmpty(initial);
state.showMistakes = false;
state.hintMessage = 'ํžŒํŠธ ๋ณด๊ธฐ๋ฅผ ๋ˆ„๋ฅด๋ฉด ๋‹ค์Œ ๋…ผ๋ฆฌ ๋‹จ๊ณ„๋ฅผ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.';
state.gameCount += 1;
state.learningLog = [
{
kind: '๋ถ„์„',
message: describeAnalysis(state.analysis)
}
];
dom.checkBtn.textContent = '์˜ค๋‹ต ํ™•์ธ';
setStatus('์ƒˆ ํผ์ฆ์„ ๋ถˆ๋Ÿฌ์™”์Šต๋‹ˆ๋‹ค.');
render();
focusSelectedCell();
}
function resetBoard() {
state.board = cloneBoard(state.initial);
state.pendingHint = null;
state.showMistakes = false;
state.selected = findFirstEmpty(state.initial);
state.hintMessage = 'ํผ์ฆ์„ ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ๋˜๋Œ๋ ธ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋…ผ๋ฆฌ๋กœ ํ’€์–ด๋ณด์„ธ์š”.';
state.learningLog.unshift({ kind: '์ดˆ๊ธฐํ™”', message: '์ž…๋ ฅํ•œ ๊ฐ’์„ ์ง€์šฐ๊ณ  ์‹œ์ž‘ ์ƒํƒœ๋กœ ๋Œ์•„๊ฐ”์Šต๋‹ˆ๋‹ค.' });
state.learningLog = state.learningLog.slice(0, 8);
dom.checkBtn.textContent = '์˜ค๋‹ต ํ™•์ธ';
setStatus('ํผ์ฆ์„ ์ดˆ๊ธฐํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.');
render();
focusSelectedCell();
}
function requestHint() {
const conflicts = collectConflicts(state.board);
const wrongCells = collectWrongCells(state.board, state.solution);
if (conflicts.size > 0) {
state.pendingHint = null;
state.hintMessage = '๊ฐ™์€ ํ–‰, ์—ด ๋˜๋Š” ๋ฐ•์Šค์— ์ค‘๋ณต๋œ ์ˆซ์ž๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ถฉ๋Œ์„ ๋จผ์ € ์ •๋ฆฌํ•˜์„ธ์š”.';
setStatus('๊ทœ์น™ ์ถฉ๋Œ์ด ์žˆ์–ด ํžŒํŠธ๋ฅผ ์ค‘๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.');
render();
return;
}
if (wrongCells.length > 0) {
state.pendingHint = null;
state.showMistakes = true;
dom.checkBtn.textContent = '์˜ค๋‹ต ์ˆจ๊ธฐ๊ธฐ';
state.hintMessage = '์ •๋‹ต๊ณผ ๋‹ค๋ฅธ ์นธ์ด ์žˆ์–ด ํžŒํŠธ๋ฅผ ๋ฉˆ์ท„์Šต๋‹ˆ๋‹ค. ๋ถ‰์€ ํ‘œ์‹œ๋ฅผ ๋จผ์ € ๊ณ ์ณ๋ณด์„ธ์š”.';
setStatus('์˜ค๋‹ต์ด ์žˆ์–ด ํžŒํŠธ๋ฅผ ์ค‘๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.');
render();
return;
}
if (isSolved(state.board)) {
state.pendingHint = null;
state.hintMessage = '์ด๋ฏธ ํผ์ฆ์„ ๋ชจ๋‘ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ ํผ์ฆ์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.';
setStatus('ํผ์ฆ์ด ์ด๋ฏธ ์™„์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.');
render();
return;
}
const step = findNextLogicalStep(state.board);
if (!step) {
state.pendingHint = null;
state.hintMessage = 'ํ˜„์žฌ MVP ๊ทœ์น™(๋‹จ์ผ ํ›„๋ณด, ์ˆจ์€ ์‹ฑ๊ธ€)๋กœ๋Š” ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.';
setStatus('์ถ”๊ฐ€ ๊ทœ์น™์ด ํ•„์š”ํ•œ ๊ตฌ๊ฐ„์ž…๋‹ˆ๋‹ค.');
render();
return;
}
state.pendingHint = step;
state.selected = { row: step.row, col: step.col };
state.hintMessage = `${TECHNIQUE_LABELS[step.type]}: ${step.detail}`;
recordLog('ํžŒํŠธ', `${cellRef(step.row, step.col)} = ${step.value} ยท ${TECHNIQUE_LABELS[step.type]}`);
setStatus('๋‹ค์Œ ๋…ผ๋ฆฌ ๋‹จ๊ณ„๋ฅผ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.');
render();
focusSelectedCell();
}
function applyHint() {
if (!state.pendingHint) {
requestHint();
return;
}
const { row, col, value, type } = state.pendingHint;
state.board[row][col] = value;
state.selected = { row, col };
state.hintMessage = `${cellRef(row, col)}์— ${value}๋ฅผ ๋„ฃ์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„๊ฐ€ ํ•„์š”ํ•˜๋ฉด ๋‹ค์‹œ ํžŒํŠธ๋ฅผ ์š”์ฒญํ•˜์„ธ์š”.`;
recordLog('์ ์šฉ', `${cellRef(row, col)} = ${value} ยท ${TECHNIQUE_LABELS[type]}`);
state.pendingHint = null;
if (boardsEqual(state.board, state.solution)) {
state.hintMessage = 'ํผ์ฆ์„ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ ํผ์ฆ๋กœ ๋‹ค์‹œ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.';
setStatus('์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์นธ์ด ๋งž์Šต๋‹ˆ๋‹ค.');
recordLog('์™„๋ฃŒ', '๋ชจ๋“  ์นธ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฑ„์› ์Šต๋‹ˆ๋‹ค.');
} else {
setStatus('ํžŒํŠธ๋ฅผ ์ ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.');
}
render();
focusSelectedCell();
}
function toggleMistakes() {
state.showMistakes = !state.showMistakes;
dom.checkBtn.textContent = state.showMistakes ? '์˜ค๋‹ต ์ˆจ๊ธฐ๊ธฐ' : '์˜ค๋‹ต ํ™•์ธ';
setStatus(state.showMistakes ? '์˜ค๋‹ต ํ‘œ์‹œ๋ฅผ ์ผฐ์Šต๋‹ˆ๋‹ค.' : '์˜ค๋‹ต ํ‘œ์‹œ๋ฅผ ๊ป์Šต๋‹ˆ๋‹ค.');
render();
}
function onCellFocus(event) {
const row = Number(event.target.dataset.row);
const col = Number(event.target.dataset.col);
state.selected = { row, col };
renderBoard();
}
function onCellInput(event) {
const input = event.target;
const row = Number(input.dataset.row);
const col = Number(input.dataset.col);
const previousValue = state.board[row][col];
if (state.initial[row][col] !== 0) {
input.value = String(state.initial[row][col]);
return;
}
const raw = input.value.replace(/[^1-9]/g, '').slice(-1);
input.value = raw;
state.board[row][col] = raw ? Number(raw) : 0;
state.pendingHint = null;
state.selected = { row, col };
const conflicts = collectConflicts(state.board);
const wrongCells = collectWrongCells(state.board, state.solution);
if (boardsEqual(state.board, state.solution)) {
state.hintMessage = 'ํผ์ฆ์„ ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ ํผ์ฆ๋กœ ๋‹ค์‹œ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.';
setStatus('์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์นธ์ด ๋งž์Šต๋‹ˆ๋‹ค.');
recordLog('์™„๋ฃŒ', '๋ชจ๋“  ์นธ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฑ„์› ์Šต๋‹ˆ๋‹ค.');
} else if (!raw && previousValue !== 0) {
setStatus('์นธ์„ ๋น„์› ์Šต๋‹ˆ๋‹ค.');
} else if (!raw) {
setStatus('1๋ถ€ํ„ฐ 9๊นŒ์ง€์˜ ์ˆซ์ž๋งŒ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.');
} else if (conflicts.size > 0) {
setStatus('๊ทœ์น™ ์ถฉ๋Œ์ด ์žˆ์Šต๋‹ˆ๋‹ค.');
} else if (wrongCells.length > 0 && countFilled(state.board) === 81) {
setStatus('์˜ค๋‹ต์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ค๋‹ต ํ™•์ธ์œผ๋กœ ์ ๊ฒ€ํ•˜์„ธ์š”.');
} else {
const filled = countFilled(state.board);
setStatus(`${filled}์นธ์„ ์ฑ„์› ์Šต๋‹ˆ๋‹ค.`);
}
render();
}
function onCellKeyDown(event) {
const row = Number(event.target.dataset.row);
const col = Number(event.target.dataset.col);
let nextRow = row;
let nextCol = col;
if (event.key === 'ArrowUp') {
nextRow = Math.max(0, row - 1);
} else if (event.key === 'ArrowDown') {
nextRow = Math.min(8, row + 1);
} else if (event.key === 'ArrowLeft') {
nextCol = Math.max(0, col - 1);
} else if (event.key === 'ArrowRight') {
nextCol = Math.min(8, col + 1);
} else if (event.key === 'Backspace' || event.key === 'Delete' || event.key === 'Escape' || event.key === '0' || event.key === ' ') {
if (state.initial[row][col] === 0) {
state.board[row][col] = 0;
state.pendingHint = null;
state.selected = { row, col };
setStatus('์นธ์„ ๋น„์› ์Šต๋‹ˆ๋‹ค.');
render();
}
event.preventDefault();
return;
} else {
return;
}
event.preventDefault();
state.selected = { row: nextRow, col: nextCol };
focusSelectedCell();
renderBoard();
}
function render() {
renderBoard();
renderSidebar();
updateActionButtons();
}
function updateActionButtons() {
const isComplete = boardsEqual(state.board, state.solution);
dom.hintBtn.disabled = isComplete;
dom.applyHintBtn.disabled = !state.pendingHint || isComplete;
dom.resetBtn.disabled = boardsEqual(state.board, state.initial);
dom.checkBtn.textContent = state.showMistakes ? '์˜ค๋‹ต ์ˆจ๊ธฐ๊ธฐ' : '์˜ค๋‹ต ํ™•์ธ';
dom.checkBtn.setAttribute('aria-pressed', String(state.showMistakes));
}
function renderBoard() {
const conflicts = collectConflicts(state.board);
const wrongCells = state.showMistakes ? new Set(collectWrongCells(state.board, state.solution)) : new Set();
for (let row = 0; row < 9; row += 1) {
for (let col = 0; col < 9; col += 1) {
const input = state.cells[row][col];
const value = state.board[row][col];
const key = toKey(row, col);
const isGiven = state.initial[row][col] !== 0;
const isSelected = Boolean(state.selected && state.selected.row === row && state.selected.col === col);
const isHint = Boolean(state.pendingHint && state.pendingHint.row === row && state.pendingHint.col === col);
const hasConflict = conflicts.has(key);
const hasMistake = wrongCells.has(key);
input.value = value === 0 ? '' : String(value);
input.readOnly = isGiven;
input.classList.toggle('is-given', isGiven);
input.classList.toggle('is-selected', isSelected);
input.classList.toggle('is-hint', isHint);
input.classList.toggle('is-conflict', hasConflict);
input.classList.toggle('is-mistake', hasMistake);
input.setAttribute('aria-label', buildCellLabel(row, col, value, isGiven));
input.setAttribute('aria-selected', String(isSelected));
input.setAttribute('aria-invalid', String(hasConflict || hasMistake));
input.setAttribute('aria-readonly', String(isGiven));
}
}
}
function renderSidebar() {
const filled = countFilled(state.board);
const conflicts = collectConflicts(state.board);
const wrongCells = collectWrongCells(state.board, state.solution);
const isComplete = boardsEqual(state.board, state.solution);
let nextTechniqueLabel = '๋Œ€๊ธฐ ์ค‘';
if (isComplete) {
nextTechniqueLabel = '์™„๋ฃŒ';
} else if (conflicts.size > 0) {
nextTechniqueLabel = '์ถฉ๋Œ ์ •๋ฆฌ ํ•„์š”';
} else if (wrongCells.length > 0) {
nextTechniqueLabel = '์˜ค๋‹ต ์ˆ˜์ • ํ•„์š”';
} else {
const nextStep = findNextLogicalStep(state.board);
nextTechniqueLabel = nextStep ? TECHNIQUE_LABELS[nextStep.type] : 'ํ˜„์žฌ ๊ทœ์น™์œผ๋กœ ์ฐพ์ง€ ๋ชปํ•จ';
}
dom.puzzleMeta.textContent = `${state.gameCount}๋ฒˆ์งธ ํผ์ฆ ยท ๋žœ๋ค ๋ณ€ํ˜• ํ•™์Šต ํผ์ฆ`;
dom.difficultyBadge.textContent = state.analysis.difficulty;
dom.hintMessage.textContent = state.hintMessage;
dom.filledCount.textContent = String(filled);
dom.remainingCount.textContent = String(81 - filled);
dom.nextTechnique.textContent = nextTechniqueLabel;
dom.analysisSummary.textContent = describeAnalysis(state.analysis);
dom.logList.innerHTML = '';
const items = state.learningLog.length > 0 ? state.learningLog : [{ kind: '์•ˆ๋‚ด', message: 'ํžŒํŠธ ๋กœ๊ทธ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค.' }];
items.slice(0, 8).forEach((item) => {
const li = document.createElement('li');
li.textContent = `${item.kind} ยท ${item.message}`;
dom.logList.appendChild(li);
});
}
function setStatus(message) {
dom.gameStatus.textContent = message;
}
function recordLog(kind, message) {
const latest = `${kind}:${message}`;
const exists = state.learningLog.some((entry) => `${entry.kind}:${entry.message}` === latest);
if (!exists) {
state.learningLog.unshift({ kind, message });
state.learningLog = state.learningLog.slice(0, 8);
}
}
function buildCellLabel(row, col, value, isGiven) {
if (isGiven) {
return `${row + 1}ํ–‰ ${col + 1}์—ด, ๊ณ ์ • ์ˆซ์ž ${value}`;
}
if (value === 0) {
return `${row + 1}ํ–‰ ${col + 1}์—ด, ๋นˆ์นธ`;
}
return `${row + 1}ํ–‰ ${col + 1}์—ด, ์ž…๋ ฅ๊ฐ’ ${value}`;
}
function focusSelectedCell() {
if (!state.selected) {
return;
}
const cell = state.cells[state.selected.row][state.selected.col];
if (cell) {
cell.focus();
}
}
function analyzePuzzle(board) {
const solverResult = buildSolverLog(board);
const counts = solverResult.steps.reduce((acc, step) => {
acc[step.type] = (acc[step.type] || 0) + 1;
return acc;
}, {});
return {
steps: solverResult.steps,
solved: solverResult.solved,
counts,
difficulty: classifyDifficulty(solverResult.solved, counts)
};
}
function describeAnalysis(analysis) {
const countSummary = formatTechniqueCounts(analysis.counts);
if (analysis.solved) {
return `${analysis.steps.length}๋‹จ๊ณ„ ๋…ผ๋ฆฌ ํ’€์ด ยท ${countSummary}`;
}
return `์ผ๋ถ€๋งŒ ๋…ผ๋ฆฌ ํ’€์ด ๊ฐ€๋Šฅ ยท ${countSummary}`;
}
function formatTechniqueCounts(counts) {
const parts = [];
if (counts['naked-single']) {
parts.push(`๋‹จ์ผ ํ›„๋ณด ${counts['naked-single']}ํšŒ`);
}
if (counts['hidden-single-row']) {
parts.push(`ํ–‰ ์ˆจ์€ ์‹ฑ๊ธ€ ${counts['hidden-single-row']}ํšŒ`);
}
if (counts['hidden-single-col']) {
parts.push(`์—ด ์ˆจ์€ ์‹ฑ๊ธ€ ${counts['hidden-single-col']}ํšŒ`);
}
if (counts['hidden-single-box']) {
parts.push(`๋ฐ•์Šค ์ˆจ์€ ์‹ฑ๊ธ€ ${counts['hidden-single-box']}ํšŒ`);
}
return parts.length > 0 ? parts.join(', ') : '๊ธฐ๋ณธ ๊ทœ์น™ ๋ถ„์„ ์—†์Œ';
}
function classifyDifficulty(solved, counts) {
const hiddenCount = (counts['hidden-single-row'] || 0) + (counts['hidden-single-col'] || 0) + (counts['hidden-single-box'] || 0);
if (!solved) {
return '์—ฐ์Šต';
}
if (hiddenCount === 0) {
return '์ž…๋ฌธ';
}
if (hiddenCount <= 8) {
return '์ดˆ๊ธ‰';
}
return '์ค‘๊ธ‰';
}
function buildSolverLog(board) {
const working = cloneBoard(board);
const steps = [];
let guard = 0;
while (guard < 100) {
if (isSolved(working)) {
return { solved: true, steps };
}
const next = findNextLogicalStep(working);
if (!next) {
break;
}
working[next.row][next.col] = next.value;
steps.push(next);
guard += 1;
}
return { solved: isSolved(working), steps };
}
function findNextLogicalStep(board) {
const candidates = [];
for (let row = 0; row < 9; row += 1) {
candidates[row] = [];
for (let col = 0; col < 9; col += 1) {
candidates[row][col] = board[row][col] === 0 ? getCandidates(board, row, col) : [];
}
}
for (let row = 0; row < 9; row += 1) {
for (let col = 0; col < 9; col += 1) {
if (board[row][col] !== 0) {
continue;
}
if (candidates[row][col].length === 1) {
const value = candidates[row][col][0];
return {
type: 'naked-single',
row,
col,
value,
detail: `${cellRef(row, col)}์˜ ํ›„๋ณด๋Š” ${value} ํ•˜๋‚˜๋ฟ์ž…๋‹ˆ๋‹ค. ํ–‰, ์—ด, ๋ฐ•์Šค์—์„œ ๋‹ค๋ฅธ ์ˆซ์ž๊ฐ€ ๋ชจ๋‘ ์ œ์™ธ๋ฉ๋‹ˆ๋‹ค.`
};
}
}
}
for (let row = 0; row < 9; row += 1) {
for (let value = 1; value <= 9; value += 1) {
const spots = [];
for (let col = 0; col < 9; col += 1) {
if (candidates[row][col].includes(value)) {
spots.push({ row, col });
}
}
if (spots.length === 1) {
return {
type: 'hidden-single-row',
row: spots[0].row,
col: spots[0].col,
value,
detail: `${row + 1}ํ–‰์—์„œ ์ˆซ์ž ${value}๊ฐ€ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š” ์นธ์€ ${cellRef(spots[0].row, spots[0].col)} ํ•˜๋‚˜๋ฟ์ž…๋‹ˆ๋‹ค.`
};
}
}
}
for (let col = 0; col < 9; col += 1) {
for (let value = 1; value <= 9; value += 1) {
const spots = [];
for (let row = 0; row < 9; row += 1) {
if (candidates[row][col].includes(value)) {
spots.push({ row, col });
}
}
if (spots.length === 1) {
return {
type: 'hidden-single-col',
row: spots[0].row,
col: spots[0].col,
value,
detail: `${col + 1}์—ด์—์„œ ์ˆซ์ž ${value}๊ฐ€ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š” ์นธ์€ ${cellRef(spots[0].row, spots[0].col)} ํ•˜๋‚˜๋ฟ์ž…๋‹ˆ๋‹ค.`
};
}
}
}
for (let box = 0; box < 9; box += 1) {
const startRow = Math.floor(box / 3) * 3;
const startCol = (box % 3) * 3;
for (let value = 1; value <= 9; value += 1) {
const spots = [];
for (let row = startRow; row < startRow + 3; row += 1) {
for (let col = startCol; col < startCol + 3; col += 1) {
if (candidates[row][col].includes(value)) {
spots.push({ row, col });
}
}
}
if (spots.length === 1) {
return {
type: 'hidden-single-box',
row: spots[0].row,
col: spots[0].col,
value,
detail: `${box + 1}๋ฒˆ ๋ฐ•์Šค์—์„œ ์ˆซ์ž ${value}๊ฐ€ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š” ์นธ์€ ${cellRef(spots[0].row, spots[0].col)} ํ•˜๋‚˜๋ฟ์ž…๋‹ˆ๋‹ค.`
};
}
}
}
return null;
}
function generatePuzzle() {
let board = parseBoard(BASE_PUZZLE);
board = remapDigits(board);
board = shuffleRows(board);
board = shuffleCols(board);
if (Math.random() > 0.5) {
board = transpose(board);
}
return board;
}
function remapDigits(board) {
const digits = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9]);
const map = new Map();
for (let i = 1; i <= 9; i += 1) {
map.set(i, digits[i - 1]);
}
return board.map((row) => row.map((value) => (value === 0 ? 0 : map.get(value))));
}
function shuffleRows(board) {
const bands = shuffle([0, 1, 2]);
const order = [];
bands.forEach((band) => {
shuffle([0, 1, 2]).forEach((offset) => order.push(band * 3 + offset));
});
return order.map((rowIndex) => board[rowIndex].slice());
}
function shuffleCols(board) {
const stacks = shuffle([0, 1, 2]);
const order = [];
stacks.forEach((stack) => {
shuffle([0, 1, 2]).forEach((offset) => order.push(stack * 3 + offset));
});
return board.map((row) => order.map((colIndex) => row[colIndex]));
}
function transpose(board) {
return board[0].map((_, col) => board.map((row) => row[col]));
}
function shuffle(values) {
const copy = values.slice();
for (let i = copy.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
}
function solveBoard(board) {
const working = cloneBoard(board);
return solveRecursive(working) ? working : null;
}
function solveRecursive(board) {
let bestCell = null;
let bestCandidates = null;
for (let row = 0; row < 9; row += 1) {
for (let col = 0; col < 9; col += 1) {
if (board[row][col] !== 0) {
continue;
}
const candidates = getCandidates(board, row, col);
if (candidates.length === 0) {
return false;
}
if (!bestCandidates || candidates.length < bestCandidates.length) {
bestCell = { row, col };
bestCandidates = candidates;
}
}
}
if (!bestCell) {
return true;
}
for (const value of bestCandidates) {
board[bestCell.row][bestCell.col] = value;
if (solveRecursive(board)) {
return true;
}
}
board[bestCell.row][bestCell.col] = 0;
return false;
}
function getCandidates(board, row, col) {
if (board[row][col] !== 0) {
return [];
}
const candidates = [];
for (let value = 1; value <= 9; value += 1) {
if (isValidPlacement(board, row, col, value)) {
candidates.push(value);
}
}
return candidates;
}
function isValidPlacement(board, row, col, value) {
for (let index = 0; index < 9; index += 1) {
if (board[row][index] === value) {
return false;
}
if (board[index][col] === value) {
return false;
}
}
const startRow = Math.floor(row / 3) * 3;
const startCol = Math.floor(col / 3) * 3;
for (let r = startRow; r < startRow + 3; r += 1) {
for (let c = startCol; c < startCol + 3; c += 1) {
if (board[r][c] === value) {
return false;
}
}
}
return true;
}
function collectConflicts(board) {
const conflicts = new Set();
for (let row = 0; row < 9; row += 1) {
markHouseConflicts(conflicts, Array.from({ length: 9 }, (_, col) => ({ row, col, value: board[row][col] })));
}
for (let col = 0; col < 9; col += 1) {
markHouseConflicts(conflicts, Array.from({ length: 9 }, (_, row) => ({ row, col, value: board[row][col] })));
}
for (let box = 0; box < 9; box += 1) {
const startRow = Math.floor(box / 3) * 3;
const startCol = (box % 3) * 3;
const cells = [];
for (let row = startRow; row < startRow + 3; row += 1) {
for (let col = startCol; col < startCol + 3; col += 1) {
cells.push({ row, col, value: board[row][col] });
}
}
markHouseConflicts(conflicts, cells);
}
return conflicts;
}
function markHouseConflicts(conflicts, cells) {
const buckets = new Map();
cells.forEach((cell) => {
if (cell.value === 0) {
return;
}
if (!buckets.has(cell.value)) {
buckets.set(cell.value, []);
}
buckets.get(cell.value).push(cell);
});
buckets.forEach((items) => {
if (items.length > 1) {
items.forEach((cell) => conflicts.add(toKey(cell.row, cell.col)));
}
});
}
function collectWrongCells(board, solution) {
const wrong = [];
for (let row = 0; row < 9; row += 1) {
for (let col = 0; col < 9; col += 1) {
if (board[row][col] !== 0 && board[row][col] !== solution[row][col]) {
wrong.push(toKey(row, col));
}
}
}
return wrong;
}
function findFirstEmpty(board) {
for (let row = 0; row < 9; row += 1) {
for (let col = 0; col < 9; col += 1) {
if (board[row][col] === 0) {
return { row, col };
}
}
}
return { row: 0, col: 0 };
}
function isSolved(board) {
return board.every((row) => row.every((value) => value !== 0));
}
function countFilled(board) {
return board.flat().filter((value) => value !== 0).length;
}
function boardsEqual(a, b) {
for (let row = 0; row < 9; row += 1) {
for (let col = 0; col < 9; col += 1) {
if (a[row][col] !== b[row][col]) {
return false;
}
}
}
return true;
}
function cloneBoard(board) {
return board.map((row) => row.slice());
}
function parseBoard(serialized) {
const board = [];
for (let row = 0; row < 9; row += 1) {
const currentRow = [];
for (let col = 0; col < 9; col += 1) {
currentRow.push(Number(serialized[row * 9 + col]));
}
board.push(currentRow);
}
return board;
}
function toKey(row, col) {
return `${row}-${col}`;
}
function cellRef(row, col) {
return `${row + 1}ํ–‰ ${col + 1}์—ด`;
}
});