From c3188bb740ed9ee4a5b5994780ccc08ae41f8ddc Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Fri, 13 Feb 2026 07:03:08 +0100 Subject: [PATCH] fix: prevent premature solved status, add DFS boost button, mark boosted exports --- src/components/GuidePanel.vue | 19 ++++++++++++++- src/components/WinModal.vue | 4 +++- src/composables/useI18n.js | 2 ++ src/composables/useSolver.js | 22 +++++++++++++++++- src/stores/puzzle.js | 30 ++++++++++++++++++++++++ src/utils/shareUtils.js | 40 ++++++++++++++++++++++++------- src/workers/solverWorker.js | 44 +++++++++++++++++++++++++++++++---- 7 files changed, 145 insertions(+), 16 deletions(-) diff --git a/src/components/GuidePanel.vue b/src/components/GuidePanel.vue index c3bb9f1..db1da1a 100644 --- a/src/components/GuidePanel.vue +++ b/src/components/GuidePanel.vue @@ -4,11 +4,13 @@ import { useI18n } from '@/composables/useI18n'; const { isPlaying, + isStuck, speedLabel, statusText, step, togglePlay, - changeSpeed + changeSpeed, + boost } = useSolver(); const { t } = useI18n(); @@ -29,6 +31,10 @@ const { t } = useI18n(); + + @@ -68,4 +74,15 @@ const { t } = useI18n(); padding: 5px 15px; font-size: 0.8rem; } + +.boost-btn { + border-color: #ffd700; + color: #ffd700; + box-shadow: 0 0 10px rgba(255, 215, 0, 0.2); +} + +.boost-btn:hover { + background: rgba(255, 215, 0, 0.1); + box-shadow: 0 0 15px rgba(255, 215, 0, 0.4); +} diff --git a/src/components/WinModal.vue b/src/components/WinModal.vue index 58924e0..9be3989 100644 --- a/src/components/WinModal.vue +++ b/src/components/WinModal.vue @@ -47,7 +47,9 @@ const getShareData = () => ({ grid: store.playerGrid, size: store.size, currentDensity: store.currentDensity, - guideUsageCount: store.guideUsageCount + guideUsageCount: store.guideUsageCount, + hasUsedBoost: store.hasUsedBoost, + boostUsageCount: store.boostUsageCount }); const downloadShareSVG = () => { diff --git a/src/composables/useI18n.js b/src/composables/useI18n.js index 55126c8..4c5022f 100644 --- a/src/composables/useI18n.js +++ b/src/composables/useI18n.js @@ -68,6 +68,7 @@ const messages = { 'win.shareDownload': 'Pobierz zrzut', 'win.difficulty': 'Poziom:', 'win.usedGuide': 'Podpowiedzi: {percent}% ({count})', + 'win.boosted': 'Wspomagany (DFS)', 'pwa.installTitle': 'Zainstaluj aplikację i graj offline', 'pwa.installMobile': 'Dodaj do ekranu głównego', 'pwa.installDesktop': 'Zainstaluj na komputerze', @@ -244,6 +245,7 @@ const messages = { 'win.shareDownload': 'Download screenshot', 'win.difficulty': 'Difficulty:', 'win.usedGuide': 'Hints: {percent}% ({count})', + 'win.boosted': 'Boosted (DFS)', 'pwa.installTitle': 'Install the app and play offline', 'pwa.installMobile': 'Add to home screen', 'pwa.installDesktop': 'Install on desktop', diff --git a/src/composables/useSolver.js b/src/composables/useSolver.js index 5949ec2..cb5a6c1 100644 --- a/src/composables/useSolver.js +++ b/src/composables/useSolver.js @@ -8,6 +8,7 @@ export function useSolver() { const isPlaying = ref(false); const isProcessing = ref(false); + const isStuck = ref(false); const speedIndex = ref(0); const speeds = [1000, 500, 250, 125, 62, 31, 16]; const speedLabels = ['x1', 'x2', 'x4', 'x8', 'x16', 'x32', 'x64']; @@ -25,6 +26,7 @@ export function useSolver() { } if (isProcessing.value) return; store.markGuideUsed(); + isStuck.value = false; ensureWorker(); isProcessing.value = true; @@ -62,6 +64,19 @@ export function useSolver() { } } + function boost() { + if (store.isGameWon || isProcessing.value) return; + store.markBoostUsed(); + isStuck.value = false; + ensureWorker(); + isProcessing.value = true; + + const playerGrid = store.playerGrid.map(row => row.slice()); + const solution = store.solution.map(row => row.slice()); + const id = ++requestId; + worker.postMessage({ id, playerGrid, solution, locale: locale.value, action: 'boost' }); + } + function ensureWorker() { if (worker) return; worker = new Worker(new URL('../workers/solverWorker.js', import.meta.url), { type: 'module' }); @@ -71,15 +86,18 @@ export function useSolver() { if (type === 'move') { store.setCell(r, c, state); isProcessing.value = false; + isStuck.value = false; if (store.isGameWon) { pause(); return; } } else if (type === 'done') { isProcessing.value = false; + isStuck.value = false; pause(); } else if (type === 'stuck') { isProcessing.value = false; + isStuck.value = true; pause(); } else { isProcessing.value = false; @@ -95,11 +113,13 @@ export function useSolver() { return { isPlaying, + isStuck, speedIndex, speedLabel: computed(() => speedLabels[speedIndex.value]), statusText, step, togglePlay, - changeSpeed + changeSpeed, + boost }; } diff --git a/src/stores/puzzle.js b/src/stores/puzzle.js index 1d942ed..0d701f6 100644 --- a/src/stores/puzzle.js +++ b/src/stores/puzzle.js @@ -11,6 +11,8 @@ export const usePuzzleStore = defineStore('puzzle', () => { const isGameWon = ref(false); const hasUsedGuide = ref(false); const guideUsageCount = ref(0); + const hasUsedBoost = ref(false); + const boostUsageCount = ref(0); const currentDifficulty = ref(null); // 'easy', 'medium', 'hard', 'custom' or object { density: 0.5 } const currentDensity = ref(0); const size = ref(5); @@ -72,6 +74,8 @@ export const usePuzzleStore = defineStore('puzzle', () => { isGameWon.value = false; hasUsedGuide.value = false; guideUsageCount.value = 0; + hasUsedBoost.value = false; + boostUsageCount.value = 0; currentDensity.value = totalCellsToFill.value / (size.value * size.value); elapsedTime.value = 0; startTimer(); @@ -90,6 +94,8 @@ export const usePuzzleStore = defineStore('puzzle', () => { isGameWon.value = false; hasUsedGuide.value = false; guideUsageCount.value = 0; + hasUsedBoost.value = false; + boostUsageCount.value = 0; currentDensity.value = density; elapsedTime.value = 0; startTimer(); @@ -110,6 +116,8 @@ export const usePuzzleStore = defineStore('puzzle', () => { isGameWon.value = false; hasUsedGuide.value = false; guideUsageCount.value = 0; + hasUsedBoost.value = false; + boostUsageCount.value = 0; // Calculate density const totalFilled = grid.flat().filter(c => c === 1).length; @@ -256,6 +264,17 @@ export const usePuzzleStore = defineStore('puzzle', () => { } if (correct) { + // Auto-fill remaining empty cells with X (2) + const rows = solution.value.length; + const cols = solution.value[0].length; + for (let r = 0; r < rows; r++) { + for (let c = 0; c < cols; c++) { + if (playerGrid.value[r][c] === 0) { + playerGrid.value[r][c] = 2; + } + } + } + isGameWon.value = true; stopTimer(); } @@ -290,6 +309,8 @@ export const usePuzzleStore = defineStore('puzzle', () => { isGameWon: isGameWon.value, hasUsedGuide: hasUsedGuide.value, guideUsageCount: guideUsageCount.value, + hasUsedBoost: hasUsedBoost.value, + boostUsageCount: boostUsageCount.value, currentDensity: currentDensity.value, elapsedTime: elapsedTime.value, moves: moves.value, @@ -310,6 +331,8 @@ export const usePuzzleStore = defineStore('puzzle', () => { isGameWon.value = parsed.isGameWon; hasUsedGuide.value = parsed.hasUsedGuide || false; guideUsageCount.value = parsed.guideUsageCount || 0; + hasUsedBoost.value = parsed.hasUsedBoost || false; + boostUsageCount.value = parsed.boostUsageCount || 0; currentDensity.value = parsed.currentDensity || 0; elapsedTime.value = parsed.elapsedTime || 0; moves.value = parsed.moves || 0; @@ -348,6 +371,13 @@ export const usePuzzleStore = defineStore('puzzle', () => { saveState(); } + function markBoostUsed() { + if (isGameWon.value) return; + hasUsedBoost.value = true; + boostUsageCount.value++; + saveState(); + } + function closeWinModal() { if (!isGameWon.value) return; isGameWon.value = false; diff --git a/src/utils/shareUtils.js b/src/utils/shareUtils.js index 7e2a55c..ba23ba4 100644 --- a/src/utils/shareUtils.js +++ b/src/utils/shareUtils.js @@ -1,7 +1,7 @@ import { calculateDifficulty } from '@/utils/puzzleUtils'; export function buildShareCanvas(data, t, formattedTime) { - const { grid, size, currentDensity, guideUsageCount } = data; + const { grid, size, currentDensity, guideUsageCount, hasUsedBoost } = data; if (!grid || !grid.length) return null; const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : ''; @@ -11,7 +11,7 @@ export function buildShareCanvas(data, t, formattedTime) { const padding = 28; const headerHeight = 64; const footerHeight = 28; - const infoHeight = 40; // New space for difficulty/guide info + const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40; const width = boardSize + padding * 2; const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight; const scale = window.devicePixelRatio || 1; @@ -97,7 +97,12 @@ export function buildShareCanvas(data, t, formattedTime) { } } - // Guide Usage Info (Dirty Flag) + // Guide Usage & Boost Info + let infoY = height - padding - footerHeight + 10; + if (guideUsageCount > 0 && hasUsedBoost) { + infoY -= 25; + } + if (guideUsageCount > 0) { ctx.fillStyle = '#ff4d4d'; ctx.font = '600 14px "Segoe UI", sans-serif'; @@ -106,7 +111,15 @@ export function buildShareCanvas(data, t, formattedTime) { const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100)); const guideText = t('win.usedGuide', { count: guideUsageCount, percent }); - ctx.fillText(`⚠️ ${guideText}`, padding, height - padding - footerHeight + 10); + ctx.fillText(`⚠️ ${guideText}`, padding, infoY); + if (hasUsedBoost) infoY += 25; + } + + if (hasUsedBoost) { + ctx.fillStyle = '#ffd700'; + ctx.font = '600 14px "Segoe UI", sans-serif'; + const boostText = t('win.boosted'); + ctx.fillText(`⚡ ${boostText}`, padding, infoY); } ctx.fillStyle = 'rgba(255, 255, 255, 0.75)'; @@ -116,7 +129,7 @@ export function buildShareCanvas(data, t, formattedTime) { } export function buildShareSVG(data, t, formattedTime) { - const { grid, size, currentDensity, guideUsageCount } = data; + const { grid, size, currentDensity, guideUsageCount, hasUsedBoost } = data; if (!grid || !grid.length) return null; const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : ''; @@ -126,7 +139,7 @@ export function buildShareSVG(data, t, formattedTime) { const padding = 28; const headerHeight = 64; const footerHeight = 28; - const infoHeight = 40; + const infoHeight = (guideUsageCount > 0 && hasUsedBoost) ? 65 : 40; const width = boardSize + padding * 2; const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight; @@ -218,12 +231,23 @@ export function buildShareSVG(data, t, formattedTime) { } svgContent += cells; - // Guide Usage + // Guide Usage & Boost Info + let infoY = height - padding - footerHeight + 10; + if (guideUsageCount > 0 && hasUsedBoost) { + infoY -= 25; + } + if (guideUsageCount > 0) { const totalCells = size * size; const percent = Math.min(100, Math.round((guideUsageCount / totalCells) * 100)); const guideText = t('win.usedGuide', { count: guideUsageCount, percent }); - svgContent += `⚠️ ${guideText}`; + svgContent += `⚠️ ${guideText}`; + if (hasUsedBoost) infoY += 25; + } + + if (hasUsedBoost) { + const boostText = t('win.boosted'); + svgContent += `⚡ ${boostText}`; } // URL diff --git a/src/workers/solverWorker.js b/src/workers/solverWorker.js index e3e2362..92b079f 100644 --- a/src/workers/solverWorker.js +++ b/src/workers/solverWorker.js @@ -7,6 +7,7 @@ const messages = { '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' @@ -16,6 +17,7 @@ const messages = { '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' @@ -79,7 +81,12 @@ const isSolved = (grid, solution) => { 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; @@ -131,9 +138,36 @@ const handleStep = (playerGrid, solution, locale) => { return { type: 'stuck', statusText: t(locale, 'worker.stuck') }; }; -self.onmessage = (event) => { - const { id, playerGrid, solution, locale } = event.data; - const resolved = resolveLocale(locale); - const result = handleStep(playerGrid, solution, resolved); - self.postMessage({ id, ...result }); +const handleBoost = (playerGrid, solution, locale) => { + const size = solution.length; + // Find first unknown cell and reveal it + 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 }); + } };