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 });
+ }
};