2 Commits

Author SHA1 Message Date
18568fa4ae 1.14.2
All checks were successful
Deploy to Production / deploy (push) Successful in 16s
2026-02-13 07:03:18 +01:00
c3188bb740 fix: prevent premature solved status, add DFS boost button, mark boosted exports 2026-02-13 07:03:08 +01:00
9 changed files with 148 additions and 19 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "vue-nonograms-solid",
"version": "1.14.1",
"version": "1.14.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "vue-nonograms-solid",
"version": "1.14.1",
"version": "1.14.2",
"dependencies": {
"fireworks-js": "^2.10.8",
"flag-icons": "^7.5.0",

View File

@@ -1,6 +1,6 @@
{
"name": "vue-nonograms-solid",
"version": "1.14.1",
"version": "1.14.2",
"homepage": "https://nonograms.7u.pl/",
"type": "module",
"scripts": {

View File

@@ -4,11 +4,13 @@ import { useI18n } from '@/composables/useI18n';
const {
isPlaying,
isStuck,
speedLabel,
statusText,
step,
togglePlay,
changeSpeed
changeSpeed,
boost
} = useSolver();
const { t } = useI18n();
</script>
@@ -29,6 +31,10 @@ const { t } = useI18n();
<button class="btn-neon small" @click="changeSpeed">
{{ t('guide.speed') }}: {{ speedLabel }}
</button>
<button v-if="isStuck" class="btn-neon small boost-btn" @click="boost">
Boost (DFS)
</button>
</div>
</div>
</template>
@@ -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);
}
</style>

View File

@@ -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 = () => {

View File

@@ -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',

View File

@@ -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
};
}

View File

@@ -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;

View File

@@ -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 += `<text x="${padding}" y="${height - padding - footerHeight + 10}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ff4d4d">⚠️ ${guideText}</text>`;
svgContent += `<text x="${padding}" y="${infoY}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ff4d4d">⚠️ ${guideText}</text>`;
if (hasUsedBoost) infoY += 25;
}
if (hasUsedBoost) {
const boostText = t('win.boosted');
svgContent += `<text x="${padding}" y="${infoY}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ffd700">⚡ ${boostText}</text>`;
}
// URL

View File

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