31 Commits

Author SHA1 Message Date
ef4b2653a8 1.15.17
All checks were successful
Build and Deploy / deploy (push) Successful in 42s
2026-02-26 21:47:22 +00:00
3237fcfa7c Add rich social media metadata (Open Graph, Twitter Cards) 2026-02-26 21:47:14 +00:00
fcef986d2b Update docker-compose name and service aliases, adjust deploy workflow
All checks were successful
Build and Deploy / deploy (push) Successful in 7s
2026-02-22 19:46:03 +00:00
4c6f4d5865 Restore workflow trigger and name
Some checks failed
Build and Deploy / deploy (push) Failing after 1s
2026-02-22 16:40:35 +00:00
3052fed279 Update deploy workflow: set working directory and simplify 2026-02-22 16:33:13 +00:00
fa983c6777 1.15.16
All checks were successful
Deploy to Production / deploy (push) Successful in 19s
2026-02-22 16:20:56 +00:00
2a1ddf92aa Remove direct host port binding (use NPM) 2026-02-22 16:20:17 +00:00
a5b93c1198 1.15.15
All checks were successful
Deploy to Production / deploy (push) Successful in 17s
2026-02-22 15:45:08 +00:00
be014fc3c8 Fix nginx config: remove volume mount, add IPv6 listen, catch-all server_name 2026-02-22 15:45:08 +00:00
269db48aec 1.15.14
Some checks failed
Deploy to Production / deploy (push) Failing after 17s
2026-02-22 15:41:43 +00:00
fe72508717 Restore correct docker-compose.yml (remove piggy-bank, fix service name) 2026-02-22 15:41:42 +00:00
70ba5ebdb3 1.15.13
All checks were successful
Deploy to Production / deploy (push) Successful in 17s
2026-02-22 15:37:51 +00:00
5aaa8c7357 1.15.12
All checks were successful
Deploy to Production / deploy (push) Successful in 17s
2026-02-22 15:37:00 +00:00
bf7cc4cb1d Add expose 80 to docker-compose 2026-02-22 15:37:00 +00:00
1aff1a0857 1.15.11
Some checks failed
Deploy to Production / deploy (push) Failing after 18s
2026-02-22 15:31:51 +00:00
a59aeb93e6 Restore npm_public network in docker-compose.yml 2026-02-22 15:31:51 +00:00
8291c8f74c 1.15.10
Some checks failed
Deploy to Production / deploy (push) Failing after 24s
2026-02-22 15:28:20 +00:00
0f240596cc 1.15.9
All checks were successful
Deploy to Production / deploy (push) Successful in 24s
2026-02-21 23:24:44 +00:00
6fc3e1c057 Enhance vibration pattern on game win with error handling 2026-02-21 23:24:40 +00:00
d488531358 1.15.8
All checks were successful
Deploy to Production / deploy (push) Successful in 24s
2026-02-21 23:13:07 +00:00
e3d2b630e1 Remove overlay rectangle from share export 2026-02-21 23:13:06 +00:00
1392a9f3d5 1.15.7
All checks were successful
Deploy to Production / deploy (push) Successful in 25s
2026-02-21 23:04:08 +00:00
8ccf5de972 Fix SVG path precision in share export 2026-02-21 23:04:04 +00:00
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
19 changed files with 311 additions and 3772 deletions

View File

@@ -1,6 +1,4 @@
name: Deploy to Production
run-name: Deploy to Production by @${{ github.actor }}
name: Build and Deploy
on:
push:
branches:
@@ -10,14 +8,11 @@ jobs:
deploy:
runs-on: self-hosted
steps:
- name: Check out repository code
uses: actions/checkout@v4
- uses: actions/checkout@v4
- name: Build and deploy with Docker Compose
- name: Deploy nonograms-app
run: |
# Próba zatrzymania i usunięcia starego kontenera (ignoruje błąd jeśli nie istnieje)
docker compose down --remove-orphans || true
docker rm -f nonograms-app || true
# Start nowej wersji
docker compose up -d --build

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@ node_modules
.DS_Store
dist/
dist-ssr/
dev-dist/
*.local
.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

View File

@@ -1,16 +1,22 @@
version: '3.8'
name: nonograms-app
services:
nonograms:
container_name: ${CONTAINER_NAME:-nonograms}
nonograms-app:
container_name: nonograms-app
build:
context: .
dockerfile: Dockerfile
# ports:
# - "8083:80"
expose:
- "80"
restart: unless-stopped
# volumes:
# - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- npm_public
npm_public:
aliases:
- nonograms-app
networks:
npm_public:

View File

@@ -6,7 +6,21 @@
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="mask-icon" href="/nonograms.svg" color="#00f2fe" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Nonograms Pro - Vue 3 SOLID</title>
<title>Nonograms Pro - Logic Puzzle Game</title>
<meta name="description" content="Interactive Nonogram logic puzzles with a modern interface. Solve puzzles, use the built-in solver, and create your own custom games.">
<!-- Open Graph (for social media sharing like Facebook/Discord) -->
<meta property="og:title" content="Nonograms Pro - Logic Puzzle Game">
<meta property="og:description" content="Interactive Nonogram logic puzzles with a modern interface. Solve puzzles, use the built-in solver, and create your own custom games.">
<meta property="og:image" content="/screenshot.png">
<meta property="og:type" content="website">
<!-- Twitter Cards -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Nonograms Pro - Logic Puzzle Game">
<meta name="twitter:description" content="Interactive Nonogram logic puzzles with a modern interface. Solve puzzles, use the built-in solver, and create your own custom games.">
<meta name="twitter:image" content="/screenshot.png">
</head>
<body>
<div id="app"></div>

View File

@@ -1,6 +1,7 @@
server {
listen 80;
server_name localhost;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;

4
package-lock.json generated
View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

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

View File

@@ -36,10 +36,14 @@ const handleKeyDown = (e) => {
const triggerVibration = () => {
if (!('vibrate' in navigator)) return;
const isCoarse = window.matchMedia?.('(pointer: coarse)')?.matches;
const isTouch = navigator.maxTouchPoints && navigator.maxTouchPoints > 0;
if (isCoarse || isTouch) {
navigator.vibrate([80, 40, 120, 40, 180]);
try {
const isCoarse = window.matchMedia?.('(pointer: coarse)')?.matches;
const isTouch = navigator.maxTouchPoints && navigator.maxTouchPoints > 0;
if (isCoarse || isTouch) {
navigator.vibrate([200, 100, 200, 100, 200]);
}
} catch (e) {
console.error('Vibration failed:', e);
}
};

View File

@@ -199,7 +199,15 @@ const messages = {
'simulation.table.solved': 'Rozwiązano (Logika)',
'simulation.empty': 'Naciśnij Start, aby uruchomić symulację Monte Carlo',
'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: {
'app.title': 'Nonograms',
@@ -399,7 +407,15 @@ const messages = {
'custom.hideMap': 'Hide difficulty map',
'custom.showMap': 'Show difficulty map',
'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: {
'app.title': 'Nonograms',

View File

@@ -97,10 +97,20 @@ export function useSolver() {
function ensureWorker() {
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) => {
const { type, r, c, state, statusText: text } = event.data;
if (text) statusText.value = text;
const { type, r, c, state, status } = event.data;
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') {
store.setCell(r, c, state);
isProcessing.value = false;

View File

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

View File

@@ -38,8 +38,6 @@ export function buildShareCanvas(data, t, formattedTime) {
bg.addColorStop(1, '#0a1324');
ctx.fillStyle = bg;
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.font = '700 26px "Segoe UI", sans-serif';
@@ -204,7 +202,6 @@ export function buildShareSVG(data, t, formattedTime) {
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#bg)"/>
<rect x="12" y="12" width="${width - 24}" height="${height - 24}" fill="${overlayColor}"/>
`;
// Text: Title & Time
@@ -253,8 +250,12 @@ export function buildShareSVG(data, t, formattedTime) {
} else if (state === 2) { // Cross
const d = cellSize * 0.6;
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 += `
<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"/>
`;
}

View File

@@ -1,60 +1,239 @@
import { calculateHints } from '../utils/puzzleUtils';
import { solvePuzzle } from '../utils/solver';
import { calculateHints } from '../utils/puzzleUtils.js';
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) => {
const { id, grid, initialGrid } = e.data;
const { id, grid, initialGrid, playerGrid, solution, action } = e.data;
try {
if (!grid || grid.length === 0) {
self.postMessage({ id, error: 'Empty grid' });
// Mode 1: Analysis (Batch) - from ImageImportModal / workerPool
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;
}
const rows = grid.length;
const cols = grid[0].length;
// Use initialGrid if provided, otherwise assume we are starting fresh
// BUT wait, 'grid' passed here is usually the 0/1 grid from Image Import (target pattern).
// 'initialGrid' would be the partial solution state (-1/0/1).
// 1. Calculate Hints from the TARGET grid (the image)
const { rowHints, colHints } = calculateHints(grid);
// 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';
// Mode 2: Assistant (Step/Boost) - from useSolver
if (playerGrid) {
let result;
if (action === 'boost') {
result = handleBoost(playerGrid, solution);
} else {
result = handleStep(playerGrid, solution);
}
self.postMessage({ id, ...result });
return;
}
// 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) {
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 });
}
};