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:
@@ -93,6 +93,7 @@ const handlePointerCancel = (e) => {
|
||||
height: var(--cell-size);
|
||||
background-color: var(--cell-empty);
|
||||
border: 1px solid var(--glass-border);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -10,6 +10,10 @@ const store = usePuzzleStore();
|
||||
const { rowHints, colHints } = useHints(computed(() => store.solution));
|
||||
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 rowHintsRef = ref(null);
|
||||
const activeRow = ref(null);
|
||||
@@ -143,13 +147,16 @@ const computeCellSize = () => {
|
||||
// Ensure we don't have negative space
|
||||
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) {
|
||||
// Desktop: Allow overflow, use comfortable size
|
||||
cellSize.value = 30;
|
||||
} else {
|
||||
// Mobile: Fit to screen
|
||||
// Mobile: Fit to screen width
|
||||
// Keep min 18, max 36
|
||||
cellSize.value = Math.max(18, Math.min(36, size));
|
||||
}
|
||||
@@ -240,17 +247,17 @@ watch(() => store.size, async () => {
|
||||
<div class="corner-spacer"></div>
|
||||
|
||||
<!-- Column Hints -->
|
||||
<Hints :hints="colHints" orientation="col" :size="store.size" :activeIndex="activeCol" />
|
||||
<Hints :hints="colHints" orientation="col" :size="gridCols" :activeIndex="activeCol" />
|
||||
|
||||
<!-- 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 -->
|
||||
<div
|
||||
class="grid"
|
||||
:style="{
|
||||
gridTemplateColumns: `repeat(${store.size}, var(--cell-size))`,
|
||||
gridTemplateRows: `repeat(${store.size}, var(--cell-size))`
|
||||
gridTemplateColumns: `repeat(${gridCols}, var(--cell-size))`,
|
||||
gridTemplateRows: `repeat(${gridRows}, var(--cell-size))`
|
||||
}"
|
||||
@pointermove.prevent="handlePointerMove"
|
||||
@mouseleave="handleGridLeave"
|
||||
@@ -263,8 +270,8 @@ watch(() => store.size, async () => {
|
||||
:r="r"
|
||||
:c="c"
|
||||
:class="{
|
||||
'guide-right': (c + 1) % 5 === 0 && c !== store.size - 1,
|
||||
'guide-bottom': (r + 1) % 5 === 0 && r !== store.size - 1
|
||||
'guide-right': (c + 1) % 5 === 0 && c !== gridCols - 1,
|
||||
'guide-bottom': (r + 1) % 5 === 0 && r !== gridRows - 1
|
||||
}"
|
||||
@start-drag="startDrag"
|
||||
@enter-cell="handleCellEnter"
|
||||
|
||||
@@ -58,13 +58,13 @@ defineProps({
|
||||
|
||||
.hints-container.col {
|
||||
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-right: var(--grid-padding);
|
||||
}
|
||||
|
||||
.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;
|
||||
width: max-content;
|
||||
}
|
||||
@@ -99,6 +99,21 @@ defineProps({
|
||||
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 */
|
||||
.hint-num.hint-alt {
|
||||
color: var(--accent-cyan);
|
||||
|
||||
@@ -225,16 +225,36 @@ const calculateStats = async () => {
|
||||
|
||||
try {
|
||||
const pool = getWorkerPool();
|
||||
pool.clearQueue(); // Clear pending tasks
|
||||
pool.cancelAll(); // Force stop previous calculations for immediate responsiveness
|
||||
|
||||
const result = await pool.run({
|
||||
id: requestId,
|
||||
grid: generatedGrid.value
|
||||
}, (progress) => {
|
||||
if (currentStatsRequestId === requestId) {
|
||||
processingProgress.value = progress;
|
||||
// Demonstrate parallel execution capability
|
||||
// We split the problem into two branches: assuming first cell is EMPTY (0) vs FILLED (1)
|
||||
// This doubles the search power by utilizing 2 workers immediately.
|
||||
|
||||
// Ensure we send plain objects to workers, not Vue proxies
|
||||
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) {
|
||||
solvability.value = result.solvability;
|
||||
@@ -242,12 +262,14 @@ const calculateStats = async () => {
|
||||
difficultyLabel.value = result.difficultyLabel;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.message !== 'Cancelled') {
|
||||
if (err.message !== 'Cancelled' && err.message !== 'Terminated') {
|
||||
console.error('Worker error:', err);
|
||||
if (currentStatsRequestId === requestId) {
|
||||
solvability.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 {
|
||||
@@ -266,6 +288,11 @@ watch([maxDimension, threshold], () => {
|
||||
debounceTimer = setTimeout(() => {
|
||||
updateGrid();
|
||||
}, 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;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
color: var(--text-color);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { usePuzzleStore } from '@/stores/puzzle';
|
||||
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 { t, locale, setLocale, locales } = useI18n();
|
||||
@@ -16,6 +16,13 @@ const isMobileMenuOpen = ref(false);
|
||||
const langMenuRef = ref(null);
|
||||
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
|
||||
const langToCountry = {
|
||||
en: 'gb',
|
||||
@@ -257,12 +264,15 @@ watch(isMobileMenuOpen, (val) => {
|
||||
class="dropdown-item"
|
||||
@click="selectLevel(lvl.id)"
|
||||
>
|
||||
<component :is="getLevelIcon(lvl.id)" :size="16" />
|
||||
{{ lvl.label }}
|
||||
</button>
|
||||
<button class="dropdown-item" @click="openCustom">
|
||||
<Shuffle :size="16" />
|
||||
{{ t('level.custom_random') }}
|
||||
</button>
|
||||
<button class="dropdown-item" @click="openImageImport">
|
||||
<ImageIcon :size="16" />
|
||||
{{ t('level.custom_image') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -357,12 +367,15 @@ watch(isMobileMenuOpen, (val) => {
|
||||
class="mobile-sub-item"
|
||||
@click="selectLevel(lvl.id)"
|
||||
>
|
||||
<component :is="getLevelIcon(lvl.id)" :size="16" />
|
||||
{{ lvl.label }}
|
||||
</button>
|
||||
<button class="mobile-sub-item" @click="openCustom">
|
||||
<Shuffle :size="16" />
|
||||
{{ t('level.custom_random') }}
|
||||
</button>
|
||||
<button class="mobile-sub-item" @click="openImageImport">
|
||||
<ImageIcon :size="16" />
|
||||
{{ t('level.custom_image') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -71,11 +71,12 @@ const startSimulation = async () => {
|
||||
for (let i = 0; i < SAMPLES_PER_POINT; i++) {
|
||||
const grid = generateRandomGrid(size, density);
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
Reference in New Issue
Block a user