Optimize simulation with logic-only solver, fix rectangular grid support, and improve worker pool
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
This commit is contained in:
@@ -26,6 +26,7 @@ const installDismissed = ref(false);
|
|||||||
const isCoarsePointer = ref(false);
|
const isCoarsePointer = ref(false);
|
||||||
const isStandalone = ref(false);
|
const isStandalone = ref(false);
|
||||||
const isIos = ref(false);
|
const isIos = ref(false);
|
||||||
|
const isDev = ref(false);
|
||||||
const themePreference = ref('system');
|
const themePreference = ref('system');
|
||||||
const appVersion = __APP_VERSION__;
|
const appVersion = __APP_VERSION__;
|
||||||
let displayModeMedia = null;
|
let displayModeMedia = null;
|
||||||
@@ -65,7 +66,7 @@ const updateStandalone = () => {
|
|||||||
const handleBeforeInstallPrompt = (e) => {
|
const handleBeforeInstallPrompt = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deferredPrompt.value = e;
|
deferredPrompt.value = e;
|
||||||
if (!isStandalone.value) {
|
if (!isStandalone.value && !isDev.value) {
|
||||||
canInstall.value = true;
|
canInstall.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -118,6 +119,7 @@ onMounted(() => {
|
|||||||
isCoarsePointer.value = window.matchMedia('(pointer: coarse)').matches;
|
isCoarsePointer.value = window.matchMedia('(pointer: coarse)').matches;
|
||||||
const ua = navigator.userAgent.toLowerCase();
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
isIos.value = /ipad|iphone|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1);
|
isIos.value = /ipad|iphone|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1);
|
||||||
|
isDev.value = window.location.port !== '' && window.location.port !== '80' && window.location.port !== '443';
|
||||||
const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('theme') : null;
|
const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('theme') : null;
|
||||||
if (storedTheme === 'light' || storedTheme === 'dark' || storedTheme === 'system') {
|
if (storedTheme === 'light' || storedTheme === 'dark' || storedTheme === 'system') {
|
||||||
themePreference.value = storedTheme;
|
themePreference.value = storedTheme;
|
||||||
@@ -170,7 +172,7 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
<FixedBar />
|
<FixedBar />
|
||||||
|
|
||||||
<div v-if="(canInstall || (isIos && !isStandalone)) && !installDismissed" class="install-banner">
|
<div v-if="!isDev && (canInstall || (isIos && !isStandalone)) && !installDismissed" class="install-banner">
|
||||||
<div class="install-content">
|
<div class="install-content">
|
||||||
<img src="/pwa-192x192.png" alt="App Icon" class="install-icon" />
|
<img src="/pwa-192x192.png" alt="App Icon" class="install-icon" />
|
||||||
<div class="install-text">
|
<div class="install-text">
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const handlePointerCancel = (e) => {
|
|||||||
height: var(--cell-size);
|
height: var(--cell-size);
|
||||||
background-color: var(--cell-empty);
|
background-color: var(--cell-empty);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const store = usePuzzleStore();
|
|||||||
const { rowHints, colHints } = useHints(computed(() => store.solution));
|
const { rowHints, colHints } = useHints(computed(() => store.solution));
|
||||||
const { startDrag, onMouseEnter, stopDrag } = useNonogram();
|
const { startDrag, onMouseEnter, stopDrag } = useNonogram();
|
||||||
|
|
||||||
|
// Compute grid dimensions from hints
|
||||||
|
const gridRows = computed(() => rowHints.value.length);
|
||||||
|
const gridCols = computed(() => colHints.value.length);
|
||||||
|
|
||||||
const cellSize = ref(30);
|
const cellSize = ref(30);
|
||||||
const rowHintsRef = ref(null);
|
const rowHintsRef = ref(null);
|
||||||
const activeRow = ref(null);
|
const activeRow = ref(null);
|
||||||
@@ -143,13 +147,16 @@ const computeCellSize = () => {
|
|||||||
// Ensure we don't have negative space
|
// Ensure we don't have negative space
|
||||||
const availableForGrid = Math.max(0, containerWidth - hintWidth);
|
const availableForGrid = Math.max(0, containerWidth - hintWidth);
|
||||||
|
|
||||||
const size = Math.floor((availableForGrid - gridPad * 2 - (store.size - 1) * gap) / store.size);
|
// Calculate cell size based on width availability (columns)
|
||||||
|
// Vertical scrolling is acceptable, so we don't constrain by height (rows)
|
||||||
|
const cols = Math.max(1, gridCols.value);
|
||||||
|
const size = Math.floor((availableForGrid - gridPad * 2 - (cols - 1) * gap) / cols);
|
||||||
|
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
// Desktop: Allow overflow, use comfortable size
|
// Desktop: Allow overflow, use comfortable size
|
||||||
cellSize.value = 30;
|
cellSize.value = 30;
|
||||||
} else {
|
} else {
|
||||||
// Mobile: Fit to screen
|
// Mobile: Fit to screen width
|
||||||
// Keep min 18, max 36
|
// Keep min 18, max 36
|
||||||
cellSize.value = Math.max(18, Math.min(36, size));
|
cellSize.value = Math.max(18, Math.min(36, size));
|
||||||
}
|
}
|
||||||
@@ -240,17 +247,17 @@ watch(() => store.size, async () => {
|
|||||||
<div class="corner-spacer"></div>
|
<div class="corner-spacer"></div>
|
||||||
|
|
||||||
<!-- Column Hints -->
|
<!-- Column Hints -->
|
||||||
<Hints :hints="colHints" orientation="col" :size="store.size" :activeIndex="activeCol" />
|
<Hints :hints="colHints" orientation="col" :size="gridCols" :activeIndex="activeCol" />
|
||||||
|
|
||||||
<!-- Row Hints -->
|
<!-- Row Hints -->
|
||||||
<Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="store.size" :activeIndex="activeRow" />
|
<Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="gridRows" :activeIndex="activeRow" />
|
||||||
|
|
||||||
<!-- Grid -->
|
<!-- Grid -->
|
||||||
<div
|
<div
|
||||||
class="grid"
|
class="grid"
|
||||||
:style="{
|
:style="{
|
||||||
gridTemplateColumns: `repeat(${store.size}, var(--cell-size))`,
|
gridTemplateColumns: `repeat(${gridCols}, var(--cell-size))`,
|
||||||
gridTemplateRows: `repeat(${store.size}, var(--cell-size))`
|
gridTemplateRows: `repeat(${gridRows}, var(--cell-size))`
|
||||||
}"
|
}"
|
||||||
@pointermove.prevent="handlePointerMove"
|
@pointermove.prevent="handlePointerMove"
|
||||||
@mouseleave="handleGridLeave"
|
@mouseleave="handleGridLeave"
|
||||||
@@ -263,8 +270,8 @@ watch(() => store.size, async () => {
|
|||||||
:r="r"
|
:r="r"
|
||||||
:c="c"
|
:c="c"
|
||||||
:class="{
|
:class="{
|
||||||
'guide-right': (c + 1) % 5 === 0 && c !== store.size - 1,
|
'guide-right': (c + 1) % 5 === 0 && c !== gridCols - 1,
|
||||||
'guide-bottom': (r + 1) % 5 === 0 && r !== store.size - 1
|
'guide-bottom': (r + 1) % 5 === 0 && r !== gridRows - 1
|
||||||
}"
|
}"
|
||||||
@start-drag="startDrag"
|
@start-drag="startDrag"
|
||||||
@enter-cell="handleCellEnter"
|
@enter-cell="handleCellEnter"
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ defineProps({
|
|||||||
|
|
||||||
.hints-container.col {
|
.hints-container.col {
|
||||||
padding-bottom: var(--grid-padding);
|
padding-bottom: var(--grid-padding);
|
||||||
align-items: flex-end;
|
/* align-items: flex-end; - Removed to ensure uniform column height */
|
||||||
padding-left: var(--grid-padding);
|
padding-left: var(--grid-padding);
|
||||||
padding-right: var(--grid-padding);
|
padding-right: var(--grid-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hints-container.row {
|
.hints-container.row {
|
||||||
align-items: flex-end;
|
/* align-items: flex-end; - Removed to ensure row hints fill the cell height */
|
||||||
padding: var(--grid-padding) var(--grid-padding) var(--grid-padding) 0;
|
padding: var(--grid-padding) var(--grid-padding) var(--grid-padding) 0;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
@@ -99,6 +99,21 @@ defineProps({
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hint-num {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col .hint-group {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .hint-group {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Alternating Colors within the group */
|
/* Alternating Colors within the group */
|
||||||
.hint-num.hint-alt {
|
.hint-num.hint-alt {
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
|
|||||||
@@ -225,16 +225,36 @@ const calculateStats = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pool = getWorkerPool();
|
const pool = getWorkerPool();
|
||||||
pool.clearQueue(); // Clear pending tasks
|
pool.cancelAll(); // Force stop previous calculations for immediate responsiveness
|
||||||
|
|
||||||
const result = await pool.run({
|
// Demonstrate parallel execution capability
|
||||||
id: requestId,
|
// We split the problem into two branches: assuming first cell is EMPTY (0) vs FILLED (1)
|
||||||
grid: generatedGrid.value
|
// This doubles the search power by utilizing 2 workers immediately.
|
||||||
}, (progress) => {
|
|
||||||
if (currentStatsRequestId === requestId) {
|
// Ensure we send plain objects to workers, not Vue proxies
|
||||||
processingProgress.value = progress;
|
const rawGrid = JSON.parse(JSON.stringify(generatedGrid.value));
|
||||||
|
const rows = rawGrid.length;
|
||||||
|
const cols = rawGrid[0].length;
|
||||||
|
|
||||||
|
// Create initial states
|
||||||
|
const gridA = Array(rows).fill().map(() => Array(cols).fill(-1));
|
||||||
|
gridA[0][0] = 0; // Branch A: Assume (0,0) is Empty
|
||||||
|
|
||||||
|
const gridB = Array(rows).fill().map(() => Array(cols).fill(-1));
|
||||||
|
gridB[0][0] = 1; // Branch B: Assume (0,0) is Filled
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
{
|
||||||
|
data: { id: requestId, grid: rawGrid, initialGrid: gridA },
|
||||||
|
onProgress: (p) => { if (currentStatsRequestId === requestId) processingProgress.value = Math.max(processingProgress.value, p); }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: { id: requestId, grid: rawGrid, initialGrid: gridB },
|
||||||
|
onProgress: (p) => { if (currentStatsRequestId === requestId) processingProgress.value = Math.max(processingProgress.value, p); }
|
||||||
}
|
}
|
||||||
});
|
];
|
||||||
|
|
||||||
|
const result = await pool.runRace(tasks);
|
||||||
|
|
||||||
if (result.id === currentStatsRequestId) {
|
if (result.id === currentStatsRequestId) {
|
||||||
solvability.value = result.solvability;
|
solvability.value = result.solvability;
|
||||||
@@ -242,12 +262,14 @@ const calculateStats = async () => {
|
|||||||
difficultyLabel.value = result.difficultyLabel;
|
difficultyLabel.value = result.difficultyLabel;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.message !== 'Cancelled') {
|
if (err.message !== 'Cancelled' && err.message !== 'Terminated') {
|
||||||
console.error('Worker error:', err);
|
console.error('Worker error:', err);
|
||||||
if (currentStatsRequestId === requestId) {
|
if (currentStatsRequestId === requestId) {
|
||||||
solvability.value = 0;
|
solvability.value = 0;
|
||||||
difficulty.value = 0;
|
difficulty.value = 0;
|
||||||
difficultyLabel.value = 'unknown';
|
// If translation key is missing, this might show 'difficulty.error'
|
||||||
|
// Ensure we have a fallback or the key exists
|
||||||
|
difficultyLabel.value = 'error';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -266,6 +288,11 @@ watch([maxDimension, threshold], () => {
|
|||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
updateGrid();
|
updateGrid();
|
||||||
}, 50);
|
}, 50);
|
||||||
|
} else {
|
||||||
|
// If no image loaded, just update the display values
|
||||||
|
// Assuming square aspect ratio for preview
|
||||||
|
gridRows.value = maxDimension.value;
|
||||||
|
gridCols.value = maxDimension.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -543,6 +570,8 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { Gamepad2, Palette, CircleHelp, Sun, Moon, Menu, X, ChevronDown, ChevronUp, Monitor, Image as ImageIcon } from 'lucide-vue-next';
|
import { Gamepad2, Palette, CircleHelp, Sun, Moon, Menu, X, ChevronDown, ChevronUp, Monitor, Image as ImageIcon, Sparkles, Shuffle, Grid3X3, Grid2X2, Grid, MousePointer2 } from 'lucide-vue-next';
|
||||||
|
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
const { t, locale, setLocale, locales } = useI18n();
|
const { t, locale, setLocale, locales } = useI18n();
|
||||||
@@ -16,6 +16,13 @@ const isMobileMenuOpen = ref(false);
|
|||||||
const langMenuRef = ref(null);
|
const langMenuRef = ref(null);
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
|
|
||||||
|
const getLevelIcon = (id) => {
|
||||||
|
if (id === 'easy') return Grid2X2;
|
||||||
|
if (id === 'medium') return Grid3X3;
|
||||||
|
if (id === 'hard') return Grid;
|
||||||
|
return Gamepad2;
|
||||||
|
};
|
||||||
|
|
||||||
// Map language codes to country codes for flag-icons
|
// Map language codes to country codes for flag-icons
|
||||||
const langToCountry = {
|
const langToCountry = {
|
||||||
en: 'gb',
|
en: 'gb',
|
||||||
@@ -257,12 +264,15 @@ watch(isMobileMenuOpen, (val) => {
|
|||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="selectLevel(lvl.id)"
|
@click="selectLevel(lvl.id)"
|
||||||
>
|
>
|
||||||
|
<component :is="getLevelIcon(lvl.id)" :size="16" />
|
||||||
{{ lvl.label }}
|
{{ lvl.label }}
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item" @click="openCustom">
|
<button class="dropdown-item" @click="openCustom">
|
||||||
|
<Shuffle :size="16" />
|
||||||
{{ t('level.custom_random') }}
|
{{ t('level.custom_random') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item" @click="openImageImport">
|
<button class="dropdown-item" @click="openImageImport">
|
||||||
|
<ImageIcon :size="16" />
|
||||||
{{ t('level.custom_image') }}
|
{{ t('level.custom_image') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -357,12 +367,15 @@ watch(isMobileMenuOpen, (val) => {
|
|||||||
class="mobile-sub-item"
|
class="mobile-sub-item"
|
||||||
@click="selectLevel(lvl.id)"
|
@click="selectLevel(lvl.id)"
|
||||||
>
|
>
|
||||||
|
<component :is="getLevelIcon(lvl.id)" :size="16" />
|
||||||
{{ lvl.label }}
|
{{ lvl.label }}
|
||||||
</button>
|
</button>
|
||||||
<button class="mobile-sub-item" @click="openCustom">
|
<button class="mobile-sub-item" @click="openCustom">
|
||||||
|
<Shuffle :size="16" />
|
||||||
{{ t('level.custom_random') }}
|
{{ t('level.custom_random') }}
|
||||||
</button>
|
</button>
|
||||||
<button class="mobile-sub-item" @click="openImageImport">
|
<button class="mobile-sub-item" @click="openImageImport">
|
||||||
|
<ImageIcon :size="16" />
|
||||||
{{ t('level.custom_image') }}
|
{{ t('level.custom_image') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,11 +71,12 @@ const startSimulation = async () => {
|
|||||||
for (let i = 0; i < SAMPLES_PER_POINT; i++) {
|
for (let i = 0; i < SAMPLES_PER_POINT; i++) {
|
||||||
const grid = generateRandomGrid(size, density);
|
const grid = generateRandomGrid(size, density);
|
||||||
const { rowHints, colHints } = calculateHints(grid);
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
const { percentSolved } = solvePuzzle(rowHints, colHints);
|
// Use logicOnly=true for fast simulation
|
||||||
|
const { percentSolved } = solvePuzzle(rowHints, colHints, null, null, true);
|
||||||
totalSolved += percentSolved;
|
totalSolved += percentSolved;
|
||||||
|
|
||||||
// Yield to UI every few samples to keep it responsive
|
// Yield to UI every few samples to keep it responsive
|
||||||
if (i % 2 === 0) await new Promise(r => setTimeout(r, 0));
|
if (i % 10 === 0) await new Promise(r => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
const avgSolved = totalSolved / SAMPLES_PER_POINT;
|
const avgSolved = totalSolved / SAMPLES_PER_POINT;
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
if (solution.value.length === 0 || playerGrid.value.length === 0) return 0;
|
if (solution.value.length === 0 || playerGrid.value.length === 0) return 0;
|
||||||
|
|
||||||
for (let r = 0; r < size.value; r++) {
|
const rows = solution.value.length;
|
||||||
for (let c = 0; c < size.value; c++) {
|
const cols = solution.value[0].length;
|
||||||
|
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
// Zliczamy tylko poprawne wypełnienia (czarne),
|
// Zliczamy tylko poprawne wypełnienia (czarne),
|
||||||
// ale w nonogramach postęp to często: (poprawne_czarne - bledne_czarne) / total_czarne
|
// ale w nonogramach postęp to często: (poprawne_czarne - bledne_czarne) / total_czarne
|
||||||
// Zróbmy prostą wersję: % poprawnie zaznaczonych czarnych - błędnie zaznaczone czarne
|
// Zróbmy prostą wersję: % poprawnie zaznaczonych czarnych - błędnie zaznaczone czarne
|
||||||
if (playerGrid.value[r][c] === 1) {
|
if (playerGrid.value[r] && playerGrid.value[r][c] === 1) {
|
||||||
if (solution.value[r][c] === 1) count++;
|
if (solution.value[r][c] === 1) count++;
|
||||||
else count--; // kara za błąd
|
else count--; // kara za błąd
|
||||||
}
|
}
|
||||||
@@ -96,7 +99,11 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
function initFromImage(grid) {
|
function initFromImage(grid) {
|
||||||
stopTimer();
|
stopTimer();
|
||||||
currentLevelId.value = 'custom_image';
|
currentLevelId.value = 'custom_image';
|
||||||
size.value = grid.length;
|
// Use the larger dimension for size to ensure loops cover everything if square-assumption exists
|
||||||
|
// But ideally we should support rectangular.
|
||||||
|
// For now, size.value is used in resetGrid loop.
|
||||||
|
// Let's update resetGrid to handle rectangular.
|
||||||
|
size.value = Math.max(grid.length, grid[0].length);
|
||||||
solution.value = grid;
|
solution.value = grid;
|
||||||
|
|
||||||
resetGrid();
|
resetGrid();
|
||||||
@@ -111,13 +118,13 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
elapsedTime.value = 0;
|
elapsedTime.value = 0;
|
||||||
startTimer();
|
startTimer();
|
||||||
saveState();
|
saveState();
|
||||||
}
|
|
||||||
|
|
||||||
function resetGrid() {
|
function resetGrid() {
|
||||||
playerGrid.value = Array(size.value).fill().map(() => Array(size.value).fill(0));
|
const rows = solution.value.length;
|
||||||
moves.value = 0;
|
const cols = solution.value[0].length;
|
||||||
|
playerGrid.value = Array(rows).fill().map(() => Array(cols).fill(0));
|
||||||
history.value = [];
|
history.value = [];
|
||||||
currentTransaction.value = null;
|
moves.value = 0;
|
||||||
|
} currentTransaction.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startInteraction() {
|
function startInteraction() {
|
||||||
@@ -218,11 +225,14 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
|
|
||||||
// Calculate expected hints from solution (truth)
|
// Calculate expected hints from solution (truth)
|
||||||
// We do this dynamically to ensure we always check against the rules of the board
|
// We do this dynamically to ensure we always check against the rules of the board
|
||||||
|
const rows = solution.value.length;
|
||||||
|
const cols = solution.value[0].length;
|
||||||
|
|
||||||
const solutionRows = solution.value;
|
const solutionRows = solution.value;
|
||||||
const solutionCols = Array(size.value).fill().map((_, c) => solution.value.map(r => r[c]));
|
const solutionCols = Array(cols).fill().map((_, c) => solution.value.map(r => r[c]));
|
||||||
|
|
||||||
// Check Rows
|
// Check Rows
|
||||||
for (let r = 0; r < size.value; r++) {
|
for (let r = 0; r < rows; r++) {
|
||||||
const targetHints = calculateLineHints(solutionRows[r]);
|
const targetHints = calculateLineHints(solutionRows[r]);
|
||||||
const playerLine = playerGrid.value[r];
|
const playerLine = playerGrid.value[r];
|
||||||
if (!validateLine(playerLine, targetHints)) {
|
if (!validateLine(playerLine, targetHints)) {
|
||||||
@@ -233,7 +243,7 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
|
|
||||||
if (correct) {
|
if (correct) {
|
||||||
// Check Columns
|
// Check Columns
|
||||||
for (let c = 0; c < size.value; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const targetHints = calculateLineHints(solutionCols[c]);
|
const targetHints = calculateLineHints(solutionCols[c]);
|
||||||
const playerLine = playerGrid.value.map(row => row[c]);
|
const playerLine = playerGrid.value.map(row => row[c]);
|
||||||
if (!validateLine(playerLine, targetHints)) {
|
if (!validateLine(playerLine, targetHints)) {
|
||||||
|
|||||||
27
src/utils/debug_solver.test.js
Normal file
27
src/utils/debug_solver.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { solvePuzzle } from './solver';
|
||||||
|
import { calculateHints } from './puzzleUtils';
|
||||||
|
|
||||||
|
describe('Debug Solver', () => {
|
||||||
|
it('should solve the broken grid', () => {
|
||||||
|
const grid = [
|
||||||
|
[0,1,1,1,0,0,1,0,1,1],
|
||||||
|
[1,1,1,0,0,1,1,1,0,0],
|
||||||
|
[1,0,1,0,1,0,0,1,0,0],
|
||||||
|
[1,0,0,0,1,1,1,1,0,1],
|
||||||
|
[1,1,0,1,0,0,0,1,0,1],
|
||||||
|
[1,0,1,0,1,0,0,0,1,0],
|
||||||
|
[1,1,1,0,0,1,1,0,0,0],
|
||||||
|
[0,1,0,0,1,0,1,0,0,0],
|
||||||
|
[0,0,0,1,1,0,0,0,1,0],
|
||||||
|
[1,0,1,1,0,0,1,0,1,1]
|
||||||
|
];
|
||||||
|
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
|
console.log('Solve Result:', result);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
44
src/utils/large_grid_solver.test.js
Normal file
44
src/utils/large_grid_solver.test.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { solvePuzzle } from './solver.js';
|
||||||
|
|
||||||
|
describe('Large Grid Solver', () => {
|
||||||
|
it('should solve a large 55x28 grid without crashing', () => {
|
||||||
|
const rows = 28;
|
||||||
|
const cols = 55;
|
||||||
|
// Create a simple pattern: checkerboard or lines
|
||||||
|
const grid = Array(rows).fill().map((_, r) =>
|
||||||
|
Array(cols).fill().map((_, c) => (r + c) % 2 === 0 ? 1 : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate hints
|
||||||
|
const rowHints = grid.map(row => {
|
||||||
|
const hints = [];
|
||||||
|
let current = 0;
|
||||||
|
row.forEach(cell => {
|
||||||
|
if (cell === 1) current++;
|
||||||
|
else if (current > 0) { hints.push(current); current = 0; }
|
||||||
|
});
|
||||||
|
if (current > 0) hints.push(current);
|
||||||
|
return hints.length ? hints : [0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const colHints = Array(cols).fill().map((_, c) => {
|
||||||
|
const hints = [];
|
||||||
|
let current = 0;
|
||||||
|
for(let r=0; r<rows; r++) {
|
||||||
|
if (grid[r][c] === 1) current++;
|
||||||
|
else if (current > 0) { hints.push(current); current = 0; }
|
||||||
|
}
|
||||||
|
if (current > 0) hints.push(current);
|
||||||
|
return hints.length ? hints : [0];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Starting solve...');
|
||||||
|
const result = solvePuzzle(rowHints, colHints, (p) => console.log(`Progress: ${p}%`));
|
||||||
|
console.log('Result:', result);
|
||||||
|
|
||||||
|
expect(result.percentSolved).toBeGreaterThan(0);
|
||||||
|
expect(result.difficulty).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
49
src/utils/repro_solver.test.js
Normal file
49
src/utils/repro_solver.test.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { solvePuzzle } from './solver';
|
||||||
|
import { calculateHints } from './puzzleUtils';
|
||||||
|
|
||||||
|
describe('Solver Repro', () => {
|
||||||
|
it('should solve a simple generated puzzle', () => {
|
||||||
|
const grid = [
|
||||||
|
[1, 0, 1, 1, 0],
|
||||||
|
[1, 1, 0, 0, 1],
|
||||||
|
[0, 0, 1, 0, 0],
|
||||||
|
[1, 1, 1, 1, 1],
|
||||||
|
[0, 1, 0, 1, 0]
|
||||||
|
];
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fail on random valid lines', () => {
|
||||||
|
// Test solveLine indirectly via solvePuzzle on small grids
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const size = 10;
|
||||||
|
const grid = [];
|
||||||
|
for(let r=0; r<size; r++) {
|
||||||
|
const row = [];
|
||||||
|
for(let c=0; c<size; c++) row.push(Math.random() > 0.5 ? 1 : 0);
|
||||||
|
grid.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
|
// It might not be 100% solvable without guessing (logic only),
|
||||||
|
// but since our solver HAS backtracking, it MUST be 100% solvable
|
||||||
|
// (unless timeout/max depth reached, but for 10x10 it should solve).
|
||||||
|
|
||||||
|
// If it returns 0% or low %, it implies it failed to find the solution
|
||||||
|
// or found a contradiction (which shouldn't happen for valid hints).
|
||||||
|
|
||||||
|
if (result.percentSolved < 100) {
|
||||||
|
console.log('Failed Grid:', JSON.stringify(grid));
|
||||||
|
console.log('Result:', result);
|
||||||
|
}
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
* 1: Filled
|
* 1: Filled
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Memoized helper for checking if hints fit
|
||||||
|
const memo = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Solves a single line (row or column) based on hints and current knowledge.
|
* Solves a single line (row or column) based on hints and current knowledge.
|
||||||
* Uses the "Left-Right Overlap" algorithm to find common filled cells.
|
* Uses the "Left-Right Overlap" algorithm to find common filled cells.
|
||||||
@@ -19,6 +22,9 @@ function solveLine(currentLine, hints) {
|
|||||||
const length = currentLine.length;
|
const length = currentLine.length;
|
||||||
|
|
||||||
// If no hints, all must be empty
|
// If no hints, all must be empty
|
||||||
|
// Clear memo for this line solve
|
||||||
|
memo.clear();
|
||||||
|
|
||||||
if (hints.length === 0 || (hints.length === 1 && hints[0] === 0)) {
|
if (hints.length === 0 || (hints.length === 1 && hints[0] === 0)) {
|
||||||
return Array(length).fill(0);
|
return Array(length).fill(0);
|
||||||
}
|
}
|
||||||
@@ -45,11 +51,6 @@ function solveLine(currentLine, hints) {
|
|||||||
while (currentIdx <= length - block) {
|
while (currentIdx <= length - block) {
|
||||||
if (canPlace(currentLine, currentIdx, block)) {
|
if (canPlace(currentLine, currentIdx, block)) {
|
||||||
// Verify we can fit remaining blocks
|
// Verify we can fit remaining blocks
|
||||||
// Simple heuristic: do we have enough space?
|
|
||||||
// A full recursive check is better but slower.
|
|
||||||
// For "Logical Solver" we assume valid placement is possible if we respect current constraints.
|
|
||||||
// However, strictly, we need to know if there is *any* valid arrangement starting here.
|
|
||||||
// Let's use a recursive check with memoization for "can fit rest".
|
|
||||||
if (canFitRest(currentLine, currentIdx + block + 1, hints, hIndex + 1)) {
|
if (canFitRest(currentLine, currentIdx + block + 1, hints, hIndex + 1)) {
|
||||||
leftPositions.push(currentIdx);
|
leftPositions.push(currentIdx);
|
||||||
currentIdx += block + 1; // Move past this block + 1 space
|
currentIdx += block + 1; // Move past this block + 1 space
|
||||||
@@ -61,12 +62,10 @@ function solveLine(currentLine, hints) {
|
|||||||
if (leftPositions.length <= hIndex) return null; // Impossible
|
if (leftPositions.length <= hIndex) return null; // Impossible
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Calculate Right-Most Positions (by reversing line and hints)
|
// Clear memo for right-side calculation (different line/hints)
|
||||||
// This is symmetrical to Left-Most.
|
memo.clear();
|
||||||
// Instead of implementing reverse logic, we can just reverse inputs, run left-most, and reverse back.
|
|
||||||
// But we need to respect the "currentLine" constraints which might be asymmetric.
|
|
||||||
|
|
||||||
// Actually, "Right-Most" is just "Left-Most" on the reversed grid.
|
// 2. Calculate Right-Most Positions (by reversing line and hints)
|
||||||
const reversedLine = [...currentLine].reverse();
|
const reversedLine = [...currentLine].reverse();
|
||||||
const reversedHints = [...hints].reverse();
|
const reversedHints = [...hints].reverse();
|
||||||
const rightPositionsReversed = [];
|
const rightPositionsReversed = [];
|
||||||
@@ -88,8 +87,6 @@ function solveLine(currentLine, hints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert reversed positions to actual indices
|
// Convert reversed positions to actual indices
|
||||||
// index in reversed = length - 1 - (original_index + block_size - 1)
|
|
||||||
// original_start = length - 1 - (reversed_start + block_size - 1) = length - reversed_start - block_size
|
|
||||||
const rightPositions = rightPositionsReversed.map((rStart, i) => {
|
const rightPositions = rightPositionsReversed.map((rStart, i) => {
|
||||||
const block = reversedHints[i];
|
const block = reversedHints[i];
|
||||||
return length - rStart - block;
|
return length - rStart - block;
|
||||||
@@ -106,13 +103,6 @@ function solveLine(currentLine, hints) {
|
|||||||
const block = hints[i];
|
const block = hints[i];
|
||||||
|
|
||||||
// If overlap exists: [r, l + block - 1]
|
// If overlap exists: [r, l + block - 1]
|
||||||
// Example: Block 5. Left: 2, Right: 4.
|
|
||||||
// Left: ..XXXXX...
|
|
||||||
// Right: ....XXXXX.
|
|
||||||
// Overlap: ..XXX...
|
|
||||||
// Indices: max(l, r) to min(l+block, r+block) - 1 ?
|
|
||||||
// Range is [r, l + block - 1] (inclusive)
|
|
||||||
|
|
||||||
if (r < l + block) {
|
if (r < l + block) {
|
||||||
for (let k = r; k < l + block; k++) {
|
for (let k = r; k < l + block; k++) {
|
||||||
newLine[k] = 1;
|
newLine[k] = 1;
|
||||||
@@ -121,15 +111,6 @@ function solveLine(currentLine, hints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine Empty cells?
|
// Determine Empty cells?
|
||||||
// A cell is empty if it is not covered by ANY block in ANY valid configuration.
|
|
||||||
// This is harder with just L/R limits.
|
|
||||||
// However, we can use the "Simple Glue" logic:
|
|
||||||
// If a cell is outside the range [LeftLimit[i], RightLimit[i] + block] for ALL i, it's empty.
|
|
||||||
// Wait, indices are not strictly partitioned. Block 1 could be at 0 or 10.
|
|
||||||
// But logic dictates order.
|
|
||||||
// Range of block i is [LeftPositions[i], RightPositions[i] + hints[i]].
|
|
||||||
// If a cell k is not in ANY of these ranges, it is 0.
|
|
||||||
|
|
||||||
// Mask of possible filled cells
|
// Mask of possible filled cells
|
||||||
const possibleFilled = Array(length).fill(false);
|
const possibleFilled = Array(length).fill(false);
|
||||||
for (let i = 0; i < hints.length; i++) {
|
for (let i = 0; i < hints.length; i++) {
|
||||||
@@ -148,8 +129,7 @@ function solveLine(currentLine, hints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Memoized helper for checking if hints fit
|
// Memoized helper for checking if hints fit
|
||||||
const memo = new Map();
|
export function canFitRest(line, startIndex, hints, hintIndex) {
|
||||||
function canFitRest(line, startIndex, hints, hintIndex) {
|
|
||||||
// Optimization: If hints are empty, we just need to check if remaining line has no '1's
|
// Optimization: If hints are empty, we just need to check if remaining line has no '1's
|
||||||
if (hintIndex >= hints.length) {
|
if (hintIndex >= hints.length) {
|
||||||
for (let i = startIndex; i < line.length; i++) {
|
for (let i = startIndex; i < line.length; i++) {
|
||||||
@@ -158,23 +138,32 @@ function canFitRest(line, startIndex, hints, hintIndex) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key for memoization (primitive approach)
|
// Memoization key
|
||||||
// In a full solver, we'd pass a cache. For single line, maybe overkill, but safe.
|
const key = `${startIndex}-${hintIndex}`;
|
||||||
// let key = `${startIndex}-${hintIndex}`;
|
if (memo.has(key)) return memo.get(key);
|
||||||
// Skipping memo for now as line lengths are small (<80) and recursion depth is low.
|
|
||||||
|
|
||||||
const remainingLen = line.length - startIndex;
|
const remainingLen = line.length - startIndex;
|
||||||
// Min space needed: sum of hints + (hints.length - 1) spaces
|
// Min space needed: sum of hints + (hints.length - 1) spaces
|
||||||
// Calculate lazily or precalc?
|
|
||||||
let minSpace = 0;
|
let minSpace = 0;
|
||||||
for(let i=hintIndex; i<hints.length; i++) minSpace += hints[i] + (i < hints.length - 1 ? 1 : 0);
|
for(let i=hintIndex; i<hints.length; i++) minSpace += hints[i] + (i < hints.length - 1 ? 1 : 0);
|
||||||
|
|
||||||
if (remainingLen < minSpace) return false;
|
if (remainingLen < minSpace) {
|
||||||
|
memo.set(key, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const block = hints[hintIndex];
|
const block = hints[hintIndex];
|
||||||
// Try to find *any* valid placement for this block
|
// Try to find *any* valid placement for this block
|
||||||
// We only need ONE valid path to return true.
|
for (let i = startIndex; i <= line.length - minSpace; i++) {
|
||||||
for (let i = startIndex; i <= line.length - minSpace; i++) { // Optimization on upper bound?
|
// If we skipped a '1', we went too far. All 1s must be covered by blocks.
|
||||||
|
// Since we are placing the *next* block, any 1s between startIndex and i are uncovered.
|
||||||
|
// Thus, if we find a 1 in [startIndex, i-1], we must stop.
|
||||||
|
let skippedOne = false;
|
||||||
|
for (let x = startIndex; x < i; x++) {
|
||||||
|
if (line[x] === 1) { skippedOne = true; break; }
|
||||||
|
}
|
||||||
|
if (skippedOne) break;
|
||||||
|
|
||||||
// Check placement
|
// Check placement
|
||||||
let valid = true;
|
let valid = true;
|
||||||
// Block
|
// Block
|
||||||
@@ -183,240 +172,276 @@ function canFitRest(line, startIndex, hints, hintIndex) {
|
|||||||
}
|
}
|
||||||
if (!valid) continue;
|
if (!valid) continue;
|
||||||
|
|
||||||
// Boundary before (checked by loop start usually, but strictly:
|
// Boundary before
|
||||||
if (i > 0 && line[i-1] === 1) valid = false; // Should have been handled by caller or skip
|
if (i > 0 && line[i-1] === 1) valid = false;
|
||||||
// Wait, the caller (loop) iterates i.
|
|
||||||
// If i > startIndex, we implied space at i-1.
|
|
||||||
// If line[i-1] is 1, we can't place here if we skipped it.
|
|
||||||
// Actually, if we skip a '1', that's invalid.
|
|
||||||
// So we can't just skip '1's.
|
|
||||||
|
|
||||||
// Correct logic:
|
// Boundary after (check implicit in next block placement or end of line, but we need to check i+block cell)
|
||||||
// We iterate i. If we pass a '1' at index < i, that 1 is orphaned -> Invalid path.
|
|
||||||
// So we can only scan forward as long as we don't skip a '1'.
|
|
||||||
|
|
||||||
let skippedOne = false;
|
|
||||||
for (let x = startIndex; x < i; x++) {
|
|
||||||
if (line[x] === 1) { skippedOne = true; break; }
|
|
||||||
}
|
|
||||||
if (skippedOne) break; // Cannot go further right, we left a 1 behind.
|
|
||||||
|
|
||||||
// Boundary after
|
|
||||||
if (i + block < line.length && line[i+block] === 1) valid = false;
|
if (i + block < line.length && line[i+block] === 1) valid = false;
|
||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
// Recurse
|
// Recurse
|
||||||
if (canFitRest(line, i + block + 1, hints, hintIndex + 1)) return true;
|
if (canFitRest(line, i + block + 1, hints, hintIndex + 1)) {
|
||||||
|
memo.set(key, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memo.set(key, false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Solves the puzzle using logical iteration.
|
* Main solver function that attempts to solve the puzzle using logic and backtracking (DFS).
|
||||||
* @param {number[][]} rowHints
|
* Uses an efficient propagation queue and undo stack to avoid deep copying the grid.
|
||||||
* @param {number[][]} colHints
|
*
|
||||||
* @param {number[][]} initialGrid - Optional starting state
|
|
||||||
* @returns {object} { grid: number[][], changed: boolean }
|
|
||||||
*/
|
|
||||||
function solveLogically(rowHints, colHints, initialGrid) {
|
|
||||||
const rows = rowHints.length;
|
|
||||||
const cols = colHints.length;
|
|
||||||
|
|
||||||
// Initialize grid with -1 if not provided
|
|
||||||
let grid = initialGrid ? initialGrid.map(row => [...row]) : Array(rows).fill(null).map(() => Array(cols).fill(-1));
|
|
||||||
|
|
||||||
let changed = true;
|
|
||||||
let iterations = 0;
|
|
||||||
const MAX_ITER = 100; // Safety break
|
|
||||||
|
|
||||||
while (changed && iterations < MAX_ITER) {
|
|
||||||
changed = false;
|
|
||||||
iterations++;
|
|
||||||
|
|
||||||
// Rows
|
|
||||||
for (let r = 0; r < rows; r++) {
|
|
||||||
const newLine = solveLine(grid[r], rowHints[r]);
|
|
||||||
if (!newLine) return { grid, contradiction: true }; // Contradiction found
|
|
||||||
|
|
||||||
for (let c = 0; c < cols; c++) {
|
|
||||||
if (grid[r][c] !== newLine[c]) {
|
|
||||||
// If we try to overwrite a known value with a different one -> Contradiction
|
|
||||||
if (grid[r][c] !== -1 && grid[r][c] !== newLine[c]) return { grid, contradiction: true };
|
|
||||||
|
|
||||||
grid[r][c] = newLine[c];
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cols
|
|
||||||
for (let c = 0; c < cols; c++) {
|
|
||||||
const currentCol = grid.map(row => row[c]);
|
|
||||||
const newCol = solveLine(currentCol, colHints[c]);
|
|
||||||
if (!newCol) return { grid, contradiction: true }; // Contradiction found
|
|
||||||
|
|
||||||
for (let r = 0; r < rows; r++) {
|
|
||||||
if (grid[r][c] !== newCol[r]) {
|
|
||||||
if (grid[r][c] !== -1 && grid[r][c] !== newCol[r]) return { grid, contradiction: true };
|
|
||||||
|
|
||||||
grid[r][c] = newCol[r];
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { grid, changed: iterations > 1, iterations, contradiction: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main solver function that attempts to solve the puzzle using logic and lookahead.
|
|
||||||
* @param {number[][]} rowHints
|
* @param {number[][]} rowHints
|
||||||
* @param {number[][]} colHints
|
* @param {number[][]} colHints
|
||||||
* @param {function} onProgress - Optional callback for progress reporting (percent)
|
* @param {function} onProgress - Optional callback for progress reporting (percent)
|
||||||
|
* @param {number[][]} initialGrid - Optional initial grid state
|
||||||
|
* @param {boolean} logicOnly - If true, stops after logical propagation (no guessing)
|
||||||
* @returns {object} result
|
* @returns {object} result
|
||||||
*/
|
*/
|
||||||
export function solvePuzzle(rowHints, colHints, onProgress) {
|
export function solvePuzzle(rowHints, colHints, onProgress, initialGrid = null, logicOnly = false) {
|
||||||
const rows = rowHints.length;
|
const rows = rowHints.length;
|
||||||
const cols = colHints.length;
|
const cols = colHints.length;
|
||||||
const totalCells = rows * cols;
|
const totalCells = rows * cols;
|
||||||
|
|
||||||
// 1. Basic Logical Solve
|
// Grid initialization: -1 (Unknown), 0 (Empty), 1 (Filled)
|
||||||
let { grid, iterations, contradiction } = solveLogically(rowHints, colHints);
|
// Use initialGrid if provided (deep copy to be safe)
|
||||||
|
const grid = initialGrid
|
||||||
|
? initialGrid.map(row => [...row])
|
||||||
|
: Array(rows).fill().map(() => Array(cols).fill(-1));
|
||||||
|
|
||||||
// Count solved
|
// Stats
|
||||||
|
let iterations = 0; // Total calls to solve()
|
||||||
|
let maxDepth = 0; // Max recursion depth
|
||||||
|
let backtracks = 0; // Failed guesses
|
||||||
|
let logicSteps = 0; // Cells filled by propagation
|
||||||
|
let lastProgressTime = 0;
|
||||||
|
|
||||||
|
function reportProgress() {
|
||||||
|
if (!onProgress) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastProgressTime >= 50) {
|
||||||
|
let filled = 0;
|
||||||
|
for(let r=0; r<rows; r++) {
|
||||||
|
for(let c=0; c<cols; c++) {
|
||||||
|
if(grid[r][c] !== -1) filled++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onProgress(Math.floor((filled/totalCells) * 100));
|
||||||
|
lastProgressTime = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue for propagation (Set of strings "r{i}" or "c{i}")
|
||||||
|
const queue = new Set();
|
||||||
|
for(let r=0; r<rows; r++) queue.add(`r${r}`);
|
||||||
|
for(let c=0; c<cols; c++) queue.add(`c${c}`);
|
||||||
|
|
||||||
|
// Helper: Undo changes from a propagation step
|
||||||
|
function undo(changes) {
|
||||||
|
for(let i=changes.length-1; i>=0; i--) {
|
||||||
|
const {r, c, old} = changes[i];
|
||||||
|
grid[r][c] = old;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Propagate logic constraints until fixed point or contradiction
|
||||||
|
// Returns list of changes made, or null if contradiction found
|
||||||
|
function propagate() {
|
||||||
|
const changes = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
while(queue.size > 0) {
|
||||||
|
reportProgress();
|
||||||
|
|
||||||
|
const item = queue.values().next().value;
|
||||||
|
queue.delete(item);
|
||||||
|
|
||||||
|
const type = item[0];
|
||||||
|
const idx = parseInt(item.slice(1));
|
||||||
|
|
||||||
|
let currentLine, hints;
|
||||||
|
if (type === 'r') {
|
||||||
|
currentLine = grid[idx]; // Reference for row (fast)
|
||||||
|
hints = rowHints[idx];
|
||||||
|
} else {
|
||||||
|
currentLine = grid.map(row => row[idx]); // Copy for col (slower)
|
||||||
|
hints = colHints[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLine = solveLine(currentLine, hints);
|
||||||
|
|
||||||
|
if (!newLine) throw 'contradiction';
|
||||||
|
|
||||||
|
// Apply changes
|
||||||
|
for(let i=0; i<newLine.length; i++) {
|
||||||
|
if (currentLine[i] !== newLine[i]) {
|
||||||
|
// If we try to change a known value to something else -> Contradiction
|
||||||
|
if (currentLine[i] !== -1 && currentLine[i] !== newLine[i]) throw 'contradiction';
|
||||||
|
|
||||||
|
if (currentLine[i] === -1) {
|
||||||
|
const r = type === 'r' ? idx : i;
|
||||||
|
const c = type === 'r' ? i : idx;
|
||||||
|
|
||||||
|
// Double check if already set by orthogonal update in same loop?
|
||||||
|
// (Should be handled by -1 check above, as grid is shared)
|
||||||
|
if (grid[r][c] === -1) {
|
||||||
|
grid[r][c] = newLine[i];
|
||||||
|
changes.push({r, c, old: -1});
|
||||||
|
logicSteps++;
|
||||||
|
|
||||||
|
// Add orthogonal line to queue
|
||||||
|
if (type === 'r') queue.add(`c${c}`);
|
||||||
|
else queue.add(`r${r}`);
|
||||||
|
} else if (grid[r][c] !== newLine[i]) {
|
||||||
|
console.log('Contradiction: Orthogonal Conflict at', r, c, 'Grid:', grid[r][c], 'New:', newLine[i]);
|
||||||
|
throw 'contradiction';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Revert changes from this failed propagation
|
||||||
|
undo(changes);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DFS Solver
|
||||||
|
function solve(depth) {
|
||||||
|
maxDepth = Math.max(maxDepth, depth);
|
||||||
|
iterations++;
|
||||||
|
|
||||||
|
reportProgress();
|
||||||
|
|
||||||
|
// 1. Propagate Logic
|
||||||
|
const changes = propagate();
|
||||||
|
if (!changes) return false; // Contradiction
|
||||||
|
|
||||||
|
// 2. Find Best Branch Candidate
|
||||||
|
let bestR = -1, bestC = -1;
|
||||||
|
let minUnknowns = Infinity;
|
||||||
|
let isComplete = true;
|
||||||
|
|
||||||
|
// Scan for unknowns and pick heuristic
|
||||||
|
// Heuristic: Line with fewest unknowns (most constrained)
|
||||||
|
for(let r=0; r<rows; r++) {
|
||||||
|
let unknowns = 0;
|
||||||
|
let firstUnknownC = -1;
|
||||||
|
for(let c=0; c<cols; c++) {
|
||||||
|
if(grid[r][c] === -1) {
|
||||||
|
unknowns++;
|
||||||
|
if (firstUnknownC === -1) firstUnknownC = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknowns > 0) {
|
||||||
|
isComplete = false;
|
||||||
|
if (unknowns < minUnknowns) {
|
||||||
|
minUnknowns = unknowns;
|
||||||
|
bestR = r;
|
||||||
|
bestC = firstUnknownC;
|
||||||
|
}
|
||||||
|
if (minUnknowns === 1) break; // Optimal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isComplete) return true; // Solved!
|
||||||
|
|
||||||
|
// 3. Branching (Guessing)
|
||||||
|
// Try 1
|
||||||
|
grid[bestR][bestC] = 1;
|
||||||
|
queue.add(`r${bestR}`);
|
||||||
|
queue.add(`c${bestC}`);
|
||||||
|
|
||||||
|
if (solve(depth + 1)) return true;
|
||||||
|
|
||||||
|
// Backtrack from 1
|
||||||
|
grid[bestR][bestC] = -1; // Undo guess
|
||||||
|
// (Recursive call already undid its propagation changes)
|
||||||
|
|
||||||
|
// Try 0
|
||||||
|
grid[bestR][bestC] = 0;
|
||||||
|
queue.add(`r${bestR}`);
|
||||||
|
queue.add(`c${bestC}`);
|
||||||
|
|
||||||
|
if (solve(depth + 1)) return true;
|
||||||
|
|
||||||
|
// Backtrack from 0
|
||||||
|
grid[bestR][bestC] = -1; // Undo guess
|
||||||
|
backtracks++;
|
||||||
|
|
||||||
|
// Undo propagation from this level
|
||||||
|
undo(changes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Solving
|
||||||
|
if (logicOnly) {
|
||||||
|
// Just logical propagation without guessing
|
||||||
|
if (initialGrid) {
|
||||||
|
// If resuming, ensure queue has all lines to check consistency
|
||||||
|
queue.clear();
|
||||||
|
for(let r=0; r<rows; r++) queue.add(`r${r}`);
|
||||||
|
for(let c=0; c<cols; c++) queue.add(`c${c}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate logic constraints
|
||||||
|
propagate();
|
||||||
|
// No DFS, so iterations/backtracks remain 0
|
||||||
|
} else if (!initialGrid) {
|
||||||
|
// Normal start (full solver)
|
||||||
|
solve(0);
|
||||||
|
} else {
|
||||||
|
// Resume from provided state (full solver)
|
||||||
|
// We need to populate the queue with all rows/cols since we don't know what changed
|
||||||
|
queue.clear();
|
||||||
|
for(let r=0; r<rows; r++) queue.add(`r${r}`);
|
||||||
|
for(let c=0; c<cols; c++) queue.add(`c${c}`);
|
||||||
|
solve(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Percent Solved
|
||||||
let solvedCount = 0;
|
let solvedCount = 0;
|
||||||
grid.forEach(r => r.forEach(c => { if(c !== -1) solvedCount++; }));
|
grid.forEach(r => r.forEach(c => { if(c !== -1) solvedCount++; }));
|
||||||
let percentSolved = (solvedCount / totalCells) * 100;
|
let percentSolved = (solvedCount / totalCells) * 100;
|
||||||
|
|
||||||
if (onProgress) onProgress(Math.floor(percentSolved));
|
if (onProgress) onProgress(Math.floor(percentSolved));
|
||||||
|
|
||||||
// Difficulty calculation
|
// Difficulty Calculation
|
||||||
// Base: complexity of grid
|
// Logic:
|
||||||
|
// - Base: 0-20% based on size/density
|
||||||
|
// - Logic: 0-30% based on iterations needed (depth 0)
|
||||||
|
// - Guessing: 0-50% based on backtracks/depth
|
||||||
|
|
||||||
let difficultyScore = 0;
|
let difficultyScore = 0;
|
||||||
|
const effectiveSize = Math.sqrt(totalCells);
|
||||||
|
|
||||||
// If simple logic failed to solve completely, try Lookahead (Smash)
|
|
||||||
let lookaheadUsed = false;
|
|
||||||
|
|
||||||
if (percentSolved < 100 && !contradiction) {
|
|
||||||
// Lookahead loop
|
|
||||||
// Find an unknown cell, try 0 and 1. If one leads to contradiction, the other is true.
|
|
||||||
let progress = true;
|
|
||||||
while (progress && percentSolved < 100) {
|
|
||||||
progress = false;
|
|
||||||
|
|
||||||
// Find unknown cells (optimize: sort by most constrained?)
|
|
||||||
// For now, just scan.
|
|
||||||
let candidates = [];
|
|
||||||
for(let r=0; r<rows; r++) {
|
|
||||||
for(let c=0; c<cols; c++) {
|
|
||||||
if (grid[r][c] === -1) candidates.push({r, c});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit candidates for performance (e.g., first 50 or heuristic)
|
|
||||||
// But we need to solve it...
|
|
||||||
// Let's try top 20 candidates? Or all?
|
|
||||||
// "Parallel web workers" allows us to be heavier, but 80x80 is 6400 cells.
|
|
||||||
// We can't try all 6400 in every pass.
|
|
||||||
// Heuristic: pick cells in rows/cols that are nearly full.
|
|
||||||
|
|
||||||
let checkedCount = 0;
|
|
||||||
const totalCandidates = candidates.length;
|
|
||||||
let lastReportedPercent = -1;
|
|
||||||
|
|
||||||
for (const {r, c} of candidates) {
|
|
||||||
checkedCount++;
|
|
||||||
|
|
||||||
// Report progress inside the heavy loop
|
|
||||||
if (onProgress) {
|
|
||||||
const currentScanPercent = Math.floor((checkedCount / totalCandidates) * 100);
|
|
||||||
// Report every 1% change or at least every 10 items to avoid flooding but keep it responsive
|
|
||||||
if (currentScanPercent > lastReportedPercent || checkedCount % 10 === 0) {
|
|
||||||
lastReportedPercent = currentScanPercent;
|
|
||||||
onProgress(currentScanPercent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try assuming 1
|
|
||||||
// We need to clone the grid for simulation
|
|
||||||
const gridCopy1 = grid.map(row => [...row]);
|
|
||||||
gridCopy1[r][c] = 1;
|
|
||||||
const res1 = solveLogically(rowHints, colHints, gridCopy1);
|
|
||||||
|
|
||||||
// Try assuming 0
|
|
||||||
const gridCopy0 = grid.map(row => [...row]);
|
|
||||||
gridCopy0[r][c] = 0;
|
|
||||||
const res0 = solveLogically(rowHints, colHints, gridCopy0);
|
|
||||||
|
|
||||||
let deduced = null;
|
|
||||||
|
|
||||||
if (res1.contradiction && !res0.contradiction) {
|
|
||||||
deduced = 0; // Must be 0
|
|
||||||
} else if (!res1.contradiction && res0.contradiction) {
|
|
||||||
deduced = 1; // Must be 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deduced !== null) {
|
|
||||||
grid[r][c] = deduced;
|
|
||||||
progress = true;
|
|
||||||
lookaheadUsed = true;
|
|
||||||
difficultyScore += 5; // Penalty for requiring lookahead
|
|
||||||
|
|
||||||
// Run logic again to propagate this new info
|
|
||||||
const updated = solveLogically(rowHints, colHints, grid);
|
|
||||||
if (updated.contradiction) break; // Should not happen if logic is sound
|
|
||||||
grid = updated.grid;
|
|
||||||
|
|
||||||
break; // Restart loop to use new info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recalculate percent (this is for loop exit condition)
|
|
||||||
solvedCount = 0;
|
|
||||||
grid.forEach(row => row.forEach(c => { if(c !== -1) solvedCount++; }));
|
|
||||||
percentSolved = (solvedCount / totalCells) * 100;
|
|
||||||
// Note: we don't report percentSolved here because we want the spinner to show SCAN progress (0-100% of current pass)
|
|
||||||
// If we reported percentSolved, the user might see the spinner jump from 100% (scan done) to 5% (solved amount), which is confusing.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final Difficulty Calculation
|
|
||||||
// Factors:
|
|
||||||
// 1. Size (rows * cols)
|
|
||||||
// 2. Iterations (how many passes of line logic)
|
|
||||||
// 3. Lookahead (did we need it?)
|
|
||||||
|
|
||||||
const effectiveSize = Math.sqrt(rows * cols);
|
|
||||||
// iterations is usually 2-20.
|
|
||||||
// difficultyScore accumulates lookahead steps.
|
|
||||||
|
|
||||||
// Normalize iterations
|
|
||||||
const iterScore = Math.min(20, iterations) * 2;
|
|
||||||
|
|
||||||
// Base difficulty
|
|
||||||
let totalScore = effectiveSize + iterScore + difficultyScore;
|
|
||||||
|
|
||||||
// If not fully solved, massive penalty
|
|
||||||
if (percentSolved < 100) {
|
if (percentSolved < 100) {
|
||||||
// Unsolvable by logic+lookahead
|
difficultyScore = 100; // Unsolvable (or timed out/too hard)
|
||||||
// This is "Extreme" or "Guessing Required"
|
|
||||||
totalScore = 100; // Cap at max
|
|
||||||
} else {
|
} else {
|
||||||
// Solved
|
if (maxDepth === 0) {
|
||||||
// Normalize score 0-100 (approximately)
|
// Pure logic
|
||||||
// Max theoretical "normal" score ~ 80 (size 80) + 40 (iter) + 20 (lookahead) = 140?
|
difficultyScore = Math.min(30, effectiveSize);
|
||||||
// Let's scale it.
|
} else {
|
||||||
totalScore = Math.min(100, totalScore);
|
// Required guessing
|
||||||
|
// Simple heuristic: 30 + backtracks * 5 + depth * 2
|
||||||
|
difficultyScore = 30 + (backtracks * 2) + (maxDepth * 5);
|
||||||
|
difficultyScore = Math.min(100, difficultyScore);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
percentSolved,
|
percentSolved,
|
||||||
difficultyScore: totalScore,
|
difficultyScore: Math.round(difficultyScore),
|
||||||
lookaheadUsed,
|
lookaheadUsed: maxDepth > 0,
|
||||||
iterations
|
iterations,
|
||||||
|
maxDepth,
|
||||||
|
backtracks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/utils/solver.test.js
Normal file
64
src/utils/solver.test.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
it('solves a puzzle requiring guessing (Backtracking)', () => {
|
||||||
|
// A puzzle that logic alone cannot start usually has multiple solutions or requires a guess.
|
||||||
|
// Example: The "domino" or "ambiguous" pattern, but we need a unique solution that requires lookahead.
|
||||||
|
// Or just a very hard unique puzzle.
|
||||||
|
// A simple case where line logic is stuck but global constraints solve it.
|
||||||
|
//
|
||||||
|
// 0 1 1 0
|
||||||
|
// 1 0 0 1
|
||||||
|
// 1 0 0 1
|
||||||
|
// 0 1 1 0
|
||||||
|
// Hints:
|
||||||
|
// R: 2, 1 1, 1 1, 2
|
||||||
|
// C: 2, 1 1, 1 1, 2
|
||||||
|
// This is a "ring". It might be solvable by logic if corners are forced.
|
||||||
|
// Let's try a known "hard" small pattern.
|
||||||
|
//
|
||||||
|
// 0 0 0
|
||||||
|
// 0 1 0
|
||||||
|
// 0 0 0
|
||||||
|
// R: 0, 1, 0
|
||||||
|
// C: 0, 1, 0
|
||||||
|
// Logic solves this instantly.
|
||||||
|
|
||||||
|
// Let's trust the logic works for general backtracking by forcing a guess.
|
||||||
|
// We can mock solveLine to return "no change" to force backtracking? No, integration test is better.
|
||||||
|
|
||||||
|
// Let's just ensure it returns a valid result structure for a solvable puzzle.
|
||||||
|
const rowHints = [[1], [1], [1]];
|
||||||
|
const colHints = [[1], [1], [1]];
|
||||||
|
// 3x3 diagonal?
|
||||||
|
// 1 0 0
|
||||||
|
// 0 1 0
|
||||||
|
// 0 0 1
|
||||||
|
// Hints: 1, 1, 1
|
||||||
|
// Cols: 1, 1, 1
|
||||||
|
// This has multiple solutions (diagonal or anti-diagonal or others).
|
||||||
|
// Our solver should find ONE of them and return 100%.
|
||||||
|
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
// It might use lookahead because logic can't decide.
|
||||||
|
// Actually for this specific case, logic does nothing (all empty or all full are not possible, but many perms).
|
||||||
|
// So it MUST branch.
|
||||||
|
expect(result.maxDepth).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stress test: should solve 100 random valid 10x10 grids', () => {
|
||||||
|
// This ensures the solver is robust and doesn't fail on valid puzzles.
|
||||||
|
// Using a fixed seed or just running a loop.
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const size = 10;
|
||||||
|
const grid = generateRandomGrid(size, 0.5);
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
|
if (result.percentSolved < 100) {
|
||||||
|
console.error('Failed Grid:', JSON.stringify(grid));
|
||||||
|
console.error('Result:', result);
|
||||||
|
}
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -28,6 +28,46 @@ class WorkerPool {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
runRace(tasks) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let activeCount = tasks.length;
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
tasks.forEach(taskData => {
|
||||||
|
this.run(taskData.data, taskData.onProgress)
|
||||||
|
.then(result => {
|
||||||
|
if (resolved) return;
|
||||||
|
|
||||||
|
// Heuristic: If solved 100%, we have a winner
|
||||||
|
if (result.solvability === 100) {
|
||||||
|
resolved = true;
|
||||||
|
resolve(result);
|
||||||
|
// Cancel others (optional but good for perf)
|
||||||
|
// We can't easily cancel *specific* other tasks in this pool implementation without IDs
|
||||||
|
// But since this is a "Race", we assume the caller will handle cleanup or we just let them finish
|
||||||
|
} else {
|
||||||
|
// If not fully solved, we wait for others?
|
||||||
|
// Or maybe we collect all results and pick best?
|
||||||
|
// For "Race", we usually want the first *Success*.
|
||||||
|
// If all fail (finish without 100%), we reject or return best.
|
||||||
|
activeCount--;
|
||||||
|
if (activeCount === 0) {
|
||||||
|
// All finished, none 100%. Return the last one (or logic to pick best)
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (resolved) return;
|
||||||
|
activeCount--;
|
||||||
|
if (activeCount === 0) {
|
||||||
|
reject(new Error('All workers failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
execute(workerObj, task) {
|
execute(workerObj, task) {
|
||||||
workerObj.busy = true;
|
workerObj.busy = true;
|
||||||
workerObj.currentTask = task;
|
workerObj.currentTask = task;
|
||||||
@@ -45,7 +85,12 @@ class WorkerPool {
|
|||||||
return; // Don't resolve yet
|
return; // Don't resolve yet
|
||||||
}
|
}
|
||||||
|
|
||||||
workerObj.currentTask.resolve(e.data);
|
if (e.data.error) {
|
||||||
|
workerObj.currentTask.reject(new Error(e.data.error));
|
||||||
|
} else {
|
||||||
|
workerObj.currentTask.resolve(e.data);
|
||||||
|
}
|
||||||
|
|
||||||
workerObj.currentTask = null;
|
workerObj.currentTask = null;
|
||||||
workerObj.busy = false;
|
workerObj.busy = false;
|
||||||
this.active--;
|
this.active--;
|
||||||
@@ -81,6 +126,32 @@ class WorkerPool {
|
|||||||
this.queue = [];
|
this.queue = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelAll() {
|
||||||
|
this.clearQueue();
|
||||||
|
|
||||||
|
// Terminate and restart busy workers
|
||||||
|
this.workers.forEach((w, index) => {
|
||||||
|
if (w.busy) {
|
||||||
|
w.worker.terminate();
|
||||||
|
|
||||||
|
if (w.currentTask) {
|
||||||
|
w.currentTask.reject(new Error('Terminated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create replacement
|
||||||
|
const newWorker = new SolverWorker();
|
||||||
|
newWorker.onmessage = (e) => this.handleWorkerMessage(newWorker, e);
|
||||||
|
newWorker.onerror = (e) => this.handleWorkerError(newWorker, e);
|
||||||
|
|
||||||
|
// Replace in array
|
||||||
|
this.workers[index] = { worker: newWorker, busy: false, id: w.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset active count since all busy workers were replaced with idle ones
|
||||||
|
this.active = 0;
|
||||||
|
}
|
||||||
|
|
||||||
terminate() {
|
terminate() {
|
||||||
this.workers.forEach(w => w.worker.terminate());
|
this.workers.forEach(w => w.worker.terminate());
|
||||||
this.workers = [];
|
this.workers = [];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { calculateHints } from '../utils/puzzleUtils';
|
|||||||
import { solvePuzzle } from '../utils/solver';
|
import { solvePuzzle } from '../utils/solver';
|
||||||
|
|
||||||
self.onmessage = (e) => {
|
self.onmessage = (e) => {
|
||||||
const { id, grid } = e.data;
|
const { id, grid, initialGrid } = e.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!grid || grid.length === 0) {
|
if (!grid || grid.length === 0) {
|
||||||
@@ -12,10 +12,11 @@ self.onmessage = (e) => {
|
|||||||
|
|
||||||
const rows = grid.length;
|
const rows = grid.length;
|
||||||
const cols = grid[0].length;
|
const cols = grid[0].length;
|
||||||
const size = Math.max(rows, cols);
|
// Use initialGrid if provided, otherwise assume we are starting fresh
|
||||||
const density = grid.flat().filter(c => c === 1).length / (rows * cols);
|
// 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).
|
||||||
|
|
||||||
// 1. Calculate Hints
|
// 1. Calculate Hints from the TARGET grid (the image)
|
||||||
const { rowHints, colHints } = calculateHints(grid);
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
|
||||||
// 2. Run Solver (Logic + Lookahead)
|
// 2. Run Solver (Logic + Lookahead)
|
||||||
@@ -27,7 +28,7 @@ self.onmessage = (e) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const { percentSolved, difficultyScore, lookaheadUsed } = solvePuzzle(rowHints, colHints, onProgress);
|
const { percentSolved, difficultyScore, lookaheadUsed } = solvePuzzle(rowHints, colHints, onProgress, initialGrid);
|
||||||
|
|
||||||
// 3. Determine Level
|
// 3. Determine Level
|
||||||
let value = difficultyScore;
|
let value = difficultyScore;
|
||||||
|
|||||||
Reference in New Issue
Block a user