Refactor solver worker to return translation keys and centralize i18n
This commit is contained in:
@@ -199,7 +199,15 @@ const messages = {
|
||||
'simulation.table.solved': 'Rozwiązano (Logika)',
|
||||
'simulation.empty': 'Naciśnij Start, aby uruchomić symulację Monte Carlo',
|
||||
'difficultyMap.size': 'Rozmiar',
|
||||
'difficultyMap.density': 'Gęstość'
|
||||
'difficultyMap.density': 'Gęstość',
|
||||
'worker.solved': 'Rozwiązane!',
|
||||
'worker.logicRow': 'Logika: Wiersz {row}, Kolumna {col} -> {state}',
|
||||
'worker.logicCol': 'Logika: Kolumna {col}, Wiersz {row} -> {state}',
|
||||
'worker.stuck': 'Brak logicznego ruchu. Spróbuj zgadnąć lub cofnąć.',
|
||||
'worker.boosted': 'Boost (DFS): Wiersz {row}, Kolumna {col} -> {state}',
|
||||
'worker.done': 'Koniec!',
|
||||
'worker.state.filled': 'Pełne',
|
||||
'worker.state.empty': 'Puste'
|
||||
},
|
||||
en: {
|
||||
'app.title': 'Nonograms',
|
||||
@@ -399,7 +407,15 @@ const messages = {
|
||||
'custom.hideMap': 'Hide difficulty map',
|
||||
'custom.showMap': 'Show difficulty map',
|
||||
'difficultyMap.size': 'Size',
|
||||
'difficultyMap.density': 'Density'
|
||||
'difficultyMap.density': 'Density',
|
||||
'worker.solved': 'Solved!',
|
||||
'worker.logicRow': 'Logic: Row {row}, Column {col} -> {state}',
|
||||
'worker.logicCol': 'Logic: Column {col}, Row {row} -> {state}',
|
||||
'worker.stuck': 'No logical move found. Try guessing or undoing.',
|
||||
'worker.boosted': 'Boost (DFS): Row {row}, Column {col} -> {state}',
|
||||
'worker.done': 'Done!',
|
||||
'worker.state.filled': 'Filled',
|
||||
'worker.state.empty': 'Empty'
|
||||
},
|
||||
zh: {
|
||||
'app.title': 'Nonograms',
|
||||
|
||||
@@ -97,10 +97,20 @@ export function useSolver() {
|
||||
|
||||
function ensureWorker() {
|
||||
if (worker) return;
|
||||
worker = new Worker(new URL('../workers/solverWorker.js', import.meta.url), { type: 'module' });
|
||||
worker = new Worker(new URL('../workers/solver.worker.js', import.meta.url), { type: 'module' });
|
||||
worker.onmessage = (event) => {
|
||||
const { type, r, c, state, statusText: text } = event.data;
|
||||
if (text) statusText.value = text;
|
||||
const { type, r, c, state, status } = event.data;
|
||||
if (status) {
|
||||
const params = status.params ? { ...status.params } : {};
|
||||
if (params.stateKey) {
|
||||
params.state = t(params.stateKey);
|
||||
}
|
||||
statusText.value = t(status.key, params);
|
||||
} else if (event.data.statusText) {
|
||||
// Fallback for legacy messages if any
|
||||
statusText.value = event.data.statusText;
|
||||
}
|
||||
|
||||
if (type === 'move') {
|
||||
store.setCell(r, c, state);
|
||||
isProcessing.value = false;
|
||||
|
||||
@@ -1,61 +1,240 @@
|
||||
import { calculateHints } from '../utils/puzzleUtils';
|
||||
import { solvePuzzle } from '../utils/solver';
|
||||
import { calculateHints } from '../utils/puzzleUtils.js';
|
||||
import { solveLine, solvePuzzle } from '../utils/solver.js';
|
||||
|
||||
// --- Logic Helpers ---
|
||||
const solveLineLogic = (lineState, hints) => {
|
||||
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
|
||||
const solverLine = lineState.map(cell => {
|
||||
if (cell === 0) return -1; // Unknown
|
||||
if (cell === 1) return 1; // Filled
|
||||
if (cell === 2) return 0; // Empty/Cross
|
||||
return -1;
|
||||
});
|
||||
|
||||
// Call robust solver
|
||||
const resultLine = solveLine(solverLine, hints);
|
||||
|
||||
// Check for new info
|
||||
if (!resultLine) return { index: -1 }; // Contradiction or error
|
||||
|
||||
for (let i = 0; i < lineState.length; i++) {
|
||||
// We only care about cells that are currently 0 (Unknown) in Store
|
||||
if (lineState[i] === 0) {
|
||||
if (resultLine[i] === 1) {
|
||||
return { index: i, state: 1 }; // Suggest Fill
|
||||
}
|
||||
if (resultLine[i] === 0) {
|
||||
return { index: i, state: 2 }; // Suggest Cross
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { index: -1 };
|
||||
};
|
||||
|
||||
const isSolved = (grid, solution) => {
|
||||
const size = grid.length;
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
const playerCell = grid[r][c];
|
||||
const solutionCell = solution[r][c];
|
||||
const isFilled = playerCell === 1;
|
||||
const shouldBeFilled = solutionCell === 1;
|
||||
|
||||
// Check correctness
|
||||
if (isFilled !== shouldBeFilled) return false;
|
||||
|
||||
// Check completeness (must be fully resolved to 1 or 2)
|
||||
if (playerCell === 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleStep = (playerGrid, solution) => {
|
||||
if (isSolved(playerGrid, solution)) {
|
||||
return { type: 'done', status: { key: 'worker.solved' } };
|
||||
}
|
||||
|
||||
const size = solution.length;
|
||||
const { rowHints, colHints } = calculateHints(solution);
|
||||
|
||||
for (let r = 0; r < size; r++) {
|
||||
const rowLine = playerGrid[r];
|
||||
const hints = rowHints[r];
|
||||
const result = solveLineLogic(rowLine, hints);
|
||||
if (result.index !== -1) {
|
||||
const stateKey = result.state === 1 ? 'worker.state.filled' : 'worker.state.empty';
|
||||
return {
|
||||
type: 'move',
|
||||
r,
|
||||
c: result.index,
|
||||
state: result.state,
|
||||
status: {
|
||||
key: 'worker.logicRow',
|
||||
params: { row: r + 1, col: result.index + 1, stateKey }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (let c = 0; c < size; c++) {
|
||||
const colLine = [];
|
||||
for (let r = 0; r < size; r++) colLine.push(playerGrid[r][c]);
|
||||
const hints = colHints[c];
|
||||
const result = solveLineLogic(colLine, hints);
|
||||
if (result.index !== -1) {
|
||||
const stateKey = result.state === 1 ? 'worker.state.filled' : 'worker.state.empty';
|
||||
return {
|
||||
type: 'move',
|
||||
r: result.index,
|
||||
c,
|
||||
state: result.state,
|
||||
status: {
|
||||
key: 'worker.logicCol',
|
||||
params: { row: result.index + 1, col: c + 1, stateKey }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { type: 'stuck', status: { key: 'worker.stuck' } };
|
||||
};
|
||||
|
||||
const handleBoost = (playerGrid, solution) => {
|
||||
const size = solution.length;
|
||||
|
||||
// 1. Try to use the Solver (DFS) to find a logical move
|
||||
try {
|
||||
const { rowHints, colHints } = calculateHints(solution);
|
||||
|
||||
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
|
||||
const solverGrid = playerGrid.map(row => row.map(cell => {
|
||||
if (cell === 0) return -1;
|
||||
if (cell === 1) return 1;
|
||||
if (cell === 2) return 0;
|
||||
return -1;
|
||||
}));
|
||||
|
||||
// Run full solver (logicOnly=false allows DFS/guessing)
|
||||
const result = solvePuzzle(rowHints, colHints, null, solverGrid, false);
|
||||
|
||||
if (result && result.solution) {
|
||||
const solvedGrid = result.solution;
|
||||
|
||||
// Find the first cell that is Unknown in playerGrid but Known in solvedGrid
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
if (playerGrid[r][c] === 0) { // Unknown in Player
|
||||
const solvedVal = solvedGrid[r][c]; // -1=Unk, 0=Empty, 1=Filled
|
||||
|
||||
if (solvedVal !== -1) {
|
||||
// Found a logical deduction!
|
||||
const newState = solvedVal === 1 ? 1 : 2; // 1->Filled, 0->Cross
|
||||
const stateKey = newState === 1 ? 'worker.state.filled' : 'worker.state.empty';
|
||||
return {
|
||||
type: 'move',
|
||||
r,
|
||||
c,
|
||||
state: newState,
|
||||
status: {
|
||||
key: 'worker.boosted',
|
||||
params: { row: r + 1, col: c + 1, stateKey }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Boost Solver failed, falling back to simple reveal:', e);
|
||||
}
|
||||
|
||||
// 2. Fallback: Cheat
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
if (playerGrid[r][c] === 0) {
|
||||
const correctState = solution[r][c] === 1 ? 1 : 2;
|
||||
const stateKey = correctState === 1 ? 'worker.state.filled' : 'worker.state.empty';
|
||||
return {
|
||||
type: 'move',
|
||||
r,
|
||||
c,
|
||||
state: correctState,
|
||||
status: {
|
||||
key: 'worker.boosted',
|
||||
params: { row: r + 1, col: c + 1, stateKey }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: 'done', status: { key: 'worker.solved' } };
|
||||
};
|
||||
|
||||
// --- Main Worker Handler ---
|
||||
self.onmessage = (e) => {
|
||||
const { id, grid, initialGrid } = e.data;
|
||||
const { id, grid, initialGrid, playerGrid, solution, action } = e.data;
|
||||
|
||||
try {
|
||||
if (!grid || grid.length === 0) {
|
||||
self.postMessage({ id, error: 'Empty grid' });
|
||||
// Mode 1: Analysis (Batch) - from ImageImportModal / workerPool
|
||||
if (grid) {
|
||||
if (grid.length === 0) {
|
||||
self.postMessage({ id, error: 'Empty grid' });
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = grid.length;
|
||||
const cols = grid[0].length;
|
||||
const { rowHints, colHints } = calculateHints(grid);
|
||||
|
||||
const onProgress = (percent) => {
|
||||
self.postMessage({
|
||||
id,
|
||||
type: 'progress',
|
||||
percent
|
||||
});
|
||||
};
|
||||
|
||||
const { percentSolved, difficultyScore } = solvePuzzle(rowHints, colHints, onProgress, initialGrid);
|
||||
|
||||
let value = difficultyScore;
|
||||
let level;
|
||||
|
||||
if (percentSolved < 100) {
|
||||
level = 'extreme';
|
||||
} else {
|
||||
if (value < 25) level = 'easy';
|
||||
else if (value < 50) level = 'medium';
|
||||
else if (value < 75) level = 'hard';
|
||||
else level = 'extreme';
|
||||
}
|
||||
|
||||
self.postMessage({
|
||||
id,
|
||||
solvability: Math.floor(percentSolved),
|
||||
difficulty: Math.round(value),
|
||||
difficultyLabel: level,
|
||||
rows,
|
||||
cols
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = grid.length;
|
||||
const cols = grid[0].length;
|
||||
// Use initialGrid if provided, otherwise assume we are starting fresh
|
||||
// BUT wait, 'grid' passed here is usually the 0/1 grid from Image Import (target pattern).
|
||||
// 'initialGrid' would be the partial solution state (-1/0/1).
|
||||
// Mode 2: Assistant (Step/Boost) - from useSolver
|
||||
if (playerGrid) {
|
||||
let result;
|
||||
if (action === 'boost') {
|
||||
result = handleBoost(playerGrid, solution);
|
||||
} else {
|
||||
result = handleStep(playerGrid, solution);
|
||||
}
|
||||
|
||||
// 1. Calculate Hints from the TARGET grid (the image)
|
||||
const { rowHints, colHints } = calculateHints(grid);
|
||||
|
||||
// 2. Run Solver (Logic + Lookahead)
|
||||
const onProgress = (percent) => {
|
||||
self.postMessage({
|
||||
id,
|
||||
type: 'progress',
|
||||
percent
|
||||
});
|
||||
};
|
||||
|
||||
const { percentSolved, difficultyScore, lookaheadUsed } = solvePuzzle(rowHints, colHints, onProgress, initialGrid);
|
||||
|
||||
// 3. Determine Level
|
||||
let value = difficultyScore;
|
||||
let level;
|
||||
|
||||
if (percentSolved < 100) {
|
||||
level = 'extreme'; // Unsolvable by logic+lookahead
|
||||
} else {
|
||||
if (value < 25) level = 'easy';
|
||||
else if (value < 50) level = 'medium';
|
||||
else if (value < 75) level = 'hard';
|
||||
else level = 'extreme';
|
||||
self.postMessage({ id, ...result });
|
||||
return;
|
||||
}
|
||||
|
||||
// Add specific note if lookahead was needed?
|
||||
// UI doesn't have a field for that, but we can encode it in difficultyLabel if needed.
|
||||
// For now, standard levels are fine.
|
||||
|
||||
self.postMessage({
|
||||
id,
|
||||
solvability: Math.floor(percentSolved),
|
||||
difficulty: Math.round(value),
|
||||
difficultyLabel: level,
|
||||
rows,
|
||||
cols
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
self.postMessage({ id, error: err.message });
|
||||
}
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
import { calculateHints } from '../utils/puzzleUtils.js';
|
||||
import { solveLine, solvePuzzle } from '../utils/solver.js';
|
||||
|
||||
const messages = {
|
||||
pl: {
|
||||
'worker.solved': 'Rozwiązane!',
|
||||
'worker.logicRow': 'Logika: Wiersz {row}, Kolumna {col} -> {state}',
|
||||
'worker.logicCol': 'Logika: Kolumna {col}, Wiersz {row} -> {state}',
|
||||
'worker.stuck': 'Brak logicznego ruchu. Spróbuj zgadnąć lub cofnąć.',
|
||||
'worker.boosted': 'Boost (DFS): Wiersz {row}, Kolumna {col} -> {state}',
|
||||
'worker.done': 'Koniec!',
|
||||
'worker.state.filled': 'Pełne',
|
||||
'worker.state.empty': 'Puste'
|
||||
},
|
||||
en: {
|
||||
'worker.solved': 'Solved!',
|
||||
'worker.logicRow': 'Logic: Row {row}, Column {col} -> {state}',
|
||||
'worker.logicCol': 'Logic: Column {col}, Row {row} -> {state}',
|
||||
'worker.stuck': 'No logical move found. Try guessing or undoing.',
|
||||
'worker.boosted': 'Boost (DFS): Row {row}, Column {col} -> {state}',
|
||||
'worker.done': 'Done!',
|
||||
'worker.state.filled': 'Filled',
|
||||
'worker.state.empty': 'Empty'
|
||||
}
|
||||
};
|
||||
|
||||
const resolveLocale = (value) => {
|
||||
if (!value) return 'en';
|
||||
const short = String(value).toLowerCase().split('-')[0];
|
||||
return short === 'pl' ? 'pl' : 'en';
|
||||
};
|
||||
|
||||
const format = (text, params = {}) => {
|
||||
return text.replace(/\{(\w+)\}/g, (_, key) => {
|
||||
const value = params[key];
|
||||
return value === undefined ? `{${key}}` : String(value);
|
||||
});
|
||||
};
|
||||
|
||||
const t = (locale, key, params) => {
|
||||
const lang = messages[locale] || messages.en;
|
||||
const value = lang[key] || messages.en[key] || key;
|
||||
return typeof value === 'string' ? format(value, params) : key;
|
||||
};
|
||||
|
||||
const solveLineLogic = (lineState, hints) => {
|
||||
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
|
||||
const solverLine = lineState.map(cell => {
|
||||
if (cell === 0) return -1; // Unknown
|
||||
if (cell === 1) return 1; // Filled
|
||||
if (cell === 2) return 0; // Empty/Cross
|
||||
return -1;
|
||||
});
|
||||
|
||||
// Call robust solver
|
||||
const resultLine = solveLine(solverLine, hints);
|
||||
|
||||
// Check for new info
|
||||
if (!resultLine) return { index: -1 }; // Contradiction or error
|
||||
|
||||
for (let i = 0; i < lineState.length; i++) {
|
||||
// We only care about cells that are currently 0 (Unknown) in Store
|
||||
if (lineState[i] === 0) {
|
||||
if (resultLine[i] === 1) {
|
||||
return { index: i, state: 1 }; // Suggest Fill
|
||||
}
|
||||
if (resultLine[i] === 0) {
|
||||
return { index: i, state: 2 }; // Suggest Cross
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { index: -1 };
|
||||
};
|
||||
|
||||
const isSolved = (grid, solution) => {
|
||||
const size = grid.length;
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
const playerCell = grid[r][c];
|
||||
const solutionCell = solution[r][c];
|
||||
const isFilled = playerCell === 1;
|
||||
const shouldBeFilled = solutionCell === 1;
|
||||
|
||||
// Check correctness
|
||||
if (isFilled !== shouldBeFilled) return false;
|
||||
|
||||
// Check completeness (must be fully resolved to 1 or 2)
|
||||
if (playerCell === 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleStep = (playerGrid, solution, locale) => {
|
||||
if (isSolved(playerGrid, solution)) {
|
||||
return { type: 'done', statusText: t(locale, 'worker.solved') };
|
||||
}
|
||||
|
||||
const size = solution.length;
|
||||
const { rowHints, colHints } = calculateHints(solution);
|
||||
|
||||
for (let r = 0; r < size; r++) {
|
||||
const rowLine = playerGrid[r];
|
||||
const hints = rowHints[r];
|
||||
const result = solveLineLogic(rowLine, hints);
|
||||
if (result.index !== -1) {
|
||||
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
|
||||
return {
|
||||
type: 'move',
|
||||
r,
|
||||
c: result.index,
|
||||
state: result.state,
|
||||
statusText: t(locale, 'worker.logicRow', { row: r + 1, col: result.index + 1, state: stateLabel })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (let c = 0; c < size; c++) {
|
||||
const colLine = [];
|
||||
for (let r = 0; r < size; r++) colLine.push(playerGrid[r][c]);
|
||||
const hints = colHints[c];
|
||||
const result = solveLineLogic(colLine, hints);
|
||||
if (result.index !== -1) {
|
||||
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
|
||||
return {
|
||||
type: 'move',
|
||||
r: result.index,
|
||||
c,
|
||||
state: result.state,
|
||||
statusText: t(locale, 'worker.logicCol', { row: result.index + 1, col: c + 1, state: stateLabel })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for guess logic - we want to avoid this unless strictly necessary
|
||||
// If no logic move found, return 'stuck' instead of cheating
|
||||
return { type: 'stuck', statusText: t(locale, 'worker.stuck') };
|
||||
};
|
||||
|
||||
const handleBoost = (playerGrid, solution, locale) => {
|
||||
const size = solution.length;
|
||||
|
||||
// 1. Try to use the Solver (DFS) to find a logical move
|
||||
try {
|
||||
const { rowHints, colHints } = calculateHints(solution);
|
||||
|
||||
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
|
||||
const solverGrid = playerGrid.map(row => row.map(cell => {
|
||||
if (cell === 0) return -1;
|
||||
if (cell === 1) return 1;
|
||||
if (cell === 2) return 0;
|
||||
return -1;
|
||||
}));
|
||||
|
||||
// Run full solver (logicOnly=false allows DFS/guessing)
|
||||
// We pass solverGrid as initial state to respect user's moves
|
||||
const result = solvePuzzle(rowHints, colHints, null, solverGrid, false);
|
||||
|
||||
if (result && result.solution) {
|
||||
const solvedGrid = result.solution;
|
||||
|
||||
// Find the first cell that is Unknown in playerGrid but Known in solvedGrid
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
if (playerGrid[r][c] === 0) { // Unknown in Player
|
||||
const solvedVal = solvedGrid[r][c]; // -1=Unk, 0=Empty, 1=Filled
|
||||
|
||||
if (solvedVal !== -1) {
|
||||
// Found a logical deduction!
|
||||
const newState = solvedVal === 1 ? 1 : 2; // 1->Filled, 0->Cross
|
||||
const stateLabel = t(locale, newState === 1 ? 'worker.state.filled' : 'worker.state.empty');
|
||||
return {
|
||||
type: 'move',
|
||||
r,
|
||||
c,
|
||||
state: newState,
|
||||
statusText: t(locale, 'worker.boosted', { row: r + 1, col: c + 1, state: stateLabel })
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Boost Solver failed, falling back to simple reveal:', e);
|
||||
}
|
||||
|
||||
// 2. Fallback: If solver failed (e.g. contradiction due to user error),
|
||||
// or no new info found, use the "Cheat" method (reveal from true solution).
|
||||
for (let r = 0; r < size; r++) {
|
||||
for (let c = 0; c < size; c++) {
|
||||
if (playerGrid[r][c] === 0) {
|
||||
const correctState = solution[r][c] === 1 ? 1 : 2;
|
||||
const stateLabel = t(locale, correctState === 1 ? 'worker.state.filled' : 'worker.state.empty');
|
||||
return {
|
||||
type: 'move',
|
||||
r,
|
||||
c,
|
||||
state: correctState,
|
||||
statusText: t(locale, 'worker.boosted', { row: r + 1, col: c + 1, state: stateLabel })
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return { type: 'done', statusText: t(locale, 'worker.solved') };
|
||||
};
|
||||
|
||||
self.onmessage = (event) => {
|
||||
const { id, playerGrid, solution, locale, action } = event.data;
|
||||
const resolved = resolveLocale(locale);
|
||||
|
||||
if (action === 'boost') {
|
||||
const result = handleBoost(playerGrid, solution, resolved);
|
||||
self.postMessage({ id, ...result });
|
||||
} else {
|
||||
const result = handleStep(playerGrid, solution, resolved);
|
||||
self.postMessage({ id, ...result });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user