Compare commits
19 Commits
v1.15.4
...
5aaa8c7357
| Author | SHA1 | Date | |
|---|---|---|---|
|
5aaa8c7357
|
|||
|
bf7cc4cb1d
|
|||
|
1aff1a0857
|
|||
|
a59aeb93e6
|
|||
|
8291c8f74c
|
|||
|
0f240596cc
|
|||
|
6fc3e1c057
|
|||
|
d488531358
|
|||
|
e3d2b630e1
|
|||
|
1392a9f3d5
|
|||
|
8ccf5de972
|
|||
|
ddf6a31f55
|
|||
|
62f35ec35e
|
|||
|
cdaa10c282
|
|||
|
56bff9fc59
|
|||
|
ff520b53f7
|
|||
|
c5e09a51e8
|
|||
|
222b8a5f4c
|
|||
|
cf84dfd7f2
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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 |
@@ -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} didn’t 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
@@ -1,17 +1,17 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
nonograms:
|
piggy-bank:
|
||||||
container_name: ${CONTAINER_NAME:-nonograms}
|
container_name: nonograms-app
|
||||||
build:
|
build: .
|
||||||
context: .
|
restart: unless-stopped
|
||||||
dockerfile: Dockerfile
|
# ports:
|
||||||
|
# - "8081:80"
|
||||||
expose:
|
expose:
|
||||||
- "80"
|
- "80"
|
||||||
restart: unless-stopped
|
|
||||||
networks:
|
networks:
|
||||||
- npm_public
|
- npm_public
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
npm_public:
|
npm_public:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.15.4",
|
"version": "1.15.12",
|
||||||
"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.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^8.1.0",
|
"@capacitor/android": "^8.1.0",
|
||||||
"@capacitor/cli": "^8.1.0",
|
"@capacitor/cli": "^8.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.15.4",
|
"version": "1.15.12",
|
||||||
"homepage": "https://nonograms.7u.pl/",
|
"homepage": "https://nonograms.7u.pl/",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,14 @@ const handleKeyDown = (e) => {
|
|||||||
|
|
||||||
const triggerVibration = () => {
|
const triggerVibration = () => {
|
||||||
if (!('vibrate' in navigator)) return;
|
if (!('vibrate' in navigator)) return;
|
||||||
const isCoarse = window.matchMedia?.('(pointer: coarse)')?.matches;
|
try {
|
||||||
const isTouch = navigator.maxTouchPoints && navigator.maxTouchPoints > 0;
|
const isCoarse = window.matchMedia?.('(pointer: coarse)')?.matches;
|
||||||
if (isCoarse || isTouch) {
|
const isTouch = navigator.maxTouchPoints && navigator.maxTouchPoints > 0;
|
||||||
navigator.vibrate([80, 40, 120, 40, 180]);
|
if (isCoarse || isTouch) {
|
||||||
|
navigator.vibrate([200, 100, 200, 100, 200]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Vibration failed:', e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ export function buildShareCanvas(data, t, formattedTime) {
|
|||||||
bg.addColorStop(1, '#0a1324');
|
bg.addColorStop(1, '#0a1324');
|
||||||
ctx.fillStyle = bg;
|
ctx.fillStyle = bg;
|
||||||
ctx.fillRect(0, 0, width, height);
|
ctx.fillRect(0, 0, width, height);
|
||||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.35)';
|
|
||||||
ctx.fillRect(12, 12, width - 24, height - 24);
|
|
||||||
|
|
||||||
ctx.fillStyle = '#e8fbff';
|
ctx.fillStyle = '#e8fbff';
|
||||||
ctx.font = '700 26px "Segoe UI", sans-serif';
|
ctx.font = '700 26px "Segoe UI", sans-serif';
|
||||||
@@ -204,7 +202,6 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="100%" height="100%" fill="url(#bg)"/>
|
<rect width="100%" height="100%" fill="url(#bg)"/>
|
||||||
<rect x="12" y="12" width="${width - 24}" height="${height - 24}" fill="${overlayColor}"/>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Text: Title & Time
|
// Text: Title & Time
|
||||||
@@ -253,8 +250,12 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
} else if (state === 2) { // Cross
|
} else if (state === 2) { // Cross
|
||||||
const d = cellSize * 0.6;
|
const d = cellSize * 0.6;
|
||||||
const off = cellSize * 0.2;
|
const off = cellSize * 0.2;
|
||||||
|
const x1 = (cx + off).toFixed(1);
|
||||||
|
const y1 = (cy + off).toFixed(1);
|
||||||
|
const x2 = (cx + off + d).toFixed(1);
|
||||||
|
const y2 = (cy + off + d).toFixed(1);
|
||||||
cells += `
|
cells += `
|
||||||
<path d="M${cx + off} ${cy + off} L${cx + off + d} ${cy + off + d} M${cx + off + d} ${cy + off} L${cx + off} ${cy + off + d}"
|
<path d="M${x1} ${y1} L${x2} ${y2} M${x2} ${y1} L${x1} ${y2}"
|
||||||
stroke="${crossColor}" stroke-width="${lineWidth}" stroke-linecap="round"/>
|
stroke="${crossColor}" stroke-width="${lineWidth}" stroke-linecap="round"/>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user