8 Commits

Author SHA1 Message Date
ddf6a31f55 1.15.6
All checks were successful
Deploy to Production / deploy (push) Successful in 24s
2026-02-21 22:39:54 +00:00
62f35ec35e Fix camera UI gradient gap at bottom 2026-02-21 22:39:28 +00:00
cdaa10c282 Consolidate assets and remove duplicate splash image 2026-02-21 22:38:06 +00:00
56bff9fc59 1.15.5
All checks were successful
Deploy to Production / deploy (push) Successful in 24s
2026-02-21 21:52:11 +00:00
ff520b53f7 Stop tracking dev-dist folder 2026-02-21 21:49:48 +00:00
c5e09a51e8 Update .gitignore 2026-02-21 20:46:05 +00:00
222b8a5f4c Fix reset behavior for custom image nonograms
All checks were successful
Deploy to Production / deploy (push) Successful in 8s
2026-02-21 20:41:33 +00:00
cf84dfd7f2 Refactor solver worker to return translation keys and centralize i18n 2026-02-21 20:41:19 +00:00
13 changed files with 266 additions and 3748 deletions

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ node_modules
.DS_Store .DS_Store
dist/ dist/
dist-ssr/ dist-ssr/
dev-dist/
*.local *.local
.npmrc .npmrc

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,92 +0,0 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict';
self.addEventListener('message', event => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
/**
* The precacheAndRoute() method efficiently caches and responds to
* requests for URLs in the manifest.
* See https://goo.gl/S9QRab
*/
workbox.precacheAndRoute([{
"url": "index.html",
"revision": "0.b79gmi6tt88"
}], {});
workbox.cleanupOutdatedCaches();
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
allowlist: [/^\/$/]
}));
}));

File diff suppressed because it is too large Load Diff

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "vue-nonograms-solid", "name": "vue-nonograms-solid",
"version": "1.15.4", "version": "1.15.6",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "vue-nonograms-solid", "name": "vue-nonograms-solid",
"version": "1.15.4", "version": "1.15.6",
"dependencies": { "dependencies": {
"@capacitor/android": "^8.1.0", "@capacitor/android": "^8.1.0",
"@capacitor/cli": "^8.1.0", "@capacitor/cli": "^8.1.0",

View File

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

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -657,13 +657,14 @@ onUnmounted(() => {
.camera-controls { .camera-controls {
position: absolute; position: absolute;
bottom: 20px; bottom: 0;
left: 0; left: 0;
width: 100%; width: 100%;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
padding-bottom: 40px;
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent); background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
} }

View File

@@ -199,7 +199,15 @@ const messages = {
'simulation.table.solved': 'Rozwiązano (Logika)', 'simulation.table.solved': 'Rozwiązano (Logika)',
'simulation.empty': 'Naciśnij Start, aby uruchomić symulację Monte Carlo', 'simulation.empty': 'Naciśnij Start, aby uruchomić symulację Monte Carlo',
'difficultyMap.size': 'Rozmiar', 'difficultyMap.size': 'Rozmiar',
'difficultyMap.density': 'Gęstość' 'difficultyMap.density': 'Gęstość',
'worker.solved': 'Rozwiązane!',
'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'
}, },
en: { en: {
'app.title': 'Nonograms', 'app.title': 'Nonograms',
@@ -399,7 +407,15 @@ const messages = {
'custom.hideMap': 'Hide difficulty map', 'custom.hideMap': 'Hide difficulty map',
'custom.showMap': 'Show difficulty map', 'custom.showMap': 'Show difficulty map',
'difficultyMap.size': 'Size', 'difficultyMap.size': 'Size',
'difficultyMap.density': 'Density' 'difficultyMap.density': 'Density',
'worker.solved': 'Solved!',
'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'
}, },
zh: { zh: {
'app.title': 'Nonograms', 'app.title': 'Nonograms',

View File

@@ -97,10 +97,20 @@ export function useSolver() {
function ensureWorker() { function ensureWorker() {
if (worker) return; if (worker) return;
worker = new Worker(new URL('../workers/solverWorker.js', import.meta.url), { type: 'module' }); worker = new Worker(new URL('../workers/solver.worker.js', import.meta.url), { type: 'module' });
worker.onmessage = (event) => { worker.onmessage = (event) => {
const { type, r, c, state, statusText: text } = event.data; const { type, r, c, state, status } = event.data;
if (text) statusText.value = text; if (status) {
const params = status.params ? { ...status.params } : {};
if (params.stateKey) {
params.state = t(params.stateKey);
}
statusText.value = t(status.key, params);
} else if (event.data.statusText) {
// Fallback for legacy messages if any
statusText.value = event.data.statusText;
}
if (type === 'move') { if (type === 'move') {
store.setCell(r, c, state); store.setCell(r, c, state);
isProcessing.value = false; isProcessing.value = false;

View File

@@ -376,7 +376,7 @@ export const usePuzzleStore = defineStore('puzzle', () => {
} }
function resetGame() { function resetGame() {
if (currentLevelId.value === 'custom') { if (currentLevelId.value === 'custom' || currentLevelId.value === 'custom_image') {
resetGrid(); resetGrid();
isGameWon.value = false; isGameWon.value = false;
hasUsedGuide.value = false; hasUsedGuide.value = false;

View File

@@ -1,61 +1,240 @@
import { calculateHints } from '../utils/puzzleUtils'; import { calculateHints } from '../utils/puzzleUtils.js';
import { solvePuzzle } from '../utils/solver'; import { solveLine, solvePuzzle } from '../utils/solver.js';
// --- Logic Helpers ---
const solveLineLogic = (lineState, hints) => {
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
const solverLine = lineState.map(cell => {
if (cell === 0) return -1; // Unknown
if (cell === 1) return 1; // Filled
if (cell === 2) return 0; // Empty/Cross
return -1;
});
// Call robust solver
const resultLine = solveLine(solverLine, hints);
// Check for new info
if (!resultLine) return { index: -1 }; // Contradiction or error
for (let i = 0; i < lineState.length; i++) {
// We only care about cells that are currently 0 (Unknown) in Store
if (lineState[i] === 0) {
if (resultLine[i] === 1) {
return { index: i, state: 1 }; // Suggest Fill
}
if (resultLine[i] === 0) {
return { index: i, state: 2 }; // Suggest Cross
}
}
}
return { index: -1 };
};
const isSolved = (grid, solution) => {
const size = grid.length;
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
const playerCell = grid[r][c];
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;
};
const handleStep = (playerGrid, solution) => {
if (isSolved(playerGrid, solution)) {
return { type: 'done', status: { key: 'worker.solved' } };
}
const size = solution.length;
const { rowHints, colHints } = calculateHints(solution);
for (let r = 0; r < size; r++) {
const rowLine = playerGrid[r];
const hints = rowHints[r];
const result = solveLineLogic(rowLine, hints);
if (result.index !== -1) {
const stateKey = result.state === 1 ? 'worker.state.filled' : 'worker.state.empty';
return {
type: 'move',
r,
c: result.index,
state: result.state,
status: {
key: 'worker.logicRow',
params: { row: r + 1, col: result.index + 1, stateKey }
}
};
}
}
for (let c = 0; c < size; c++) {
const colLine = [];
for (let r = 0; r < size; r++) colLine.push(playerGrid[r][c]);
const hints = colHints[c];
const result = solveLineLogic(colLine, hints);
if (result.index !== -1) {
const stateKey = result.state === 1 ? 'worker.state.filled' : 'worker.state.empty';
return {
type: 'move',
r: result.index,
c,
state: result.state,
status: {
key: 'worker.logicCol',
params: { row: result.index + 1, col: c + 1, stateKey }
}
};
}
}
return { type: 'stuck', status: { key: 'worker.stuck' } };
};
const handleBoost = (playerGrid, solution) => {
const size = solution.length;
// 1. Try to use the Solver (DFS) to find a logical move
try {
const { rowHints, colHints } = calculateHints(solution);
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
const solverGrid = playerGrid.map(row => row.map(cell => {
if (cell === 0) return -1;
if (cell === 1) return 1;
if (cell === 2) return 0;
return -1;
}));
// Run full solver (logicOnly=false allows DFS/guessing)
const result = solvePuzzle(rowHints, colHints, null, solverGrid, false);
if (result && result.solution) {
const solvedGrid = result.solution;
// Find the first cell that is Unknown in playerGrid but Known in solvedGrid
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (playerGrid[r][c] === 0) { // Unknown in Player
const solvedVal = solvedGrid[r][c]; // -1=Unk, 0=Empty, 1=Filled
if (solvedVal !== -1) {
// Found a logical deduction!
const newState = solvedVal === 1 ? 1 : 2; // 1->Filled, 0->Cross
const stateKey = newState === 1 ? 'worker.state.filled' : 'worker.state.empty';
return {
type: 'move',
r,
c,
state: newState,
status: {
key: 'worker.boosted',
params: { row: r + 1, col: c + 1, stateKey }
}
};
}
}
}
}
}
} catch (e) {
console.warn('Boost Solver failed, falling back to simple reveal:', e);
}
// 2. Fallback: Cheat
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 stateKey = correctState === 1 ? 'worker.state.filled' : 'worker.state.empty';
return {
type: 'move',
r,
c,
state: correctState,
status: {
key: 'worker.boosted',
params: { row: r + 1, col: c + 1, stateKey }
}
};
}
}
}
return { type: 'done', status: { key: 'worker.solved' } };
};
// --- Main Worker Handler ---
self.onmessage = (e) => { self.onmessage = (e) => {
const { id, grid, initialGrid } = e.data; const { id, grid, initialGrid, playerGrid, solution, action } = e.data;
try { try {
if (!grid || grid.length === 0) { // Mode 1: Analysis (Batch) - from ImageImportModal / workerPool
self.postMessage({ id, error: 'Empty grid' }); if (grid) {
if (grid.length === 0) {
self.postMessage({ id, error: 'Empty grid' });
return;
}
const rows = grid.length;
const cols = grid[0].length;
const { rowHints, colHints } = calculateHints(grid);
const onProgress = (percent) => {
self.postMessage({
id,
type: 'progress',
percent
});
};
const { percentSolved, difficultyScore } = solvePuzzle(rowHints, colHints, onProgress, initialGrid);
let value = difficultyScore;
let level;
if (percentSolved < 100) {
level = 'extreme';
} else {
if (value < 25) level = 'easy';
else if (value < 50) level = 'medium';
else if (value < 75) level = 'hard';
else level = 'extreme';
}
self.postMessage({
id,
solvability: Math.floor(percentSolved),
difficulty: Math.round(value),
difficultyLabel: level,
rows,
cols
});
return; return;
} }
const rows = grid.length; // Mode 2: Assistant (Step/Boost) - from useSolver
const cols = grid[0].length; if (playerGrid) {
// Use initialGrid if provided, otherwise assume we are starting fresh let result;
// BUT wait, 'grid' passed here is usually the 0/1 grid from Image Import (target pattern). if (action === 'boost') {
// 'initialGrid' would be the partial solution state (-1/0/1). result = handleBoost(playerGrid, solution);
} else {
result = handleStep(playerGrid, solution);
}
// 1. Calculate Hints from the TARGET grid (the image) self.postMessage({ id, ...result });
const { rowHints, colHints } = calculateHints(grid); return;
// 2. Run Solver (Logic + Lookahead)
const onProgress = (percent) => {
self.postMessage({
id,
type: 'progress',
percent
});
};
const { percentSolved, difficultyScore, lookaheadUsed } = solvePuzzle(rowHints, colHints, onProgress, initialGrid);
// 3. Determine Level
let value = difficultyScore;
let level;
if (percentSolved < 100) {
level = 'extreme'; // Unsolvable by logic+lookahead
} else {
if (value < 25) level = 'easy';
else if (value < 50) level = 'medium';
else if (value < 75) level = 'hard';
else level = 'extreme';
} }
// Add specific note if lookahead was needed?
// UI doesn't have a field for that, but we can encode it in difficultyLabel if needed.
// For now, standard levels are fine.
self.postMessage({
id,
solvability: Math.floor(percentSolved),
difficulty: Math.round(value),
difficultyLabel: level,
rows,
cols
});
} catch (err) { } catch (err) {
self.postMessage({ id, error: err.message }); self.postMessage({ id, error: err.message });
} }

View File

@@ -1,220 +0,0 @@
import { calculateHints } from '../utils/puzzleUtils.js';
import { solveLine, solvePuzzle } from '../utils/solver.js';
const messages = {
pl: {
'worker.solved': 'Rozwiązane!',
'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'
},
en: {
'worker.solved': 'Solved!',
'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'
}
};
const resolveLocale = (value) => {
if (!value) return 'en';
const short = String(value).toLowerCase().split('-')[0];
return short === 'pl' ? 'pl' : 'en';
};
const format = (text, params = {}) => {
return text.replace(/\{(\w+)\}/g, (_, key) => {
const value = params[key];
return value === undefined ? `{${key}}` : String(value);
});
};
const t = (locale, key, params) => {
const lang = messages[locale] || messages.en;
const value = lang[key] || messages.en[key] || key;
return typeof value === 'string' ? format(value, params) : key;
};
const solveLineLogic = (lineState, hints) => {
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
const solverLine = lineState.map(cell => {
if (cell === 0) return -1; // Unknown
if (cell === 1) return 1; // Filled
if (cell === 2) return 0; // Empty/Cross
return -1;
});
// Call robust solver
const resultLine = solveLine(solverLine, hints);
// Check for new info
if (!resultLine) return { index: -1 }; // Contradiction or error
for (let i = 0; i < lineState.length; i++) {
// We only care about cells that are currently 0 (Unknown) in Store
if (lineState[i] === 0) {
if (resultLine[i] === 1) {
return { index: i, state: 1 }; // Suggest Fill
}
if (resultLine[i] === 0) {
return { index: i, state: 2 }; // Suggest Cross
}
}
}
return { index: -1 };
};
const isSolved = (grid, solution) => {
const size = grid.length;
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
const playerCell = grid[r][c];
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;
};
const handleStep = (playerGrid, solution, locale) => {
if (isSolved(playerGrid, solution)) {
return { type: 'done', statusText: t(locale, 'worker.solved') };
}
const size = solution.length;
const { rowHints, colHints } = calculateHints(solution);
for (let r = 0; r < size; r++) {
const rowLine = playerGrid[r];
const hints = rowHints[r];
const result = solveLineLogic(rowLine, hints);
if (result.index !== -1) {
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
return {
type: 'move',
r,
c: result.index,
state: result.state,
statusText: t(locale, 'worker.logicRow', { row: r + 1, col: result.index + 1, state: stateLabel })
};
}
}
for (let c = 0; c < size; c++) {
const colLine = [];
for (let r = 0; r < size; r++) colLine.push(playerGrid[r][c]);
const hints = colHints[c];
const result = solveLineLogic(colLine, hints);
if (result.index !== -1) {
const stateLabel = t(locale, result.state === 1 ? 'worker.state.filled' : 'worker.state.empty');
return {
type: 'move',
r: result.index,
c,
state: result.state,
statusText: t(locale, 'worker.logicCol', { row: result.index + 1, col: c + 1, state: stateLabel })
};
}
}
// Check for guess logic - we want to avoid this unless strictly necessary
// If no logic move found, return 'stuck' instead of cheating
return { type: 'stuck', statusText: t(locale, 'worker.stuck') };
};
const handleBoost = (playerGrid, solution, locale) => {
const size = solution.length;
// 1. Try to use the Solver (DFS) to find a logical move
try {
const { rowHints, colHints } = calculateHints(solution);
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
const solverGrid = playerGrid.map(row => row.map(cell => {
if (cell === 0) return -1;
if (cell === 1) return 1;
if (cell === 2) return 0;
return -1;
}));
// Run full solver (logicOnly=false allows DFS/guessing)
// We pass solverGrid as initial state to respect user's moves
const result = solvePuzzle(rowHints, colHints, null, solverGrid, false);
if (result && result.solution) {
const solvedGrid = result.solution;
// Find the first cell that is Unknown in playerGrid but Known in solvedGrid
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
if (playerGrid[r][c] === 0) { // Unknown in Player
const solvedVal = solvedGrid[r][c]; // -1=Unk, 0=Empty, 1=Filled
if (solvedVal !== -1) {
// Found a logical deduction!
const newState = solvedVal === 1 ? 1 : 2; // 1->Filled, 0->Cross
const stateLabel = t(locale, newState === 1 ? 'worker.state.filled' : 'worker.state.empty');
return {
type: 'move',
r,
c,
state: newState,
statusText: t(locale, 'worker.boosted', { row: r + 1, col: c + 1, state: stateLabel })
};
}
}
}
}
}
} catch (e) {
console.warn('Boost Solver failed, falling back to simple reveal:', e);
}
// 2. Fallback: If solver failed (e.g. contradiction due to user error),
// or no new info found, use the "Cheat" method (reveal from true solution).
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 });
}
};