Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
43c0290fac
|
|||
|
fa5fa12157
|
|||
|
29682c9a06
|
|||
|
2d30315ae6
|
|||
|
2261f44b4a
|
|||
| 2cd32d4a3e | |||
|
48def6c400
|
|||
|
f1f3f81466
|
|||
|
121e4c418f
|
|||
|
5583a08b9a
|
|||
|
fed32c6cbe
|
|||
|
99052a2b6c
|
|||
| f483d39a2c | |||
| 29952b22e7 | |||
| c42210ac24 | |||
| 0799cb2162 | |||
| 2cd2291d03 | |||
|
ec20d5ee8f
|
|||
|
b20a829d37
|
|||
|
988c4a899b
|
|||
|
d8faa308e6
|
|||
|
6bddb24bfe
|
|||
|
1c2be3567a
|
|||
|
|
d62cec415b | ||
|
8be28a4472
|
|||
|
48778b3e8a
|
|||
|
8bd5d5c3e6
|
|||
|
cf37ccd843
|
|||
| 9a65dfe55d | |||
| 2a88362d00 | |||
| 8e3ae3e7d6 | |||
| 51bbe0cb52 | |||
| 08292039cf | |||
| 934b2a0483 | |||
| 27270d6452 | |||
|
a4681b5b97
|
|||
|
8327597e2e
|
|||
|
bae67fc1ec
|
|||
|
47426d529a
|
|||
|
71edc3103d
|
|||
| 30c1faeae4 | |||
|
b90809fca1
|
|||
|
322182245c
|
|||
| 324b761d37 | |||
| 4dab0e2c63 | |||
| b3e08b53fc | |||
| 3ce15ed794 | |||
| bd310d8305 | |||
| a22897e19e | |||
| 6ec3e66e9c | |||
| defde986a4 | |||
| bb3752b8ae | |||
| be04f333b0 |
23
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Deploy to Production
|
||||||
|
run-name: Deploy to Production by @${{ github.actor }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build and deploy with Docker Compose
|
||||||
|
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
|
||||||
6
.gitignore
vendored
@@ -1,7 +1,3 @@
|
|||||||
|
.gpg/
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
*.log
|
|
||||||
dev-dist
|
|
||||||
|
|||||||
23
LICENSE
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 gkucmierz
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
See README.md for project description.
|
||||||
12
README.md
@@ -1,3 +1,13 @@
|
|||||||
# Nonograms
|
# Nonograms
|
||||||
|
|
||||||
Link do aplikacji: https://nonograms.7u.pl
|
## Description
|
||||||
|
|
||||||
|
Nonograms is a modern, fast, and accessible logic puzzle game (also known as Picross or Griddlers). Solve pixel-art puzzles by marking cells according to numeric clues for rows and columns. The app features:
|
||||||
|
- Clean UX with keyboard and touch support
|
||||||
|
- Multiple languages and PWA support (installable on desktop and mobile)
|
||||||
|
- Difficulty simulation and guide to learn solving strategies
|
||||||
|
- Shareable puzzles and persistent progress
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Play online at https://nonograms.7u.pl or install as a PWA for an app-like experience.
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
|
||||||
const fileContent = fs.readFileSync('src/composables/useI18n.js', 'utf8');
|
const fileContent = fs.readFileSync('src/composables/useI18n.js', 'utf8');
|
||||||
|
|
||||||
// Extract the messages object
|
|
||||||
const match = fileContent.match(/const messages = ({[\s\S]*?});/);
|
const match = fileContent.match(/const messages = ({[\s\S]*?});/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
console.error('Could not find messages object');
|
console.error('Could not find messages object');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to make the string valid JS to eval it.
|
|
||||||
// It seems the content inside `const messages = { ... };` is valid JS object notation.
|
|
||||||
// But we need to be careful about imports or other things if we were to `eval` the whole file.
|
|
||||||
// We'll just `eval` the object part.
|
|
||||||
|
|
||||||
const messagesStr = match[1];
|
const messagesStr = match[1];
|
||||||
const messages = eval(`(${messagesStr})`);
|
const messages = eval(`(${messagesStr})`);
|
||||||
|
|
||||||
@@ -25,10 +18,8 @@ const missing = {};
|
|||||||
|
|
||||||
languages.forEach(lang => {
|
languages.forEach(lang => {
|
||||||
if (lang === 'en') return;
|
if (lang === 'en') return;
|
||||||
|
|
||||||
const langKeys = Object.keys(messages[lang]);
|
const langKeys = Object.keys(messages[lang]);
|
||||||
const missingKeys = enKeys.filter(k => !langKeys.includes(k));
|
const missingKeys = enKeys.filter(k => !langKeys.includes(k));
|
||||||
|
|
||||||
if (missingKeys.length > 0) {
|
if (missingKeys.length > 0) {
|
||||||
missing[lang] = missingKeys;
|
missing[lang] = missingKeys;
|
||||||
}
|
}
|
||||||
92
dev-dist/sw.js
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* 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.kkc80cp3p5o"
|
||||||
|
}], {});
|
||||||
|
workbox.cleanupOutdatedCaches();
|
||||||
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
allowlist: [/^\/$/]
|
||||||
|
}));
|
||||||
|
|
||||||
|
}));
|
||||||
3377
dev-dist/workbox-7a5e81cd.js
Normal file
@@ -1,938 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 89.4,
|
|
||||||
"minSolved": 36,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.031666799999999905
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 74.2,
|
|
||||||
"minSolved": 8,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.03671869999999924
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 74.2,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.04439559999999983
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 80.8,
|
|
||||||
"minSolved": 8,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.0317166499999999
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 96.8,
|
|
||||||
"minSolved": 68,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.0309604000000002
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 97.6,
|
|
||||||
"minSolved": 84,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.031464499999999875
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.2,
|
|
||||||
"minSolved": 84,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.03086874999999978
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.01615615000000048
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 5,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.02271474999999956
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 56.6,
|
|
||||||
"minSolved": 19,
|
|
||||||
"maxSolved": 86,
|
|
||||||
"avgTime": 0.042958299999999915
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 19.8,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 51,
|
|
||||||
"avgTime": 0.050141749999999874
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 15.75,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 73,
|
|
||||||
"avgTime": 0.06852290000000014
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 54.05,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.12701870000000018
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 91.8,
|
|
||||||
"minSolved": 59,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.16561034999999985
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 99.8,
|
|
||||||
"minSolved": 96,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.07136649999999882
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.8,
|
|
||||||
"minSolved": 96,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.04808134999999893
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.03795824999999979
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 10,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.024952100000000855
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 37.044444444444444,
|
|
||||||
"minSolved": 13.333333333333334,
|
|
||||||
"maxSolved": 61.77777777777778,
|
|
||||||
"avgTime": 0.045045850000000345
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 9.777777777777775,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 26.666666666666668,
|
|
||||||
"avgTime": 0.034581349999998776
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 1.8888888888888886,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 8,
|
|
||||||
"avgTime": 0.029402199999999823
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 11.822222222222223,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 61.33333333333333,
|
|
||||||
"avgTime": 0.07898965000000047
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 68.19999999999999,
|
|
||||||
"minSolved": 2.666666666666667,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.1374602999999997
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 99.55555555555554,
|
|
||||||
"minSolved": 96.44444444444444,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.09379159999999978
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.77777777777779,
|
|
||||||
"minSolved": 97.33333333333334,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.07072704999999928
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.050104250000000405
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 15,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.031362550000000766
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 22.5875,
|
|
||||||
"minSolved": 5,
|
|
||||||
"maxSolved": 41.5,
|
|
||||||
"avgTime": 0.04363335000000035
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 3.25,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 14.499999999999998,
|
|
||||||
"avgTime": 0.03823525000000103
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0.5625,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 5,
|
|
||||||
"avgTime": 0.03880414999999786
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 1.4625,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 3.25,
|
|
||||||
"avgTime": 0.06692695000000129
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 36.75,
|
|
||||||
"minSolved": 1.25,
|
|
||||||
"maxSolved": 99,
|
|
||||||
"avgTime": 0.25872084999999884
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 99.8,
|
|
||||||
"minSolved": 99,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.2258772000000004
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.95,
|
|
||||||
"minSolved": 99,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.13418124999999997
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.09264785000000053
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 20,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.05307699999999756
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 16.000000000000004,
|
|
||||||
"minSolved": 7.84,
|
|
||||||
"maxSolved": 32.800000000000004,
|
|
||||||
"avgTime": 0.05678540000000112
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.04278334999999842
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0.048,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.64,
|
|
||||||
"avgTime": 0.05884794999999983
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0.8880000000000001,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 9.44,
|
|
||||||
"avgTime": 0.11761245000000287
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 19.128000000000007,
|
|
||||||
"minSolved": 1.1199999999999999,
|
|
||||||
"maxSolved": 96.48,
|
|
||||||
"avgTime": 0.3490229000000021
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 99.24799999999998,
|
|
||||||
"minSolved": 94.88,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.49611459999999996
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.904,
|
|
||||||
"minSolved": 99.36,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.23916465000000073
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.14604994999999973
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 25,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 99.96799999999999,
|
|
||||||
"minSolved": 99.36,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.08385419999999896
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 7.988888888888889,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 16,
|
|
||||||
"avgTime": 0.08026245000000073
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0.16666666666666669,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 3.3333333333333335,
|
|
||||||
"avgTime": 0.06999999999999887
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.08285835000000005
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0.3777777777777777,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 4.111111111111112,
|
|
||||||
"avgTime": 0.1756041499999988
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 5.4222222222222225,
|
|
||||||
"minSolved": 0.7777777777777778,
|
|
||||||
"maxSolved": 21.444444444444443,
|
|
||||||
"avgTime": 0.41105620000000015
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 99.41666666666669,
|
|
||||||
"minSolved": 94.77777777777779,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.9417500999999995
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.93333333333335,
|
|
||||||
"minSolved": 99.55555555555556,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.41628955000000334
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.22320620000000133
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 30,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.13331460000000134
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 5.653061224489796,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 13.795918367346937,
|
|
||||||
"avgTime": 0.11177699999999931
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0.14285714285714285,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 2.857142857142857,
|
|
||||||
"avgTime": 0.09598544999999917
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.1290145000000038
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0.1346938775510204,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.5714285714285714,
|
|
||||||
"avgTime": 0.21904799999999797
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 4.424489795918367,
|
|
||||||
"minSolved": 0.40816326530612246,
|
|
||||||
"maxSolved": 23.183673469387756,
|
|
||||||
"avgTime": 0.5596769500000022
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 91.1061224489796,
|
|
||||||
"minSolved": 8.408163265306122,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 1.5827311000000024
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.9673469387755,
|
|
||||||
"minSolved": 99.67346938775509,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.5970167499999988
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.33084175000000327
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 35,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.1685022000000032
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 2.734375,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 9.8125,
|
|
||||||
"avgTime": 0.13156869999999826
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.13052910000000112
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.17675199999999905
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0.03125,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.3125,
|
|
||||||
"avgTime": 0.26616039999999686
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 2.14375,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 9.5625,
|
|
||||||
"avgTime": 0.694316649999999
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 91.44375,
|
|
||||||
"minSolved": 22.3125,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 2.9244042000000006
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.9875,
|
|
||||||
"minSolved": 99.75,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.8381519999999967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.4339062999999925
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 40,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.2375938000000076
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 1.7827160493827159,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 4.691358024691358,
|
|
||||||
"avgTime": 0.1660813500000046
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.1715666999999968
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.23760415000000706
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0.012345679012345678,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.14814814814814814,
|
|
||||||
"avgTime": 0.333931249999992
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 1.439506172839506,
|
|
||||||
"minSolved": 0.39506172839506176,
|
|
||||||
"maxSolved": 5.135802469135802,
|
|
||||||
"avgTime": 0.9644125499999916
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 81.71851851851852,
|
|
||||||
"minSolved": 6.0246913580246915,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 5.281324949999998
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.94074074074075,
|
|
||||||
"minSolved": 99.4074074074074,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 1.2768960000000107
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.638566650000007
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 45,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.3004915999999923
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 1.7,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 5.92,
|
|
||||||
"avgTime": 0.20294785000000387
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.23199789999999892
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.29876259999999205
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0.008,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.16,
|
|
||||||
"avgTime": 0.38459799999998834
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 0.5099999999999999,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 1.7999999999999998,
|
|
||||||
"avgTime": 0.8961771499999941
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 73.258,
|
|
||||||
"minSolved": 5.6000000000000005,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 7.937735449999991
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.96399999999998,
|
|
||||||
"minSolved": 99.76,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 1.6324250000000062
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.8293270000000064
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 50,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.41459575000000654
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 0.16666666666666669,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 1.6666666666666667,
|
|
||||||
"avgTime": 0.2432124999999928
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.35082704999999237
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.49827310000000014
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0.0027777777777777775,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.05555555555555555,
|
|
||||||
"avgTime": 0.6393062499999985
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 0.23055555555555554,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 1.6666666666666667,
|
|
||||||
"avgTime": 1.2402395500000012
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 35.01805555555556,
|
|
||||||
"minSolved": 1.3333333333333335,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 10.759754149999992
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.96944444444445,
|
|
||||||
"minSolved": 99.83333333333333,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 2.964204100000029
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 1.2736664999999903
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 60,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.6249353999999812
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 0.14285714285714285,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 1.4285714285714286,
|
|
||||||
"avgTime": 0.34277719999998907
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.5435105000000249
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.7600602999999865
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.9591250999999943
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 0.04081632653061225,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.2857142857142857,
|
|
||||||
"avgTime": 1.491010399999982
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 16.403061224489797,
|
|
||||||
"minSolved": 1,
|
|
||||||
"maxSolved": 99.71428571428571,
|
|
||||||
"avgTime": 22.21432699999999
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.96836734693878,
|
|
||||||
"minSolved": 99.73469387755102,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 4.92020829999999
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 99.99591836734695,
|
|
||||||
"minSolved": 99.91836734693878,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 2.0306394499999554
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 70,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 0.8882499500000336
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.1,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.4418666499999858
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.2,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 0.7795667999999978
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.3,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 1.0745101999999747
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.4,
|
|
||||||
"avgSolved": 0,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0,
|
|
||||||
"avgTime": 1.3407041500000105
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.5,
|
|
||||||
"avgSolved": 0.0125,
|
|
||||||
"minSolved": 0,
|
|
||||||
"maxSolved": 0.078125,
|
|
||||||
"avgTime": 1.9724897000000283
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.6,
|
|
||||||
"avgSolved": 1.21484375,
|
|
||||||
"minSolved": 0.40625,
|
|
||||||
"maxSolved": 2.296875,
|
|
||||||
"avgTime": 3.9163123999999927
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.7,
|
|
||||||
"avgSolved": 99.978125,
|
|
||||||
"minSolved": 99.9375,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 7.790070799999967
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.8,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 3.1350061999999754
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size": 80,
|
|
||||||
"density": 0.9,
|
|
||||||
"avgSolved": 100,
|
|
||||||
"minSolved": 100,
|
|
||||||
"maxSolved": 100,
|
|
||||||
"avgTime": 1.3134414999999535
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -10,5 +10,5 @@ services:
|
|||||||
- "8081:80"
|
- "8081:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
# Uncomment the following lines if you want to mount the configuration locally for development/testing
|
# Uncomment the following lines if you want to mount the configuration locally for development/testing
|
||||||
volumes:
|
# volumes:
|
||||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
# - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/nonograms.svg" />
|
<link rel="icon" type="image/svg+xml" href="/nonograms.svg" />
|
||||||
<link rel="apple-touch-icon" href="/nonograms.svg" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<link rel="mask-icon" href="/nonograms.svg" color="#00f2fe" />
|
<link rel="mask-icon" href="/nonograms.svg" color="#00f2fe" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Nonograms Pro - Vue 3 SOLID</title>
|
<title>Nonograms Pro - Vue 3 SOLID</title>
|
||||||
|
|||||||
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.9.10",
|
"version": "1.14.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.9.10",
|
"version": "1.14.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fireworks-js": "^2.10.8",
|
"fireworks-js": "^2.10.8",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.9.10",
|
"version": "1.14.1",
|
||||||
|
"homepage": "https://nonograms.7u.pl/",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest"
|
"test": "vitest"
|
||||||
|
|||||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -1,28 +1,44 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
|
||||||
<rect width="192" height="192" fill="#0b0f1f"/>
|
<defs>
|
||||||
<g transform="translate(24,24)">
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
<rect x="0" y="0" width="144" height="144" rx="16" fill="#121639" stroke="#00f2fe" stroke-width="4"/>
|
<stop offset="0" stop-color="#43C6AC"/>
|
||||||
<g stroke="#00f2fe" stroke-width="2">
|
<stop offset="1" stop-color="#191654"/>
|
||||||
<line x1="24" y1="0" x2="24" y2="144"/>
|
</linearGradient>
|
||||||
<line x1="48" y1="0" x2="48" y2="144"/>
|
<linearGradient id="cell" x1="0" y1="0" x2="1" y2="1">
|
||||||
<line x1="72" y1="0" x2="72" y2="144"/>
|
<stop offset="0" stop-color="#00f2fe"/>
|
||||||
<line x1="96" y1="0" x2="96" y2="144"/>
|
<stop offset="1" stop-color="#4facfe"/>
|
||||||
<line x1="120" y1="0" x2="120" y2="144"/>
|
</linearGradient>
|
||||||
<line x1="0" y1="24" x2="144" y2="24"/>
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
<line x1="0" y1="48" x2="144" y2="48"/>
|
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
||||||
<line x1="0" y1="72" x2="144" y2="72"/>
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
<line x1="0" y1="96" x2="144" y2="96"/>
|
</filter>
|
||||||
<line x1="0" y1="120" x2="144" y2="120"/>
|
</defs>
|
||||||
</g>
|
|
||||||
<g fill="#00f2fe">
|
<!-- Main Background -->
|
||||||
<rect x="6" y="6" width="18" height="18" rx="3"/>
|
<rect width="192" height="192" rx="42" fill="url(#bg)"/>
|
||||||
<rect x="54" y="30" width="18" height="18" rx="3"/>
|
|
||||||
<rect x="102" y="78" width="18" height="18" rx="3"/>
|
<!-- Console Screen Background -->
|
||||||
<rect x="30" y="126" width="18" height="18" rx="3"/>
|
<rect x="26" y="26" width="140" height="140" rx="16" fill="rgba(0,10,30,0.5)" stroke="rgba(0,242,254,0.2)" stroke-width="1.5"/>
|
||||||
</g>
|
|
||||||
<g fill="#ffffff">
|
<!-- Letter N built from Nonogram cells -->
|
||||||
<path d="M36 40 h16 v64 h-16 z"/>
|
<g fill="url(#cell)" filter="url(#glow)">
|
||||||
<path d="M52 40 h16 l32 48 v-48 h16 v64 h-16 l-32 -48 v48 h-16 z"/>
|
<!-- Left Column -->
|
||||||
</g>
|
<rect x="38" y="38" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="110" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="134" width="20" height="20" rx="4"/>
|
||||||
|
|
||||||
|
<!-- Diagonal -->
|
||||||
|
<rect x="62" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="86" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="110" y="110" width="20" height="20" rx="4"/>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<rect x="134" y="38" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="110" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="134" width="20" height="20" rx="4"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
public/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -8,17 +8,37 @@
|
|||||||
<stop offset="0" stop-color="#00f2fe"/>
|
<stop offset="0" stop-color="#00f2fe"/>
|
||||||
<stop offset="1" stop-color="#4facfe"/>
|
<stop offset="1" stop-color="#4facfe"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="192" height="192" rx="28" fill="url(#bg)"/>
|
|
||||||
<rect x="28" y="28" width="136" height="136" rx="14" fill="rgba(0,0,0,0.35)"/>
|
<!-- Main Background -->
|
||||||
<g fill="url(#cell)">
|
<rect width="192" height="192" rx="42" fill="url(#bg)"/>
|
||||||
<rect x="48" y="48" width="20" height="20" rx="4"/>
|
|
||||||
<rect x="76" y="48" width="20" height="20" rx="4"/>
|
<!-- Console Screen Background -->
|
||||||
<rect x="104" y="48" width="20" height="20" rx="4"/>
|
<rect x="26" y="26" width="140" height="140" rx="16" fill="rgba(0,10,30,0.5)" stroke="rgba(0,242,254,0.2)" stroke-width="1.5"/>
|
||||||
<rect x="48" y="76" width="20" height="20" rx="4"/>
|
|
||||||
<rect x="104" y="76" width="20" height="20" rx="4"/>
|
<!-- Letter N built from Nonogram cells -->
|
||||||
<rect x="48" y="104" width="20" height="20" rx="4"/>
|
<g fill="url(#cell)" filter="url(#glow)">
|
||||||
<rect x="76" y="104" width="20" height="20" rx="4"/>
|
<!-- Left Column -->
|
||||||
<rect x="104" y="104" width="20" height="20" rx="4"/>
|
<rect x="38" y="38" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="110" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="134" width="20" height="20" rx="4"/>
|
||||||
|
|
||||||
|
<!-- Diagonal -->
|
||||||
|
<rect x="62" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="86" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="110" y="110" width="20" height="20" rx="4"/>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<rect x="134" y="38" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="110" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="134" width="20" height="20" rx="4"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
public/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 192 192">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
<stop offset="0" stop-color="#43C6AC"/>
|
<stop offset="0" stop-color="#43C6AC"/>
|
||||||
@@ -8,17 +8,37 @@
|
|||||||
<stop offset="0" stop-color="#00f2fe"/>
|
<stop offset="0" stop-color="#00f2fe"/>
|
||||||
<stop offset="1" stop-color="#4facfe"/>
|
<stop offset="1" stop-color="#4facfe"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
|
||||||
|
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
||||||
|
<feComposite in="SourceGraphic" in2="blur" operator="over"/>
|
||||||
|
</filter>
|
||||||
</defs>
|
</defs>
|
||||||
<rect width="512" height="512" rx="80" fill="url(#bg)"/>
|
|
||||||
<rect x="74" y="74" width="364" height="364" rx="40" fill="rgba(0,0,0,0.35)"/>
|
<!-- Main Background -->
|
||||||
<g fill="url(#cell)">
|
<rect width="192" height="192" rx="42" fill="url(#bg)"/>
|
||||||
<rect x="138" y="138" width="54" height="54" rx="10"/>
|
|
||||||
<rect x="214" y="138" width="54" height="54" rx="10"/>
|
<!-- Console Screen Background -->
|
||||||
<rect x="290" y="138" width="54" height="54" rx="10"/>
|
<rect x="26" y="26" width="140" height="140" rx="16" fill="rgba(0,10,30,0.5)" stroke="rgba(0,242,254,0.2)" stroke-width="1.5"/>
|
||||||
<rect x="138" y="214" width="54" height="54" rx="10"/>
|
|
||||||
<rect x="290" y="214" width="54" height="54" rx="10"/>
|
<!-- Letter N built from Nonogram cells -->
|
||||||
<rect x="138" y="290" width="54" height="54" rx="10"/>
|
<g fill="url(#cell)" filter="url(#glow)">
|
||||||
<rect x="214" y="290" width="54" height="54" rx="10"/>
|
<!-- Left Column -->
|
||||||
<rect x="290" y="290" width="54" height="54" rx="10"/>
|
<rect x="38" y="38" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="110" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="38" y="134" width="20" height="20" rx="4"/>
|
||||||
|
|
||||||
|
<!-- Diagonal -->
|
||||||
|
<rect x="62" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="86" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="110" y="110" width="20" height="20" rx="4"/>
|
||||||
|
|
||||||
|
<!-- Right Column -->
|
||||||
|
<rect x="134" y="38" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="62" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="86" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="110" width="20" height="20" rx="4"/>
|
||||||
|
<rect x="134" y="134" width="20" height="20" rx="4"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
public/screenshot.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
49
scripts/fill_language_labels.cjs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = 'src/composables/useI18n.js';
|
||||||
|
let content = fs.readFileSync(path, 'utf8');
|
||||||
|
|
||||||
|
const messagesMatch = content.match(/const messages = ({[\s\S]*?});/);
|
||||||
|
if (!messagesMatch) {
|
||||||
|
console.error('Could not find messages object');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = eval(`(${messagesMatch[1]})`);
|
||||||
|
const en = messages.en || {};
|
||||||
|
const allLangKeys = Object.keys(en).filter((k) => k.startsWith('language.'));
|
||||||
|
|
||||||
|
function injectMissingLanguageLabels(localeCode) {
|
||||||
|
const blockStartRegex = new RegExp(`\\s{2}['\"]?${localeCode}['\"]?\\s*:\\s*\\{`);
|
||||||
|
const startIndex = content.search(blockStartRegex);
|
||||||
|
if (startIndex === -1) return;
|
||||||
|
const braceStart = content.indexOf('{', startIndex);
|
||||||
|
let i = braceStart + 1;
|
||||||
|
let depth = 1;
|
||||||
|
while (i < content.length && depth > 0) {
|
||||||
|
if (content[i] === '{') depth++;
|
||||||
|
else if (content[i] === '}') depth--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
const block = content.slice(braceStart + 1, i - 1);
|
||||||
|
let updated = block;
|
||||||
|
let addedAny = false;
|
||||||
|
allLangKeys.forEach((key) => {
|
||||||
|
const keyRegex = new RegExp(`(['\"])${key}\\1\\s*:\\s*(['\"]).*?\\2`);
|
||||||
|
if (!keyRegex.test(updated)) {
|
||||||
|
const value = en[key];
|
||||||
|
updated = updated.trim().endsWith(',') ? updated + `\n '${key}': '${value}'` : updated + `,\n '${key}': '${value}'`;
|
||||||
|
addedAny = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (addedAny) {
|
||||||
|
content = content.slice(0, braceStart + 1) + updated + content.slice(i - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(messages).forEach((locale) => {
|
||||||
|
if (locale === 'en') return;
|
||||||
|
injectMissingLanguageLabels(locale);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
|
console.log('Filled missing language.* labels for all locales.');
|
||||||
42
scripts/generate-icons.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import sharp from 'sharp';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const INPUT_FILE = path.join(__dirname, '../public/nonograms.svg');
|
||||||
|
const OUTPUT_DIR = path.join(__dirname, '../public');
|
||||||
|
|
||||||
|
async function generateIcons() {
|
||||||
|
console.log('Generating icons from ' + INPUT_FILE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 192x192
|
||||||
|
await sharp(INPUT_FILE)
|
||||||
|
.resize(192, 192)
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(OUTPUT_DIR, 'pwa-192x192.png'));
|
||||||
|
console.log('Created pwa-192x192.png');
|
||||||
|
|
||||||
|
// 512x512
|
||||||
|
await sharp(INPUT_FILE)
|
||||||
|
.resize(512, 512)
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(OUTPUT_DIR, 'pwa-512x512.png'));
|
||||||
|
console.log('Created pwa-512x512.png');
|
||||||
|
|
||||||
|
// Apple Touch Icon (180x180)
|
||||||
|
await sharp(INPUT_FILE)
|
||||||
|
.resize(180, 180)
|
||||||
|
.png()
|
||||||
|
.toFile(path.join(OUTPUT_DIR, 'apple-touch-icon.png'));
|
||||||
|
console.log('Created apple-touch-icon.png');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error generating icons:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateIcons();
|
||||||
47
scripts/run_simulation.mjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
import { generateRandomGrid, calculateHints } from '../src/utils/puzzleUtils.js';
|
||||||
|
import { solvePuzzle } from '../src/utils/solver.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
const SIZES = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80];
|
||||||
|
const DENSITIES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
||||||
|
const SAMPLES = 200; // High sample count for stability
|
||||||
|
|
||||||
|
console.log('Starting simulation...');
|
||||||
|
console.log(`Sizes: ${SIZES.join(', ')}`);
|
||||||
|
console.log(`Densities: ${DENSITIES.join(', ')}`);
|
||||||
|
console.log(`Samples per point: ${SAMPLES}`);
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const totalSteps = SIZES.length * DENSITIES.length;
|
||||||
|
let currentStep = 0;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
for (const size of SIZES) {
|
||||||
|
results[size] = [];
|
||||||
|
for (const density of DENSITIES) {
|
||||||
|
let totalSolved = 0;
|
||||||
|
for (let i = 0; i < SAMPLES; i++) {
|
||||||
|
const grid = generateRandomGrid(size, density);
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
const { percentSolved } = solvePuzzle(rowHints, colHints);
|
||||||
|
totalSolved += percentSolved;
|
||||||
|
}
|
||||||
|
const avg = Math.round(totalSolved / SAMPLES);
|
||||||
|
results[size].push(avg);
|
||||||
|
|
||||||
|
currentStep++;
|
||||||
|
const pct = Math.round((currentStep / totalSteps) * 100);
|
||||||
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
||||||
|
process.stdout.write(`\rProgress: ${pct}% (${currentStep}/${totalSteps}) - Elapsed: ${elapsed}s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n\nSimulation complete!');
|
||||||
|
console.log('SIM_DATA = ' + JSON.stringify(results, null, 4) + ';');
|
||||||
|
fs.writeFileSync('simulation_results.json', JSON.stringify(results, null, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
@@ -1,75 +1,38 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { generateRandomGrid, calculateHints } from '../src/utils/puzzleUtils.js';
|
import { generateRandomGrid, calculateHints } from '../src/utils/puzzleUtils.js';
|
||||||
import { solvePuzzle } from '../src/utils/solver.js';
|
import { solvePuzzle } from '../src/utils/solver.js';
|
||||||
|
|
||||||
const OUTPUT_FILE = 'difficulty_simulation_results.json';
|
const SIZES = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80];
|
||||||
const CSV_FILE = 'difficulty_simulation_results.csv';
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
const SIZES = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80]; // Steps of 5 up to 50, then 10
|
|
||||||
const DENSITIES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
const DENSITIES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
||||||
const SAMPLES_PER_POINT = 20; // Adjust based on time/accuracy needs
|
const SAMPLES_SMALL = 100; // For size <= 25
|
||||||
|
const SAMPLES_LARGE = 30; // For size > 25
|
||||||
|
|
||||||
console.log('Starting Monte Carlo Simulation for Nonogram Difficulty...');
|
const results = {};
|
||||||
console.log(`Config: Sizes=${SIZES.length}, Densities=${DENSITIES.length}, Samples=${SAMPLES_PER_POINT}`);
|
|
||||||
|
|
||||||
const results = [];
|
console.log('Starting Monte Carlo Simulation...');
|
||||||
const csvRows = ['size,density,avg_solved_percent,min_solved_percent,max_solved_percent,avg_time_ms'];
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
for (const size of SIZES) {
|
for (const size of SIZES) {
|
||||||
|
const samples = size <= 25 ? SAMPLES_SMALL : SAMPLES_LARGE;
|
||||||
|
const rowData = [];
|
||||||
|
|
||||||
for (const density of DENSITIES) {
|
for (const density of DENSITIES) {
|
||||||
let totalSolved = 0;
|
let totalSolved = 0;
|
||||||
let minSolved = 100;
|
|
||||||
let maxSolved = 0;
|
|
||||||
let totalTime = 0;
|
|
||||||
|
|
||||||
process.stdout.write(`Simulating Size: ${size}x${size}, Density: ${density} ... `);
|
for (let i = 0; i < samples; i++) {
|
||||||
|
|
||||||
for (let i = 0; i < SAMPLES_PER_POINT; i++) {
|
|
||||||
const t0 = performance.now();
|
|
||||||
|
|
||||||
// 1. Generate
|
|
||||||
const grid = generateRandomGrid(size, density);
|
const grid = generateRandomGrid(size, density);
|
||||||
const { rowHints, colHints } = calculateHints(grid);
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
|
||||||
// 2. Solve
|
|
||||||
const { percentSolved } = solvePuzzle(rowHints, colHints);
|
const { percentSolved } = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
const t1 = performance.now();
|
|
||||||
|
|
||||||
totalSolved += percentSolved;
|
totalSolved += percentSolved;
|
||||||
minSolved = Math.min(minSolved, percentSolved);
|
|
||||||
maxSolved = Math.max(maxSolved, percentSolved);
|
|
||||||
totalTime += (t1 - t0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const avgSolved = totalSolved / SAMPLES_PER_POINT;
|
const avg = Math.round(totalSolved / samples);
|
||||||
const avgTime = totalTime / SAMPLES_PER_POINT;
|
rowData.push(avg);
|
||||||
|
|
||||||
results.push({
|
|
||||||
size,
|
|
||||||
density,
|
|
||||||
avgSolved,
|
|
||||||
minSolved,
|
|
||||||
maxSolved,
|
|
||||||
avgTime
|
|
||||||
});
|
|
||||||
|
|
||||||
csvRows.push(`${size},${density},${avgSolved.toFixed(2)},${minSolved.toFixed(2)},${maxSolved.toFixed(2)},${avgTime.toFixed(2)}`);
|
|
||||||
|
|
||||||
console.log(`Avg Solved: ${avgSolved.toFixed(1)}%`);
|
|
||||||
}
|
}
|
||||||
|
results[size] = rowData;
|
||||||
|
console.log(` Size ${size}: [${rowData.join(', ')}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalDuration = (Date.now() - startTime) / 1000;
|
const duration = (Date.now() - startTime) / 1000;
|
||||||
console.log(`Simulation complete in ${totalDuration.toFixed(1)}s`);
|
console.log(`\nSimulation Complete in ${duration.toFixed(2)}s. Result JSON:`);
|
||||||
|
console.log(JSON.stringify(results, null, 4));
|
||||||
// Save results
|
|
||||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(results, null, 2));
|
|
||||||
fs.writeFileSync(CSV_FILE, csvRows.join('\n'));
|
|
||||||
|
|
||||||
console.log(`Results saved to ${OUTPUT_FILE} and ${CSV_FILE}`);
|
|
||||||
|
|||||||
514
scripts/translate_simulation.cjs
Normal file
@@ -0,0 +1,514 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = 'src/composables/useI18n.js';
|
||||||
|
let content = fs.readFileSync(path, 'utf8');
|
||||||
|
|
||||||
|
const keys = {
|
||||||
|
'custom.showMap': {
|
||||||
|
es: 'Mostrar mapa de dificultad',
|
||||||
|
fr: 'Afficher la carte de difficulté',
|
||||||
|
de: 'Schwierigkeitskarte anzeigen',
|
||||||
|
it: 'Mostra mappa difficoltà',
|
||||||
|
pt: 'Mostrar mapa de dificuldade',
|
||||||
|
'pt-br': 'Mostrar mapa de dificuldade',
|
||||||
|
ru: 'Показать карту сложности',
|
||||||
|
zh: '显示难度地图',
|
||||||
|
ja: '難易度マップを表示',
|
||||||
|
ko: '난이도 맵 표시',
|
||||||
|
tr: 'Zorluk haritasını göster',
|
||||||
|
uk: 'Показати карту складності',
|
||||||
|
cs: 'Zobrazit mapu obtížnosti',
|
||||||
|
sk: 'Zobraziť mapu náročnosti',
|
||||||
|
hu: 'Nehézségi térkép megjelenítése',
|
||||||
|
ro: 'Arată harta dificultății',
|
||||||
|
bg: 'Покажи картата на трудността',
|
||||||
|
el: 'Εμφάνιση χάρτη δυσκολίας',
|
||||||
|
sr: 'Прикажи карту тежине',
|
||||||
|
hr: 'Prikaži kartu težine',
|
||||||
|
sl: 'Prikaži zemljevid težavnosti',
|
||||||
|
lt: 'Rodyti sudėtingumo žemėlapį',
|
||||||
|
lv: 'Rādīt grūtības karti',
|
||||||
|
et: 'Näita raskusaste kaarti',
|
||||||
|
nl: 'Moeilijkheidskaart weergeven',
|
||||||
|
sv: 'Visa svårighetskarta',
|
||||||
|
da: 'Vis sværhedsgradskort',
|
||||||
|
fi: 'Näytä vaikeuskartta',
|
||||||
|
no: 'Vis vanskelighetskart',
|
||||||
|
ar: 'إظهار خريطة الصعوبة',
|
||||||
|
hi: 'कठिनाई मानचित्र दिखाएँ',
|
||||||
|
bn: 'কঠিনতার মানচিত্র দেখান'
|
||||||
|
},
|
||||||
|
'custom.hideMap': {
|
||||||
|
es: 'Ocultar mapa de dificultad',
|
||||||
|
fr: 'Masquer la carte de difficulté',
|
||||||
|
de: 'Schwierigkeitskarte ausblenden',
|
||||||
|
it: 'Nascondi mappa difficoltà',
|
||||||
|
pt: 'Ocultar mapa de dificuldade',
|
||||||
|
'pt-br': 'Ocultar mapa de dificuldade',
|
||||||
|
ru: 'Скрыть карту сложности',
|
||||||
|
zh: '隐藏难度地图',
|
||||||
|
ja: '難易度マップを非表示',
|
||||||
|
ko: '난이도 맵 숨기기',
|
||||||
|
tr: 'Zorluk haritasını gizle',
|
||||||
|
uk: 'Приховати карту складності',
|
||||||
|
cs: 'Skrýt mapu obtížnosti',
|
||||||
|
sk: 'Skryť mapu náročnosti',
|
||||||
|
hu: 'Nehézségi térkép elrejtése',
|
||||||
|
ro: 'Ascunde harta dificultății',
|
||||||
|
bg: 'Скрий картата на трудността',
|
||||||
|
el: 'Απόκρυψη χάρτη δυσκολίας',
|
||||||
|
sr: 'Сакриј карту тежине',
|
||||||
|
hr: 'Sakrij kartu težine',
|
||||||
|
sl: 'Skrij zemljevid težavnosti',
|
||||||
|
lt: 'Slėpti sudėtingumo žemėlapį',
|
||||||
|
lv: 'Paslēpt grūtības karti',
|
||||||
|
et: 'Peida raskusaste kaart',
|
||||||
|
nl: 'Moeilijkheidskaart verbergen',
|
||||||
|
sv: 'Dölj svårighetskarta',
|
||||||
|
da: 'Skjul sværhedsgradskort',
|
||||||
|
fi: 'Piilota vaikeuskartta',
|
||||||
|
no: 'Skjul vanskelighetskart',
|
||||||
|
ar: 'إخفاء خريطة الصعوبة',
|
||||||
|
hi: 'कठिनाई मानचित्र छुपाएँ',
|
||||||
|
bn: 'কঠিনতার মানচিত্র লুকান'
|
||||||
|
},
|
||||||
|
'simulation.title': {
|
||||||
|
es: 'Simulación de Dificultad',
|
||||||
|
fr: 'Simulation de difficulté',
|
||||||
|
de: 'Schwierigkeitssimulation',
|
||||||
|
it: 'Simulazione della difficoltà',
|
||||||
|
pt: 'Simulação de Dificuldade',
|
||||||
|
'pt-br': 'Simulação de Dificuldade',
|
||||||
|
ru: 'Симуляция сложности',
|
||||||
|
zh: '难度模拟',
|
||||||
|
ja: '難易度シミュレーション',
|
||||||
|
ko: '난이도 시뮬레이션',
|
||||||
|
tr: 'Zorluk simülasyonu',
|
||||||
|
uk: 'Симуляція складності',
|
||||||
|
cs: 'Simulace obtížnosti',
|
||||||
|
sk: 'Simulácia náročnosti',
|
||||||
|
hu: 'Nehézség szimuláció',
|
||||||
|
ro: 'Simulare de dificultate',
|
||||||
|
bg: 'Симулиране на трудност',
|
||||||
|
el: 'Προσομοίωση δυσκολίας',
|
||||||
|
sr: 'Симулација тежине',
|
||||||
|
hr: 'Simulacija težine',
|
||||||
|
sl: 'Simulacija težavnosti',
|
||||||
|
lt: 'Sudėtingumo simuliacija',
|
||||||
|
lv: 'Grūtības simulācija',
|
||||||
|
et: 'Raskusastme simulatsioon',
|
||||||
|
nl: 'Moeilijkheidssimulatie',
|
||||||
|
sv: 'Svårighetssimulering',
|
||||||
|
da: 'Sværhedsgradssimulering',
|
||||||
|
fi: 'Vaikeussimulointi',
|
||||||
|
no: 'Vanskelighetssimulering',
|
||||||
|
ar: 'محاكاة الصعوبة',
|
||||||
|
hi: 'कठिनाई सिमुलेशन',
|
||||||
|
bn: 'কঠিনতা সিমুলেশন'
|
||||||
|
},
|
||||||
|
'simulation.status.ready': {
|
||||||
|
es: 'Listo',
|
||||||
|
fr: 'Prêt',
|
||||||
|
de: 'Bereit',
|
||||||
|
it: 'Pronto',
|
||||||
|
pt: 'Pronto',
|
||||||
|
'pt-br': 'Pronto',
|
||||||
|
ru: 'Готово',
|
||||||
|
zh: '就绪',
|
||||||
|
ja: '準備完了',
|
||||||
|
ko: '준비됨',
|
||||||
|
tr: 'Hazır',
|
||||||
|
uk: 'Готово',
|
||||||
|
cs: 'Připraveno',
|
||||||
|
sk: 'Pripravené',
|
||||||
|
hu: 'Kész',
|
||||||
|
ro: 'Gata',
|
||||||
|
bg: 'Готово',
|
||||||
|
el: 'Έτοιμο',
|
||||||
|
sr: 'Спремно',
|
||||||
|
hr: 'Spremno',
|
||||||
|
sl: 'Pripravljeno',
|
||||||
|
lt: 'Paruošta',
|
||||||
|
lv: 'Gatavs',
|
||||||
|
et: 'Valmis',
|
||||||
|
nl: 'Gereed',
|
||||||
|
sv: 'Klar',
|
||||||
|
da: 'Klar',
|
||||||
|
fi: 'Valmis',
|
||||||
|
no: 'Klar',
|
||||||
|
ar: 'جاهز',
|
||||||
|
hi: 'तैयार',
|
||||||
|
bn: 'প্রস্তুত'
|
||||||
|
},
|
||||||
|
'simulation.status.stopped': {
|
||||||
|
es: 'Detenido',
|
||||||
|
fr: 'Arrêté',
|
||||||
|
de: 'Gestoppt',
|
||||||
|
it: 'Arrestato',
|
||||||
|
pt: 'Parado',
|
||||||
|
'pt-br': 'Parado',
|
||||||
|
ru: 'Остановлено',
|
||||||
|
zh: '已停止',
|
||||||
|
ja: '停止',
|
||||||
|
ko: '중지됨',
|
||||||
|
tr: 'Durduruldu',
|
||||||
|
uk: 'Зупинено',
|
||||||
|
cs: 'Zastaveno',
|
||||||
|
sk: 'Zastavené',
|
||||||
|
hu: 'Leállítva',
|
||||||
|
ro: 'Oprit',
|
||||||
|
bg: 'Спряно',
|
||||||
|
el: 'Διακοπή',
|
||||||
|
sr: 'Заустављено',
|
||||||
|
hr: 'Zaustavljeno',
|
||||||
|
sl: 'Ustavljeno',
|
||||||
|
lt: 'Sustabdyta',
|
||||||
|
lv: 'Apturēts',
|
||||||
|
et: 'Peatatud',
|
||||||
|
nl: 'Gestopt',
|
||||||
|
sv: 'Stoppad',
|
||||||
|
da: 'Stoppet',
|
||||||
|
fi: 'Pysäytetty',
|
||||||
|
no: 'Stoppet',
|
||||||
|
ar: 'متوقف',
|
||||||
|
hi: 'रोका गया',
|
||||||
|
bn: 'বন্ধ'
|
||||||
|
},
|
||||||
|
'simulation.status.completed': {
|
||||||
|
es: 'Completado',
|
||||||
|
fr: 'Terminé',
|
||||||
|
de: 'Abgeschlossen',
|
||||||
|
it: 'Completato',
|
||||||
|
pt: 'Concluído',
|
||||||
|
'pt-br': 'Concluído',
|
||||||
|
ru: 'Завершено',
|
||||||
|
zh: '已完成',
|
||||||
|
ja: '完了',
|
||||||
|
ko: '완료됨',
|
||||||
|
tr: 'Tamamlandı',
|
||||||
|
uk: 'Завершено',
|
||||||
|
cs: 'Dokončeno',
|
||||||
|
sk: 'Dokončené',
|
||||||
|
hu: 'Befejezve',
|
||||||
|
ro: 'Finalizat',
|
||||||
|
bg: 'Завършено',
|
||||||
|
el: 'Ολοκληρώθηκε',
|
||||||
|
sr: 'Завршено',
|
||||||
|
hr: 'Dovršeno',
|
||||||
|
sl: 'Dokončano',
|
||||||
|
lt: 'Baigta',
|
||||||
|
lv: 'Pabeigts',
|
||||||
|
et: 'Lõpetatud',
|
||||||
|
nl: 'Voltooid',
|
||||||
|
sv: 'Slutförd',
|
||||||
|
da: 'Fuldført',
|
||||||
|
fi: 'Valmis',
|
||||||
|
no: 'Fullført',
|
||||||
|
ar: 'مكتمل',
|
||||||
|
hi: 'पूर्ण',
|
||||||
|
bn: 'সম্পন্ন'
|
||||||
|
},
|
||||||
|
'simulation.status.simulating': {
|
||||||
|
es: 'Simulando {size}x{size} @ {density}%',
|
||||||
|
fr: 'Simulation de {size}x{size} à {density}%',
|
||||||
|
de: 'Simuliere {size}x{size} @ {density}%',
|
||||||
|
it: 'Simulazione {size}x{size} @ {density}%',
|
||||||
|
pt: 'Simulando {size}x{size} @ {density}%',
|
||||||
|
'pt-br': 'Simulando {size}x{size} @ {density}%',
|
||||||
|
ru: 'Симуляция {size}x{size} @ {density}%',
|
||||||
|
zh: '正在模拟 {size}x{size} @ {density}%',
|
||||||
|
ja: '{size}x{size} @ {density}% をシミュレーション中',
|
||||||
|
ko: '{size}x{size} @ {density}% 시뮬레이션 중',
|
||||||
|
tr: '{size}x{size} @ {density}% simüle ediliyor',
|
||||||
|
uk: 'Симулювання {size}x{size} @ {density}%',
|
||||||
|
cs: 'Simulace {size}x{size} @ {density}%',
|
||||||
|
sk: 'Simulácia {size}x{size} @ {density}%',
|
||||||
|
hu: 'Szimulálás {size}x{size} @ {density}%',
|
||||||
|
ro: 'Simulare {size}x{size} @ {density}%',
|
||||||
|
bg: 'Симулиране {size}x{size} @ {density}%',
|
||||||
|
el: 'Προσομοίωση {size}x{size} @ {density}%',
|
||||||
|
sr: 'Симулирање {size}x{size} @ {density}%',
|
||||||
|
hr: 'Simulacija {size}x{size} @ {density}%',
|
||||||
|
sl: 'Simulacija {size}x{size} @ {density}%',
|
||||||
|
lt: 'Simuliuojama {size}x{size} @ {density}%',
|
||||||
|
lv: 'Simulācija {size}x{size} @ {density}%',
|
||||||
|
et: 'Simuleerimine {size}x{size} @ {density}%',
|
||||||
|
nl: 'Simuleren {size}x{size} @ {density}%',
|
||||||
|
sv: 'Simulerar {size}x{size} @ {density}%',
|
||||||
|
da: 'Simulerer {size}x{size} @ {density}%',
|
||||||
|
fi: 'Simulointi {size}x{size} @ {density}%',
|
||||||
|
no: 'Simulerer {size}x{size} @ {density}%',
|
||||||
|
ar: 'محاكاة {size}x{size} @ {density}%',
|
||||||
|
hi: '{size}x{size} @ {density}% का सिमुलेशन',
|
||||||
|
bn: '{size}x{size} @ {density}% সিমুলেট করা হচ্ছে'
|
||||||
|
},
|
||||||
|
'simulation.start': {
|
||||||
|
es: 'Iniciar simulación',
|
||||||
|
fr: 'Démarrer la simulation',
|
||||||
|
de: 'Simulation starten',
|
||||||
|
it: 'Avvia simulazione',
|
||||||
|
pt: 'Iniciar simulação',
|
||||||
|
'pt-br': 'Iniciar simulação',
|
||||||
|
ru: 'Начать симуляцию',
|
||||||
|
zh: '开始模拟',
|
||||||
|
ja: 'シミュレーション開始',
|
||||||
|
ko: '시뮬레이션 시작',
|
||||||
|
tr: 'Simülasyonu başlat',
|
||||||
|
uk: 'Почати симуляцію',
|
||||||
|
cs: 'Spustit simulaci',
|
||||||
|
sk: 'Spustiť simuláciu',
|
||||||
|
hu: 'Szimuláció indítása',
|
||||||
|
ro: 'Pornește simularea',
|
||||||
|
bg: 'Стартирай симулация',
|
||||||
|
el: 'Έναρξη προσομοίωσης',
|
||||||
|
sr: 'Покрени симулацију',
|
||||||
|
hr: 'Pokreni simulaciju',
|
||||||
|
sl: 'Zaženi simulacijo',
|
||||||
|
lt: 'Pradėti simuliaciją',
|
||||||
|
lv: 'Sākt simulāciju',
|
||||||
|
et: 'Alusta simulatsiooni',
|
||||||
|
nl: 'Simulatie starten',
|
||||||
|
sv: 'Starta simulering',
|
||||||
|
da: 'Start simulering',
|
||||||
|
fi: 'Aloita simulointi',
|
||||||
|
no: 'Start simulering',
|
||||||
|
ar: 'بدء المحاكاة',
|
||||||
|
hi: 'सिमुलेशन शुरू करें',
|
||||||
|
bn: 'সিমুলেশন শুরু'
|
||||||
|
},
|
||||||
|
'simulation.stop': {
|
||||||
|
es: 'Detener',
|
||||||
|
fr: 'Arrêter',
|
||||||
|
de: 'Stoppen',
|
||||||
|
it: 'Stop',
|
||||||
|
pt: 'Parar',
|
||||||
|
'pt-br': 'Parar',
|
||||||
|
ru: 'Стоп',
|
||||||
|
zh: '停止',
|
||||||
|
ja: '停止',
|
||||||
|
ko: '중지',
|
||||||
|
tr: 'Durdur',
|
||||||
|
uk: 'Зупинити',
|
||||||
|
cs: 'Zastavit',
|
||||||
|
sk: 'Zastaviť',
|
||||||
|
hu: 'Leállítás',
|
||||||
|
ro: 'Oprește',
|
||||||
|
bg: 'Спри',
|
||||||
|
el: 'Διακοπή',
|
||||||
|
sr: 'Заустави',
|
||||||
|
hr: 'Zaustavi',
|
||||||
|
sl: 'Ustavi',
|
||||||
|
lt: 'Stabdyti',
|
||||||
|
lv: 'Apturēt',
|
||||||
|
et: 'Peata',
|
||||||
|
nl: 'Stoppen',
|
||||||
|
sv: 'Stoppa',
|
||||||
|
da: 'Stop',
|
||||||
|
fi: 'Pysäytä',
|
||||||
|
no: 'Stopp',
|
||||||
|
ar: 'إيقاف',
|
||||||
|
hi: 'रोकें',
|
||||||
|
bn: 'বন্ধ করুন'
|
||||||
|
},
|
||||||
|
'simulation.table.size': {
|
||||||
|
es: 'Tamaño',
|
||||||
|
fr: 'Taille',
|
||||||
|
de: 'Größe',
|
||||||
|
it: 'Dimensione',
|
||||||
|
pt: 'Tamanho',
|
||||||
|
'pt-br': 'Tamanho',
|
||||||
|
ru: 'Размер',
|
||||||
|
zh: '大小',
|
||||||
|
ja: 'サイズ',
|
||||||
|
ko: '크기',
|
||||||
|
tr: 'Boyut',
|
||||||
|
uk: 'Розмір',
|
||||||
|
cs: 'Velikost',
|
||||||
|
sk: 'Veľkosť',
|
||||||
|
hu: 'Méret',
|
||||||
|
ro: 'Dimensiune',
|
||||||
|
bg: 'Размер',
|
||||||
|
el: 'Μέγεθος',
|
||||||
|
sr: 'Величина',
|
||||||
|
hr: 'Veličina',
|
||||||
|
sl: 'Velikost',
|
||||||
|
lt: 'Dydis',
|
||||||
|
lv: 'Izmērs',
|
||||||
|
et: 'Suurus',
|
||||||
|
nl: 'Grootte',
|
||||||
|
sv: 'Storlek',
|
||||||
|
da: 'Størrelse',
|
||||||
|
fi: 'Koko',
|
||||||
|
no: 'Størrelse',
|
||||||
|
ar: 'الحجم',
|
||||||
|
hi: 'आकार',
|
||||||
|
bn: 'আকার'
|
||||||
|
},
|
||||||
|
'simulation.table.density': {
|
||||||
|
es: 'Densidad',
|
||||||
|
fr: 'Densité',
|
||||||
|
de: 'Dichte',
|
||||||
|
it: 'Densità',
|
||||||
|
pt: 'Densidade',
|
||||||
|
'pt-br': 'Densidade',
|
||||||
|
ru: 'Плотность',
|
||||||
|
zh: '密度',
|
||||||
|
ja: '密度',
|
||||||
|
ko: '밀도',
|
||||||
|
tr: 'Yoğunluk',
|
||||||
|
uk: 'Щільність',
|
||||||
|
cs: 'Hustota',
|
||||||
|
sk: 'Hustota',
|
||||||
|
hu: 'Sűrűség',
|
||||||
|
ro: 'Densitate',
|
||||||
|
bg: 'Плътност',
|
||||||
|
el: 'Πυκνότητα',
|
||||||
|
sr: 'Густина',
|
||||||
|
hr: 'Gustoća',
|
||||||
|
sl: 'Gostota',
|
||||||
|
lt: 'Tankis',
|
||||||
|
lv: 'Blīvums',
|
||||||
|
et: 'Tihedus',
|
||||||
|
nl: 'Dichtheid',
|
||||||
|
sv: 'Densitet',
|
||||||
|
da: 'Densitet',
|
||||||
|
fi: 'Tiheys',
|
||||||
|
no: 'Tetthet',
|
||||||
|
ar: 'الكثافة',
|
||||||
|
hi: 'घनत्व',
|
||||||
|
bn: 'ঘনত্ব'
|
||||||
|
},
|
||||||
|
'simulation.table.solved': {
|
||||||
|
es: 'Resuelto (Lógica)',
|
||||||
|
fr: 'Résolu (Logique)',
|
||||||
|
de: 'Gelöst (Logik)',
|
||||||
|
it: 'Risolto (Logica)',
|
||||||
|
pt: 'Resolvido (Lógica)',
|
||||||
|
'pt-br': 'Resolvido (Lógica)',
|
||||||
|
ru: 'Решено (Логика)',
|
||||||
|
zh: '已解(逻辑)',
|
||||||
|
ja: '解決(ロジック)',
|
||||||
|
ko: '해결됨(논리)',
|
||||||
|
tr: 'Çözüldü (Mantık)',
|
||||||
|
uk: 'Розв’язано (Логіка)',
|
||||||
|
cs: 'Vyřešeno (Logika)',
|
||||||
|
sk: 'Vyriešené (Logika)',
|
||||||
|
hu: 'Megoldva (Logika)',
|
||||||
|
ro: 'Rezolvat (Logică)',
|
||||||
|
bg: 'Решено (Логика)',
|
||||||
|
el: 'Επιλύθηκε (Λογική)',
|
||||||
|
sr: 'Решено (Логика)',
|
||||||
|
hr: 'Riješeno (Logika)',
|
||||||
|
sl: 'Rešeno (Logika)',
|
||||||
|
lt: 'Išspręsta (Logika)',
|
||||||
|
lv: 'Atrisināts (Loģika)',
|
||||||
|
et: 'Lahendatud (Loogika)',
|
||||||
|
nl: 'Opgelost (Logica)',
|
||||||
|
sv: 'Löst (Logik)',
|
||||||
|
da: 'Løst (Logik)',
|
||||||
|
fi: 'Ratkaistu (Logiikka)',
|
||||||
|
no: 'Løst (Logikk)',
|
||||||
|
ar: 'تم الحل (منطق)',
|
||||||
|
hi: 'हल (तर्क)',
|
||||||
|
bn: 'সমাধান (লজিক)'
|
||||||
|
},
|
||||||
|
'simulation.empty': {
|
||||||
|
es: 'Pulsa Iniciar para ejecutar la simulación Monte Carlo',
|
||||||
|
fr: 'Appuyez sur Démarrer pour lancer la simulation Monte Carlo',
|
||||||
|
de: 'Drücke Start, um die Monte-Carlo-Simulation zu starten',
|
||||||
|
it: 'Premi Avvia per eseguire la simulazione Monte Carlo',
|
||||||
|
pt: 'Pressione Iniciar para executar a simulação de Monte Carlo',
|
||||||
|
'pt-br': 'Pressione Iniciar para executar a simulação de Monte Carlo',
|
||||||
|
ru: 'Нажмите «Старт», чтобы запустить моделирование Монте‑Карло',
|
||||||
|
zh: '点击开始运行蒙特卡罗模拟',
|
||||||
|
ja: 'Monte Carlo シミュレーションを実行するには開始を押してください',
|
||||||
|
ko: 'Monte Carlo 시뮬레이션을 실행하려면 시작을 누르세요',
|
||||||
|
tr: 'Monte Carlo simülasyonunu çalıştırmak için Başlat’a basın',
|
||||||
|
uk: 'Натисніть «Почати», щоб запустити симуляцію Монте‑Карло',
|
||||||
|
cs: 'Stiskněte Start pro spuštění simulace Monte Carlo',
|
||||||
|
sk: 'Stlačte Štart pre spustenie simulácie Monte Carlo',
|
||||||
|
hu: 'Nyomd meg a Startot a Monte Carlo szimulációhoz',
|
||||||
|
ro: 'Apasă Start pentru a rula simularea Monte Carlo',
|
||||||
|
bg: 'Натисни Старт, за да стартираш симулация Монте Карло',
|
||||||
|
el: 'Πατήστε Έναρξη για να τρέξετε προσομοίωση Monte Carlo',
|
||||||
|
sr: 'Притисни Старт да покренеш Монте Карло симулацију',
|
||||||
|
hr: 'Pritisni Start za pokretanje Monte Carlo simulacije',
|
||||||
|
sl: 'Pritisnite Start za zagon simulacije Monte Carlo',
|
||||||
|
lt: 'Paspauskite Start, kad paleistumėte Monte Karlo simuliaciją',
|
||||||
|
lv: 'Nospiediet Start, lai palaistu Monte Carlo simulāciju',
|
||||||
|
et: 'Vajuta Start, et käivitada Monte Carlo simulatsioon',
|
||||||
|
nl: 'Druk op Start om de Monte Carlo-simulatie te starten',
|
||||||
|
sv: 'Tryck Start för att köra Monte Carlo-simuleringen',
|
||||||
|
da: 'Tryk Start for at køre Monte Carlo-simuleringen',
|
||||||
|
fi: 'Paina Käynnistä aloittaaksesi Monte Carlo -simulaation',
|
||||||
|
no: 'Trykk Start for å kjøre Monte Carlo-simuleringen',
|
||||||
|
ar: 'اضغط ابدأ لتشغيل محاكاة مونتِ كارلو',
|
||||||
|
hi: 'मोंटे कार्लो सिमुलेशन चलाने के लिए स्टार्ट दबाएँ',
|
||||||
|
bn: 'মোন্টে কার্লো সিমুলেশন চালাতে স্টার্ট চাপুন'
|
||||||
|
},
|
||||||
|
'custom.simulationHelp': {
|
||||||
|
es: '¿Cómo se calcula?',
|
||||||
|
fr: 'Comment est-ce calculé ?',
|
||||||
|
de: 'Wie wird das berechnet?',
|
||||||
|
it: 'Come viene calcolato?',
|
||||||
|
pt: 'Como isso é calculado?',
|
||||||
|
'pt-br': 'Como isso é calculado?',
|
||||||
|
ru: 'Как это рассчитывается?',
|
||||||
|
zh: '这是如何计算的?',
|
||||||
|
ja: 'これはどのように計算されますか?',
|
||||||
|
ko: '이것은 어떻게 계산됩니까?',
|
||||||
|
tr: 'Bu nasıl hesaplanıyor?',
|
||||||
|
uk: 'Як це обчислюється?',
|
||||||
|
cs: 'Jak se to počítá?',
|
||||||
|
sk: 'Ako sa to počíta?',
|
||||||
|
hu: 'Hogyan számoljuk?',
|
||||||
|
ro: 'Cum este calculat?',
|
||||||
|
bg: 'Как се изчислява?',
|
||||||
|
el: 'Πώς υπολογίζεται;',
|
||||||
|
sr: 'Како се израчунава?',
|
||||||
|
hr: 'Kako se izračunava?',
|
||||||
|
sl: 'Kako je izračunano?',
|
||||||
|
lt: 'Kaip tai apskaičiuojama?',
|
||||||
|
lv: 'Kā tas tiek aprēķināts?',
|
||||||
|
et: 'Kuidas see arvutatakse?',
|
||||||
|
nl: 'Hoe wordt dit berekend?',
|
||||||
|
sv: 'Hur beräknas detta?',
|
||||||
|
da: 'Hvordan beregnes dette?',
|
||||||
|
fi: 'Miten tämä lasketaan?',
|
||||||
|
no: 'Hvordan beregnes dette?',
|
||||||
|
ar: 'كيف يتم احتساب ذلك؟',
|
||||||
|
hi: 'यह कैसे गणना किया जाता है?',
|
||||||
|
bn: 'এটি কীভাবে গণনা করা হয়?'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function replaceInLanguage(lang, key, value) {
|
||||||
|
const langStart = new RegExp(`\\s{2}['\"]?${lang}['\"]?\\s*:\\s*\\{`);
|
||||||
|
const index = content.search(langStart);
|
||||||
|
if (index === -1) return;
|
||||||
|
const start = content.indexOf('{', index);
|
||||||
|
let depth = 1;
|
||||||
|
let i = start + 1;
|
||||||
|
while (i < content.length && depth > 0) {
|
||||||
|
if (content[i] === '{') depth++;
|
||||||
|
else if (content[i] === '}') depth--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
const block = content.slice(start + 1, i - 1);
|
||||||
|
let newBlock;
|
||||||
|
const keyRegex = new RegExp(`(['\"])${key}\\1\\s*:\\s*(['\"]).*?\\2`);
|
||||||
|
if (keyRegex.test(block)) {
|
||||||
|
newBlock = block.replace(keyRegex, `'${key}': '${value}'`);
|
||||||
|
} else {
|
||||||
|
newBlock = block.trim().endsWith(',') ? block + `\n '${key}': '${value}'` : block + `,\n '${key}': '${value}'`;
|
||||||
|
}
|
||||||
|
content = content.slice(0, start + 1) + newBlock + content.slice(i - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(keys).forEach(([key, translations]) => {
|
||||||
|
Object.entries(translations).forEach(([lang, value]) => {
|
||||||
|
replaceInLanguage(lang, key, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(path, content);
|
||||||
|
console.log('Translations updated.');
|
||||||
142
src/App.vue
@@ -8,6 +8,7 @@ import StatusPanel from './components/StatusPanel.vue';
|
|||||||
import GuidePanel from './components/GuidePanel.vue';
|
import GuidePanel from './components/GuidePanel.vue';
|
||||||
import WinModal from './components/WinModal.vue';
|
import WinModal from './components/WinModal.vue';
|
||||||
import CustomGameModal from './components/CustomGameModal.vue';
|
import CustomGameModal from './components/CustomGameModal.vue';
|
||||||
|
import ImageImportModal from './components/ImageImportModal.vue';
|
||||||
import SimulationView from './components/SimulationView.vue';
|
import SimulationView from './components/SimulationView.vue';
|
||||||
import FixedBar from './components/FixedBar.vue';
|
import FixedBar from './components/FixedBar.vue';
|
||||||
import ReloadPrompt from './components/ReloadPrompt.vue';
|
import ReloadPrompt from './components/ReloadPrompt.vue';
|
||||||
@@ -16,6 +17,7 @@ import ReloadPrompt from './components/ReloadPrompt.vue';
|
|||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
const { t, locale, setLocale, locales } = useI18n();
|
const { t, locale, setLocale, locales } = useI18n();
|
||||||
const showCustomModal = ref(false);
|
const showCustomModal = ref(false);
|
||||||
|
const showImageImportModal = ref(false);
|
||||||
const showSimulation = ref(false);
|
const showSimulation = ref(false);
|
||||||
const showGuide = ref(false);
|
const showGuide = ref(false);
|
||||||
const deferredPrompt = ref(null);
|
const deferredPrompt = ref(null);
|
||||||
@@ -23,6 +25,8 @@ const canInstall = ref(false);
|
|||||||
const installDismissed = ref(false);
|
const installDismissed = ref(false);
|
||||||
const isCoarsePointer = ref(false);
|
const isCoarsePointer = ref(false);
|
||||||
const isStandalone = ref(false);
|
const isStandalone = ref(false);
|
||||||
|
const isIos = ref(false);
|
||||||
|
const isDev = ref(false);
|
||||||
const themePreference = ref('system');
|
const themePreference = ref('system');
|
||||||
const appVersion = __APP_VERSION__;
|
const appVersion = __APP_VERSION__;
|
||||||
let displayModeMedia = null;
|
let displayModeMedia = null;
|
||||||
@@ -38,6 +42,10 @@ const onKeyDownGlobal = (e) => {
|
|||||||
showCustomModal.value = false;
|
showCustomModal.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (showImageImportModal.value) {
|
||||||
|
showImageImportModal.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (store.isGameWon) {
|
if (store.isGameWon) {
|
||||||
store.closeWinModal();
|
store.closeWinModal();
|
||||||
}
|
}
|
||||||
@@ -58,7 +66,7 @@ const updateStandalone = () => {
|
|||||||
const handleBeforeInstallPrompt = (e) => {
|
const handleBeforeInstallPrompt = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deferredPrompt.value = e;
|
deferredPrompt.value = e;
|
||||||
if (!isStandalone.value) {
|
if (!isStandalone.value && !isDev.value) {
|
||||||
canInstall.value = true;
|
canInstall.value = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -109,6 +117,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
isCoarsePointer.value = window.matchMedia('(pointer: coarse)').matches;
|
isCoarsePointer.value = window.matchMedia('(pointer: coarse)').matches;
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
isIos.value = /ipad|iphone|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1);
|
||||||
|
isDev.value = window.location.port !== '' && window.location.port !== '80' && window.location.port !== '443';
|
||||||
const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('theme') : null;
|
const storedTheme = typeof localStorage !== 'undefined' ? localStorage.getItem('theme') : null;
|
||||||
if (storedTheme === 'light' || storedTheme === 'dark' || storedTheme === 'system') {
|
if (storedTheme === 'light' || storedTheme === 'dark' || storedTheme === 'system') {
|
||||||
themePreference.value = storedTheme;
|
themePreference.value = storedTheme;
|
||||||
@@ -155,18 +166,30 @@ onUnmounted(() => {
|
|||||||
<main class="game-container">
|
<main class="game-container">
|
||||||
<NavBar
|
<NavBar
|
||||||
@open-custom="showCustomModal = true"
|
@open-custom="showCustomModal = true"
|
||||||
|
@open-image-import="showImageImportModal = true"
|
||||||
@toggle-guide="showGuide = !showGuide"
|
@toggle-guide="showGuide = !showGuide"
|
||||||
@set-theme="setThemePreference"
|
@set-theme="setThemePreference"
|
||||||
/>
|
/>
|
||||||
<FixedBar />
|
<FixedBar />
|
||||||
|
|
||||||
<div v-if="canInstall && !installDismissed" class="install-banner">
|
<div v-if="!isDev && (canInstall || (isIos && !isStandalone)) && !installDismissed" class="install-banner">
|
||||||
<div class="install-text">{{ t('pwa.installTitle') }}</div>
|
<div class="install-content">
|
||||||
|
<img src="/pwa-192x192.png" alt="App Icon" class="install-icon" />
|
||||||
|
<div class="install-text">
|
||||||
|
<div class="install-title">{{ t('app.title') }}</div>
|
||||||
|
<div class="install-desc">
|
||||||
|
<span v-if="isIos && !isStandalone">{{ t('pwa.installIos') }}</span>
|
||||||
|
<span v-else>{{ t('pwa.installTitle') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="install-actions">
|
<div class="install-actions">
|
||||||
<button class="btn-neon secondary install-btn" @click="handleInstall">
|
<button v-if="!isIos" class="btn-neon secondary install-btn" @click="handleInstall">
|
||||||
{{ installLabel }}
|
{{ installLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button class="install-close" @click="installDismissed = true">×</button>
|
<button class="install-close" @click="installDismissed = true" aria-label="Close">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -193,6 +216,10 @@ onUnmounted(() => {
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<WinModal v-if="store.isGameWon" />
|
<WinModal v-if="store.isGameWon" />
|
||||||
<CustomGameModal v-if="showCustomModal" @close="showCustomModal = false" @open-simulation="showSimulation = true" />
|
<CustomGameModal v-if="showCustomModal" @close="showCustomModal = false" @open-simulation="showSimulation = true" />
|
||||||
|
<ImageImportModal
|
||||||
|
v-if="showImageImportModal"
|
||||||
|
@close="showImageImportModal = false"
|
||||||
|
/>
|
||||||
<SimulationView v-if="showSimulation" @close="showSimulation = false" />
|
<SimulationView v-if="showSimulation" @close="showSimulation = false" />
|
||||||
<ReloadPrompt />
|
<ReloadPrompt />
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -203,7 +230,7 @@ onUnmounted(() => {
|
|||||||
.game-container {
|
.game-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch; /* was center */
|
align-items: stretch;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
@@ -211,38 +238,111 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.install-banner {
|
.install-banner {
|
||||||
background: var(--banner-bg);
|
position: fixed;
|
||||||
border: 1px solid var(--banner-border);
|
bottom: 0;
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
width: 90%;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto 20px auto; /* Center it manually */
|
|
||||||
box-shadow: var(--banner-shadow);
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
width: 100%;
|
||||||
|
background: var(--panel-bg);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
border-top: 1px solid var(--neon-cyan);
|
||||||
|
box-shadow: 0 -4px 30px rgba(0, 242, 254, 0.15);
|
||||||
|
padding: 16px 20px;
|
||||||
|
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
z-index: 2000;
|
||||||
|
animation: slideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.install-banner {
|
||||||
|
width: 400px;
|
||||||
|
bottom: 24px;
|
||||||
|
right: 24px;
|
||||||
|
left: auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--neon-cyan);
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-text {
|
.install-text {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-title {
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--neon-cyan);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-actions {
|
.install-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-close {
|
.install-close {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-close:hover {
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-layout {
|
.game-layout {
|
||||||
|
|||||||
@@ -43,21 +43,30 @@ const handlePointerDown = (e) => {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (now - lastTap < 300) {
|
if (now - lastTap < 300) {
|
||||||
// Double tap -> X (Force)
|
// Double tap -> X (Force)
|
||||||
|
clearLongPress();
|
||||||
emit('start-drag', props.r, props.c, true, true);
|
emit('start-drag', props.r, props.c, true, true);
|
||||||
lastTap = 0;
|
lastTap = 0;
|
||||||
} else {
|
} else {
|
||||||
// Single tap / Start drag -> Fill
|
// Single tap / Start drag -> Fill
|
||||||
emit('start-drag', props.r, props.c, false, false);
|
emit('start-drag', props.r, props.c, false, false);
|
||||||
lastTap = now;
|
lastTap = now;
|
||||||
|
|
||||||
|
// Start Long Press Timer
|
||||||
|
clearLongPress();
|
||||||
|
longPressTimer = setTimeout(() => {
|
||||||
|
if (navigator.vibrate) navigator.vibrate(50);
|
||||||
|
// Switch to Cross (Right click logic, force=true to overwrite the just-placed Fill)
|
||||||
|
emit('start-drag', props.r, props.c, true, true);
|
||||||
|
}, 500);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerUp = (e) => {
|
const handlePointerUp = (e) => {
|
||||||
// Handled in pointerdown
|
clearLongPress();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePointerCancel = (e) => {
|
const handlePointerCancel = (e) => {
|
||||||
// Handled in pointerdown
|
clearLongPress();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -84,6 +93,7 @@ const handlePointerCancel = (e) => {
|
|||||||
height: var(--cell-size);
|
height: var(--cell-size);
|
||||||
background-color: var(--cell-empty);
|
background-color: var(--cell-empty);
|
||||||
border: 1px solid var(--glass-border);
|
border: 1px solid var(--glass-border);
|
||||||
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -340,12 +340,20 @@ const confirm = () => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
border: 1px solid var(--accent-cyan);
|
border: 1px solid var(--accent-cyan);
|
||||||
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
|
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
|
||||||
animation: slideUp 0.3s ease;
|
animation: slideUp 0.3s ease;
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px), (max-height: 600px) {
|
||||||
|
.modal {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ const store = usePuzzleStore();
|
|||||||
const { rowHints, colHints } = useHints(computed(() => store.solution));
|
const { rowHints, colHints } = useHints(computed(() => store.solution));
|
||||||
const { startDrag, onMouseEnter, stopDrag } = useNonogram();
|
const { startDrag, onMouseEnter, stopDrag } = useNonogram();
|
||||||
|
|
||||||
|
// Compute grid dimensions from hints
|
||||||
|
const gridRows = computed(() => rowHints.value.length);
|
||||||
|
const gridCols = computed(() => colHints.value.length);
|
||||||
|
|
||||||
const cellSize = ref(30);
|
const cellSize = ref(30);
|
||||||
const rowHintsRef = ref(null);
|
const rowHintsRef = ref(null);
|
||||||
const activeRow = ref(null);
|
const activeRow = ref(null);
|
||||||
@@ -27,17 +31,28 @@ let dragStartLeft = 0;
|
|||||||
const checkScroll = () => {
|
const checkScroll = () => {
|
||||||
const el = scrollWrapper.value;
|
const el = scrollWrapper.value;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
|
const content = el.firstElementChild;
|
||||||
|
const contentWidth = content ? content.offsetWidth : el.scrollWidth;
|
||||||
const sw = el.scrollWidth;
|
const sw = el.scrollWidth;
|
||||||
const cw = el.clientWidth;
|
const cw = el.clientWidth;
|
||||||
|
|
||||||
// Only show custom scrollbar on mobile/tablet (width < 768px) and if content overflows
|
// Only show custom scrollbar on mobile/tablet (width < 768px) and if content overflows
|
||||||
const isMobile = window.innerWidth <= 768;
|
const isMobile = window.innerWidth <= 768;
|
||||||
showScrollbar.value = isMobile && (sw > cw + 1);
|
// Use contentWidth to check for overflow, as scrollWidth might be misleading
|
||||||
|
showScrollbar.value = isMobile && (contentWidth > cw + 1);
|
||||||
|
|
||||||
if (showScrollbar.value) {
|
if (showScrollbar.value) {
|
||||||
// Thumb width percentage = (viewport / total) * 100
|
// Thumb width percentage = (viewport / total) * 100
|
||||||
const ratio = cw / sw;
|
// Use contentWidth for more accurate ratio
|
||||||
|
const ratio = cw / contentWidth;
|
||||||
thumbWidth.value = Math.max(10, ratio * 100);
|
thumbWidth.value = Math.max(10, ratio * 100);
|
||||||
|
|
||||||
|
// Hide if content fits or almost fits (prevent useless scrollbar)
|
||||||
|
// Increased tolerance to 95%
|
||||||
|
if (ratio >= 0.95) {
|
||||||
|
showScrollbar.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,13 +147,16 @@ const computeCellSize = () => {
|
|||||||
// Ensure we don't have negative space
|
// Ensure we don't have negative space
|
||||||
const availableForGrid = Math.max(0, containerWidth - hintWidth);
|
const availableForGrid = Math.max(0, containerWidth - hintWidth);
|
||||||
|
|
||||||
const size = Math.floor((availableForGrid - gridPad * 2 - (store.size - 1) * gap) / store.size);
|
// Calculate cell size based on width availability (columns)
|
||||||
|
// Vertical scrolling is acceptable, so we don't constrain by height (rows)
|
||||||
|
const cols = Math.max(1, gridCols.value);
|
||||||
|
const size = Math.floor((availableForGrid - gridPad * 2 - (cols - 1) * gap) / cols);
|
||||||
|
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
// Desktop: Allow overflow, use comfortable size
|
// Desktop: Allow overflow, use comfortable size
|
||||||
cellSize.value = 30;
|
cellSize.value = 30;
|
||||||
} else {
|
} else {
|
||||||
// Mobile: Fit to screen
|
// Mobile: Fit to screen width
|
||||||
// Keep min 18, max 36
|
// Keep min 18, max 36
|
||||||
cellSize.value = Math.max(18, Math.min(36, size));
|
cellSize.value = Math.max(18, Math.min(36, size));
|
||||||
}
|
}
|
||||||
@@ -175,21 +193,38 @@ const handleGridLeave = () => {
|
|||||||
activeCol.value = null;
|
activeCol.value = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
computeCellSize();
|
||||||
|
checkScroll();
|
||||||
|
// Re-check after potential layout animation/transition
|
||||||
|
setTimeout(() => {
|
||||||
|
computeCellSize();
|
||||||
|
checkScroll();
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
computeCellSize();
|
computeCellSize();
|
||||||
|
checkScroll();
|
||||||
|
// Extra check for slow layout/font loading or orientation changes
|
||||||
|
setTimeout(() => {
|
||||||
|
computeCellSize();
|
||||||
|
checkScroll();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
isFinePointer.value = window.matchMedia('(pointer: fine)').matches;
|
isFinePointer.value = window.matchMedia('(pointer: fine)').matches;
|
||||||
window.addEventListener('resize', computeCellSize);
|
|
||||||
window.addEventListener('resize', checkScroll);
|
window.addEventListener('resize', handleResize);
|
||||||
|
window.addEventListener('orientationchange', handleResize);
|
||||||
window.addEventListener('mouseup', handleGlobalMouseUp);
|
window.addEventListener('mouseup', handleGlobalMouseUp);
|
||||||
window.addEventListener('pointerup', handleGlobalPointerUp);
|
window.addEventListener('pointerup', handleGlobalPointerUp);
|
||||||
window.addEventListener('touchend', handleGlobalPointerUp, { passive: true });
|
window.addEventListener('touchend', handleGlobalPointerUp, { passive: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', computeCellSize);
|
window.removeEventListener('resize', handleResize);
|
||||||
window.removeEventListener('resize', checkScroll);
|
window.removeEventListener('orientationchange', handleResize);
|
||||||
window.removeEventListener('mouseup', handleGlobalMouseUp);
|
window.removeEventListener('mouseup', handleGlobalMouseUp);
|
||||||
window.removeEventListener('pointerup', handleGlobalPointerUp);
|
window.removeEventListener('pointerup', handleGlobalPointerUp);
|
||||||
window.removeEventListener('touchend', handleGlobalPointerUp);
|
window.removeEventListener('touchend', handleGlobalPointerUp);
|
||||||
@@ -199,6 +234,10 @@ watch(() => store.size, async () => {
|
|||||||
await nextTick();
|
await nextTick();
|
||||||
computeCellSize();
|
computeCellSize();
|
||||||
checkScroll();
|
checkScroll();
|
||||||
|
setTimeout(() => {
|
||||||
|
computeCellSize();
|
||||||
|
checkScroll();
|
||||||
|
}, 300);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -208,17 +247,17 @@ watch(() => store.size, async () => {
|
|||||||
<div class="corner-spacer"></div>
|
<div class="corner-spacer"></div>
|
||||||
|
|
||||||
<!-- Column Hints -->
|
<!-- Column Hints -->
|
||||||
<Hints :hints="colHints" orientation="col" :size="store.size" :activeIndex="activeCol" />
|
<Hints :hints="colHints" orientation="col" :size="gridCols" :activeIndex="activeCol" />
|
||||||
|
|
||||||
<!-- Row Hints -->
|
<!-- Row Hints -->
|
||||||
<Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="store.size" :activeIndex="activeRow" />
|
<Hints ref="rowHintsRef" :hints="rowHints" orientation="row" :size="gridRows" :activeIndex="activeRow" />
|
||||||
|
|
||||||
<!-- Grid -->
|
<!-- Grid -->
|
||||||
<div
|
<div
|
||||||
class="grid"
|
class="grid"
|
||||||
:style="{
|
:style="{
|
||||||
gridTemplateColumns: `repeat(${store.size}, var(--cell-size))`,
|
gridTemplateColumns: `repeat(${gridCols}, var(--cell-size))`,
|
||||||
gridTemplateRows: `repeat(${store.size}, var(--cell-size))`
|
gridTemplateRows: `repeat(${gridRows}, var(--cell-size))`
|
||||||
}"
|
}"
|
||||||
@pointermove.prevent="handlePointerMove"
|
@pointermove.prevent="handlePointerMove"
|
||||||
@mouseleave="handleGridLeave"
|
@mouseleave="handleGridLeave"
|
||||||
@@ -231,8 +270,8 @@ watch(() => store.size, async () => {
|
|||||||
:r="r"
|
:r="r"
|
||||||
:c="c"
|
:c="c"
|
||||||
:class="{
|
:class="{
|
||||||
'guide-right': (c + 1) % 5 === 0 && c !== store.size - 1,
|
'guide-right': (c + 1) % 5 === 0 && c !== gridCols - 1,
|
||||||
'guide-bottom': (r + 1) % 5 === 0 && r !== store.size - 1
|
'guide-bottom': (r + 1) % 5 === 0 && r !== gridRows - 1
|
||||||
}"
|
}"
|
||||||
@start-drag="startDrag"
|
@start-drag="startDrag"
|
||||||
@enter-cell="handleCellEnter"
|
@enter-cell="handleCellEnter"
|
||||||
@@ -272,6 +311,7 @@ watch(() => store.size, async () => {
|
|||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin: 0 auto; /* Center the wrapper safely */
|
margin: 0 auto; /* Center the wrapper safely */
|
||||||
align-items: flex-start; /* Prevent cropping when centered */
|
align-items: flex-start; /* Prevent cropping when centered */
|
||||||
|
padding-right: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-container {
|
.game-container {
|
||||||
|
|||||||
@@ -32,7 +32,11 @@ defineProps({
|
|||||||
v-for="(group, index) in hints"
|
v-for="(group, index) in hints"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="hint-group"
|
class="hint-group"
|
||||||
:class="{ 'is-active': index === activeIndex }"
|
:class="{
|
||||||
|
'is-active': index === activeIndex,
|
||||||
|
'guide-right': orientation === 'col' && (index + 1) % 5 === 0 && index !== size - 1,
|
||||||
|
'guide-bottom': orientation === 'row' && (index + 1) % 5 === 0 && index !== size - 1
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-for="(num, idx) in group"
|
v-for="(num, idx) in group"
|
||||||
@@ -54,13 +58,13 @@ defineProps({
|
|||||||
|
|
||||||
.hints-container.col {
|
.hints-container.col {
|
||||||
padding-bottom: var(--grid-padding);
|
padding-bottom: var(--grid-padding);
|
||||||
align-items: flex-end;
|
/* align-items: flex-end; - Removed to ensure uniform column height */
|
||||||
padding-left: var(--grid-padding);
|
padding-left: var(--grid-padding);
|
||||||
padding-right: var(--grid-padding);
|
padding-right: var(--grid-padding);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hints-container.row {
|
.hints-container.row {
|
||||||
align-items: flex-end;
|
/* align-items: flex-end; - Removed to ensure row hints fill the cell height */
|
||||||
padding: var(--grid-padding) var(--grid-padding) var(--grid-padding) 0;
|
padding: var(--grid-padding) var(--grid-padding) var(--grid-padding) 0;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
@@ -95,6 +99,21 @@ defineProps({
|
|||||||
padding: 2px;
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hint-num {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col .hint-group {
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .hint-group {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Alternating Colors within the group */
|
/* Alternating Colors within the group */
|
||||||
.hint-num.hint-alt {
|
.hint-num.hint-alt {
|
||||||
color: var(--accent-cyan);
|
color: var(--accent-cyan);
|
||||||
@@ -111,4 +130,12 @@ defineProps({
|
|||||||
border-color: rgba(79, 172, 254, 0.8);
|
border-color: rgba(79, 172, 254, 0.8);
|
||||||
box-shadow: 0 0 12px rgba(79, 172, 254, 0.35);
|
box-shadow: 0 0 12px rgba(79, 172, 254, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Guide lines every 5 */
|
||||||
|
.hint-group.guide-right {
|
||||||
|
border-right: 2px solid rgba(0, 242, 255, 0.5);
|
||||||
|
}
|
||||||
|
.hint-group.guide-bottom {
|
||||||
|
border-bottom: 2px solid rgba(0, 242, 255, 0.5);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
859
src/components/ImageImportModal.vue
Normal file
@@ -0,0 +1,859 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||||
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { getWorkerPool } from '@/utils/workerPool';
|
||||||
|
import { Upload, Image as ImageIcon, X, AlertTriangle, Camera, RefreshCw } from 'lucide-vue-next';
|
||||||
|
|
||||||
|
const emit = defineEmits(['close']);
|
||||||
|
const store = usePuzzleStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const fileInput = ref(null);
|
||||||
|
const canvasRef = ref(null);
|
||||||
|
const previewCanvasRef = ref(null);
|
||||||
|
const videoRef = ref(null);
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const imageLoaded = ref(false);
|
||||||
|
const processing = ref(false);
|
||||||
|
const processingProgress = ref(0);
|
||||||
|
const isCameraOpen = ref(false);
|
||||||
|
const hasMultipleCameras = ref(false);
|
||||||
|
const stream = ref(null);
|
||||||
|
const facingMode = ref('environment');
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
const maxDimension = ref(15);
|
||||||
|
const threshold = ref(50); // 0-100% density
|
||||||
|
|
||||||
|
// State
|
||||||
|
const originalImage = ref(null);
|
||||||
|
const generatedGrid = ref([]);
|
||||||
|
const difficulty = ref(0);
|
||||||
|
const solvability = ref(0);
|
||||||
|
const difficultyLabel = ref('');
|
||||||
|
const gridRows = ref(15);
|
||||||
|
const gridCols = ref(15);
|
||||||
|
|
||||||
|
const onDrop = (e) => {
|
||||||
|
isDragging.value = false;
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
processFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFileSelect = (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
processFile(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const processFile = (file) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
originalImage.value = img;
|
||||||
|
imageLoaded.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
updateGrid();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
img.src = e.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateGrid = () => {
|
||||||
|
if (!originalImage.value || !canvasRef.value) return;
|
||||||
|
|
||||||
|
const ctx = canvasRef.value.getContext('2d');
|
||||||
|
|
||||||
|
// Calculate dimensions preserving aspect ratio
|
||||||
|
const imgW = originalImage.value.width;
|
||||||
|
const imgH = originalImage.value.height;
|
||||||
|
const aspect = imgW / imgH;
|
||||||
|
|
||||||
|
let w, h;
|
||||||
|
const maxDim = maxDimension.value;
|
||||||
|
|
||||||
|
if (aspect >= 1) {
|
||||||
|
w = maxDim;
|
||||||
|
h = Math.round(w / aspect);
|
||||||
|
} else {
|
||||||
|
h = maxDim;
|
||||||
|
w = Math.round(h * aspect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure bounds [5, 80]
|
||||||
|
// 1. Clamp min (prioritize min 5)
|
||||||
|
if (w < 5) { w = 5; h = Math.round(w / aspect); }
|
||||||
|
if (h < 5) { h = 5; w = Math.round(h * aspect); }
|
||||||
|
|
||||||
|
// 2. Clamp max (hard limit 80)
|
||||||
|
if (w > 80) w = 80;
|
||||||
|
if (h > 80) h = 80;
|
||||||
|
|
||||||
|
// Final safeguard
|
||||||
|
w = Math.max(5, Math.min(80, Math.round(w)));
|
||||||
|
h = Math.max(5, Math.min(80, Math.round(h)));
|
||||||
|
|
||||||
|
gridRows.value = h;
|
||||||
|
gridCols.value = w;
|
||||||
|
|
||||||
|
// Resize logic
|
||||||
|
canvasRef.value.width = w;
|
||||||
|
canvasRef.value.height = h;
|
||||||
|
|
||||||
|
// Draw image scaled to grid size
|
||||||
|
ctx.drawImage(originalImage.value, 0, 0, w, h);
|
||||||
|
|
||||||
|
// Get pixel data
|
||||||
|
const imageData = ctx.getImageData(0, 0, w, h);
|
||||||
|
const data = imageData.data;
|
||||||
|
|
||||||
|
// 1. Collect brightness of valid pixels
|
||||||
|
const pixels = [];
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const i = (y * w + x) * 4;
|
||||||
|
const r = data[i];
|
||||||
|
const g = data[i + 1];
|
||||||
|
const b = data[i + 2];
|
||||||
|
const a = data[i + 3];
|
||||||
|
|
||||||
|
// If transparent, ignore
|
||||||
|
if (a >= 128) {
|
||||||
|
// Calculate brightness (Luminance)
|
||||||
|
const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
|
||||||
|
pixels.push({ x, y, brightness });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Sort by brightness (ascending: dark -> light)
|
||||||
|
// Darker pixels have lower brightness.
|
||||||
|
// We want to fill the darkest X% pixels.
|
||||||
|
pixels.sort((a, b) => a.brightness - b.brightness);
|
||||||
|
|
||||||
|
// 3. Determine cutoff
|
||||||
|
// threshold.value is density (0-100%)
|
||||||
|
// If density is 50%, we want 50% of pixels to be black.
|
||||||
|
// So we take the first 50% of sorted pixels.
|
||||||
|
const totalValid = pixels.length;
|
||||||
|
const targetFillCount = Math.floor(totalValid * (threshold.value / 100));
|
||||||
|
|
||||||
|
// 4. Build grid
|
||||||
|
const grid = Array(h).fill().map(() => Array(w).fill(0));
|
||||||
|
|
||||||
|
// Mark the darkest pixels as filled (1)
|
||||||
|
for (let i = 0; i < targetFillCount; i++) {
|
||||||
|
const p = pixels[i];
|
||||||
|
grid[p.y][p.x] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
generatedGrid.value = grid;
|
||||||
|
drawPreview();
|
||||||
|
calculateStats();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawPreview = () => {
|
||||||
|
if (!previewCanvasRef.value || generatedGrid.value.length === 0) return;
|
||||||
|
const ctx = previewCanvasRef.value.getContext('2d');
|
||||||
|
const rows = generatedGrid.value.length;
|
||||||
|
const cols = generatedGrid.value[0].length;
|
||||||
|
|
||||||
|
// Max 300px width/height, preserving aspect
|
||||||
|
const maxPreview = 300;
|
||||||
|
const cellW = maxPreview / Math.max(rows, cols);
|
||||||
|
const cellH = cellW; // square cells
|
||||||
|
|
||||||
|
const previewW = cols * cellW;
|
||||||
|
const previewH = rows * cellH;
|
||||||
|
|
||||||
|
previewCanvasRef.value.width = previewW;
|
||||||
|
previewCanvasRef.value.height = previewH;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, previewW, previewH);
|
||||||
|
|
||||||
|
// Draw original image with low opacity
|
||||||
|
if (originalImage.value) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = 0.3;
|
||||||
|
ctx.drawImage(originalImage.value, 0, 0, previewW, previewH);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = 0; y < rows; y++) {
|
||||||
|
for (let x = 0; x < cols; x++) {
|
||||||
|
if (generatedGrid.value[y][x] === 1) {
|
||||||
|
ctx.fillStyle = '#000';
|
||||||
|
ctx.fillRect(x * cellW, y * cellH, cellW, cellH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grid lines
|
||||||
|
ctx.strokeStyle = '#ccc';
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
|
||||||
|
// Vertical lines
|
||||||
|
for (let i = 0; i <= cols; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(i * cellW, 0);
|
||||||
|
ctx.lineTo(i * cellW, previewH);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal lines
|
||||||
|
for (let i = 0; i <= rows; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, i * cellH);
|
||||||
|
ctx.lineTo(previewW, i * cellH);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateStats = async () => {
|
||||||
|
if (generatedGrid.value.length === 0) return;
|
||||||
|
|
||||||
|
processing.value = true;
|
||||||
|
processingProgress.value = 0;
|
||||||
|
const requestId = Date.now();
|
||||||
|
currentStatsRequestId = requestId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pool = getWorkerPool();
|
||||||
|
pool.cancelAll(); // Force stop previous calculations for immediate responsiveness
|
||||||
|
|
||||||
|
// Demonstrate parallel execution capability
|
||||||
|
// We split the problem into two branches: assuming first cell is EMPTY (0) vs FILLED (1)
|
||||||
|
// This doubles the search power by utilizing 2 workers immediately.
|
||||||
|
|
||||||
|
// Ensure we send plain objects to workers, not Vue proxies
|
||||||
|
const rawGrid = JSON.parse(JSON.stringify(generatedGrid.value));
|
||||||
|
const rows = rawGrid.length;
|
||||||
|
const cols = rawGrid[0].length;
|
||||||
|
|
||||||
|
// Create initial states
|
||||||
|
const gridA = Array(rows).fill().map(() => Array(cols).fill(-1));
|
||||||
|
gridA[0][0] = 0; // Branch A: Assume (0,0) is Empty
|
||||||
|
|
||||||
|
const gridB = Array(rows).fill().map(() => Array(cols).fill(-1));
|
||||||
|
gridB[0][0] = 1; // Branch B: Assume (0,0) is Filled
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
{
|
||||||
|
data: { id: requestId, grid: rawGrid, initialGrid: gridA },
|
||||||
|
onProgress: (p) => { if (currentStatsRequestId === requestId) processingProgress.value = Math.max(processingProgress.value, p); }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: { id: requestId, grid: rawGrid, initialGrid: gridB },
|
||||||
|
onProgress: (p) => { if (currentStatsRequestId === requestId) processingProgress.value = Math.max(processingProgress.value, p); }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await pool.runRace(tasks);
|
||||||
|
|
||||||
|
if (result.id === currentStatsRequestId) {
|
||||||
|
solvability.value = result.solvability;
|
||||||
|
difficulty.value = result.difficulty;
|
||||||
|
difficultyLabel.value = result.difficultyLabel;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err.message !== 'Cancelled' && err.message !== 'Terminated') {
|
||||||
|
console.error('Worker error:', err);
|
||||||
|
if (currentStatsRequestId === requestId) {
|
||||||
|
solvability.value = 0;
|
||||||
|
difficulty.value = 0;
|
||||||
|
// If translation key is missing, this might show 'difficulty.error'
|
||||||
|
// Ensure we have a fallback or the key exists
|
||||||
|
difficultyLabel.value = 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (currentStatsRequestId === requestId) {
|
||||||
|
processing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentStatsRequestId = 0;
|
||||||
|
|
||||||
|
let debounceTimer;
|
||||||
|
watch([maxDimension, threshold], () => {
|
||||||
|
if (imageLoaded.value) {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
updateGrid();
|
||||||
|
}, 50);
|
||||||
|
} else {
|
||||||
|
// If no image loaded, just update the display values
|
||||||
|
// Assuming square aspect ratio for preview
|
||||||
|
gridRows.value = maxDimension.value;
|
||||||
|
gridCols.value = maxDimension.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const createPuzzle = () => {
|
||||||
|
if (generatedGrid.value.length > 0) {
|
||||||
|
store.initFromImage(generatedGrid.value);
|
||||||
|
emit('close');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerFileInput = () => {
|
||||||
|
fileInput.value.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const startCamera = async () => {
|
||||||
|
isCameraOpen.value = true;
|
||||||
|
try {
|
||||||
|
if (stream.value) {
|
||||||
|
stopCameraStream();
|
||||||
|
}
|
||||||
|
const constraints = {
|
||||||
|
video: {
|
||||||
|
facingMode: facingMode.value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
stream.value = await navigator.mediaDevices.getUserMedia(constraints);
|
||||||
|
|
||||||
|
// Check available devices
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
||||||
|
hasMultipleCameras.value = videoDevices.length > 1;
|
||||||
|
|
||||||
|
// Wait for next tick or ensure videoRef is available
|
||||||
|
setTimeout(() => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.srcObject = stream.value;
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error accessing camera:", err);
|
||||||
|
alert(t('image.cameraError'));
|
||||||
|
isCameraOpen.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopCameraStream = () => {
|
||||||
|
if (stream.value) {
|
||||||
|
stream.value.getTracks().forEach(track => track.stop());
|
||||||
|
stream.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeCamera = () => {
|
||||||
|
stopCameraStream();
|
||||||
|
isCameraOpen.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const switchCamera = async () => {
|
||||||
|
facingMode.value = facingMode.value === 'user' ? 'environment' : 'user';
|
||||||
|
await startCamera();
|
||||||
|
};
|
||||||
|
|
||||||
|
const capturePhoto = () => {
|
||||||
|
if (!videoRef.value) return;
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = videoRef.value.videoWidth;
|
||||||
|
canvas.height = videoRef.value.videoHeight;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Mirror if using front camera
|
||||||
|
if (facingMode.value === 'user') {
|
||||||
|
ctx.translate(canvas.width, 0);
|
||||||
|
ctx.scale(-1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(videoRef.value, 0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
originalImage.value = img;
|
||||||
|
imageLoaded.value = true;
|
||||||
|
nextTick(() => {
|
||||||
|
updateGrid();
|
||||||
|
});
|
||||||
|
closeCamera();
|
||||||
|
};
|
||||||
|
img.src = canvas.toDataURL('image/png');
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopCameraStream();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="modal-overlay" @click.self="$emit('close')">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button class="close-btn" @click="$emit('close')">
|
||||||
|
<X :size="24" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h2 class="modal-title">{{ t('image.title') }}</h2>
|
||||||
|
|
||||||
|
<div v-if="isCameraOpen" class="camera-overlay">
|
||||||
|
<video ref="videoRef" autoplay playsinline></video>
|
||||||
|
<div class="camera-controls">
|
||||||
|
<button class="camera-btn secondary" @click="closeCamera">
|
||||||
|
<X :size="24" />
|
||||||
|
</button>
|
||||||
|
<button class="camera-btn capture" @click="capturePhoto">
|
||||||
|
<div class="shutter"></div>
|
||||||
|
</button>
|
||||||
|
<button v-if="hasMultipleCameras" class="camera-btn secondary" @click="switchCamera">
|
||||||
|
<RefreshCw :size="24" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-grid">
|
||||||
|
<!-- Left: Input & Preview -->
|
||||||
|
<div class="input-section">
|
||||||
|
<div
|
||||||
|
class="drop-zone"
|
||||||
|
:class="{ dragging: isDragging, loaded: imageLoaded }"
|
||||||
|
@dragover.prevent="isDragging = true"
|
||||||
|
@dragleave.prevent="isDragging = false"
|
||||||
|
@drop.prevent="onDrop"
|
||||||
|
@click="!imageLoaded ? triggerFileInput() : null"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden-input"
|
||||||
|
@change="onFileSelect"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="!imageLoaded" class="placeholder">
|
||||||
|
<Upload :size="48" class="icon" />
|
||||||
|
<p>{{ t('image.drop') }}</p>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn-neon small" @click.stop="triggerFileInput">
|
||||||
|
{{ t('image.select') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn-neon small secondary" @click.stop="startCamera">
|
||||||
|
<Camera :size="16" />
|
||||||
|
{{ t('image.camera') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="preview-container">
|
||||||
|
<canvas ref="previewCanvasRef" class="preview-canvas"></canvas>
|
||||||
|
<div class="button-group">
|
||||||
|
<button class="btn-change" @click.stop="triggerFileInput">
|
||||||
|
{{ t('image.change') }}
|
||||||
|
</button>
|
||||||
|
<button class="btn-change" @click.stop="startCamera">
|
||||||
|
<Camera :size="16" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden canvas for processing -->
|
||||||
|
<canvas ref="canvasRef" style="display: none;"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Controls & Stats -->
|
||||||
|
<div class="controls-section">
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{ t('image.size') }}: {{ gridCols }}x{{ gridRows }}</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model.number="maxDimension"
|
||||||
|
min="5"
|
||||||
|
max="80"
|
||||||
|
step="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label>{{ t('image.threshold') }}: {{ threshold }}%</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
v-model.number="threshold"
|
||||||
|
min="1"
|
||||||
|
max="99"
|
||||||
|
/>
|
||||||
|
<div class="threshold-preview">
|
||||||
|
<span>Low Density</span>
|
||||||
|
<span>High Density</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="imageLoaded" class="stats-panel">
|
||||||
|
<div v-if="processing" class="loading-stats">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<span>{{ t('image.calculatingSolvability') || 'Calculating solvability...' }} {{ processingProgress }}%</span>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>{{ t('image.solvability') }}:</span>
|
||||||
|
<span :class="{ 'good': solvability === 100, 'bad': solvability < 100 }">
|
||||||
|
{{ solvability }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="solvability < 100" class="warning">
|
||||||
|
<AlertTriangle :size="16" />
|
||||||
|
{{ t('image.warning') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-row">
|
||||||
|
<span>{{ t('image.difficulty') }}:</span>
|
||||||
|
<span>{{ difficulty }}% ({{ t(`difficulty.${difficultyLabel.toLowerCase()}`) }})</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button
|
||||||
|
class="btn-neon primary"
|
||||||
|
:disabled="!imageLoaded || processing"
|
||||||
|
@click="createPuzzle"
|
||||||
|
>
|
||||||
|
{{ t('image.create') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--text-muted);
|
||||||
|
border-top-color: var(--primary-accent);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--panel-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 30px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||||
|
color: var(--text-color);
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: var(--primary-accent);
|
||||||
|
font-size: 1.8rem;
|
||||||
|
text-shadow: 0 0 10px var(--primary-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-overlay video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-btn.capture {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
background: rgba(255,255,255,0.3);
|
||||||
|
border: 4px solid white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shutter {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-btn.capture:active .shutter {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone:hover, .drop-zone.dragging {
|
||||||
|
border-color: var(--primary-accent);
|
||||||
|
background: rgba(0, 242, 254, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.loaded {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container {
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 300px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-container .button-group {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
box-shadow: 0 0 15px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-change {
|
||||||
|
background: rgba(0,0,0,0.7);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-change:hover {
|
||||||
|
background: rgba(0,0,0,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
accent-color: var(--primary-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.threshold-preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-panel {
|
||||||
|
background: rgba(0,0,0,0.2);
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.good { color: #4ade80; }
|
||||||
|
.bad { color: #f87171; }
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: -5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neon {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 1px solid var(--primary-accent);
|
||||||
|
background: rgba(0, 242, 254, 0.1);
|
||||||
|
color: var(--primary-accent);
|
||||||
|
font-family: 'Orbitron', sans-serif;
|
||||||
|
text-transform: uppercase;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 242, 254, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neon:hover:not(:disabled) {
|
||||||
|
background: var(--primary-accent);
|
||||||
|
color: #000;
|
||||||
|
box-shadow: 0 0 20px var(--primary-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neon:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-neon.small {
|
||||||
|
padding: 5px 15px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { Gamepad2, Palette, CircleHelp, Sun, Moon, Menu, X, ChevronDown, ChevronUp, Monitor } from 'lucide-vue-next';
|
import { Gamepad2, Palette, CircleHelp, Sun, Moon, Menu, X, ChevronDown, ChevronUp, Monitor, Image as ImageIcon, Sparkles, Shuffle, Grid3X3, Grid2X2, Grid, MousePointer2 } from 'lucide-vue-next';
|
||||||
|
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
const { t, locale, setLocale, locales } = useI18n();
|
const { t, locale, setLocale, locales } = useI18n();
|
||||||
|
|
||||||
const emit = defineEmits(['open-custom', 'toggle-guide', 'set-theme']);
|
const emit = defineEmits(['open-custom', 'open-image-import', 'toggle-guide', 'set-theme']);
|
||||||
|
|
||||||
const isGameOpen = ref(false);
|
const isGameOpen = ref(false);
|
||||||
const isThemeOpen = ref(false);
|
const isThemeOpen = ref(false);
|
||||||
@@ -16,6 +16,13 @@ const isMobileMenuOpen = ref(false);
|
|||||||
const langMenuRef = ref(null);
|
const langMenuRef = ref(null);
|
||||||
const searchTerm = ref('');
|
const searchTerm = ref('');
|
||||||
|
|
||||||
|
const getLevelIcon = (id) => {
|
||||||
|
if (id === 'easy') return Grid2X2;
|
||||||
|
if (id === 'medium') return Grid3X3;
|
||||||
|
if (id === 'hard') return Grid;
|
||||||
|
return Gamepad2;
|
||||||
|
};
|
||||||
|
|
||||||
// Map language codes to country codes for flag-icons
|
// Map language codes to country codes for flag-icons
|
||||||
const langToCountry = {
|
const langToCountry = {
|
||||||
en: 'gb',
|
en: 'gb',
|
||||||
@@ -180,6 +187,12 @@ const openCustom = () => {
|
|||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openImageImport = () => {
|
||||||
|
emit('open-image-import');
|
||||||
|
isGameOpen.value = false;
|
||||||
|
closeMobileMenu();
|
||||||
|
};
|
||||||
|
|
||||||
const setTheme = (theme) => {
|
const setTheme = (theme) => {
|
||||||
emit('set-theme', theme);
|
emit('set-theme', theme);
|
||||||
isThemeOpen.value = false;
|
isThemeOpen.value = false;
|
||||||
@@ -251,10 +264,16 @@ watch(isMobileMenuOpen, (val) => {
|
|||||||
class="dropdown-item"
|
class="dropdown-item"
|
||||||
@click="selectLevel(lvl.id)"
|
@click="selectLevel(lvl.id)"
|
||||||
>
|
>
|
||||||
|
<component :is="getLevelIcon(lvl.id)" :size="16" />
|
||||||
{{ lvl.label }}
|
{{ lvl.label }}
|
||||||
</button>
|
</button>
|
||||||
<button class="dropdown-item" @click="openCustom">
|
<button class="dropdown-item" @click="openCustom">
|
||||||
{{ t('level.custom') }}
|
<Shuffle :size="16" />
|
||||||
|
{{ t('level.custom_random') }}
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" @click="openImageImport">
|
||||||
|
<ImageIcon :size="16" />
|
||||||
|
{{ t('level.custom_image') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
@@ -321,8 +340,10 @@ watch(isMobileMenuOpen, (val) => {
|
|||||||
</transition>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<!-- Mobile Menu Overlay -->
|
<!-- Mobile Menu Overlay -->
|
||||||
|
<Teleport to="body">
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="isMobileMenuOpen" class="mobile-menu-overlay">
|
<div v-if="isMobileMenuOpen" class="mobile-menu-overlay">
|
||||||
<div class="mobile-menu-header">
|
<div class="mobile-menu-header">
|
||||||
@@ -346,10 +367,16 @@ watch(isMobileMenuOpen, (val) => {
|
|||||||
class="mobile-sub-item"
|
class="mobile-sub-item"
|
||||||
@click="selectLevel(lvl.id)"
|
@click="selectLevel(lvl.id)"
|
||||||
>
|
>
|
||||||
|
<component :is="getLevelIcon(lvl.id)" :size="16" />
|
||||||
{{ lvl.label }}
|
{{ lvl.label }}
|
||||||
</button>
|
</button>
|
||||||
<button class="mobile-sub-item" @click="openCustom">
|
<button class="mobile-sub-item" @click="openCustom">
|
||||||
{{ t('level.custom') }}
|
<Shuffle :size="16" />
|
||||||
|
{{ t('level.custom_random') }}
|
||||||
|
</button>
|
||||||
|
<button class="mobile-sub-item" @click="openImageImport">
|
||||||
|
<ImageIcon :size="16" />
|
||||||
|
{{ t('level.custom_image') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -402,7 +429,7 @@ watch(isMobileMenuOpen, (val) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="lang-list mobile-lang-list">
|
<div class="lang-list mobile-lang-list">
|
||||||
<button
|
<button
|
||||||
v-for="lang in filteredLanguages"
|
v-for="lang in filteredLanguages"
|
||||||
:key="lang.code"
|
:key="lang.code"
|
||||||
class="mobile-sub-item"
|
class="mobile-sub-item"
|
||||||
@@ -420,7 +447,7 @@ watch(isMobileMenuOpen, (val) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</nav>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import { X, Play, Square, RotateCcw } from 'lucide-vue-next';
|
|||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const SIZES = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50];
|
const SIZES = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80];
|
||||||
const DENSITIES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
const DENSITIES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
||||||
const SAMPLES_PER_POINT = 10; // Reduced for web performance demo
|
const SAMPLES_PER_POINT = 50; // Increased for better accuracy
|
||||||
|
|
||||||
const isRunning = ref(false);
|
const isRunning = ref(false);
|
||||||
const progress = ref(0);
|
const progress = ref(0);
|
||||||
@@ -71,11 +71,12 @@ const startSimulation = async () => {
|
|||||||
for (let i = 0; i < SAMPLES_PER_POINT; i++) {
|
for (let i = 0; i < SAMPLES_PER_POINT; i++) {
|
||||||
const grid = generateRandomGrid(size, density);
|
const grid = generateRandomGrid(size, density);
|
||||||
const { rowHints, colHints } = calculateHints(grid);
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
const { percentSolved } = solvePuzzle(rowHints, colHints);
|
// Use logicOnly=true for fast simulation
|
||||||
|
const { percentSolved } = solvePuzzle(rowHints, colHints, null, null, true);
|
||||||
totalSolved += percentSolved;
|
totalSolved += percentSolved;
|
||||||
|
|
||||||
// Yield to UI every few samples to keep it responsive
|
// Yield to UI every few samples to keep it responsive
|
||||||
if (i % 2 === 0) await new Promise(r => setTimeout(r, 0));
|
if (i % 10 === 0) await new Promise(r => setTimeout(r, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
const avgSolved = totalSolved / SAMPLES_PER_POINT;
|
const avgSolved = totalSolved / SAMPLES_PER_POINT;
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ onUnmounted(() => {
|
|||||||
width: fit-content;
|
width: fit-content;
|
||||||
max-width: min(92vw, 560px);
|
max-width: min(92vw, 560px);
|
||||||
min-width: 280px;
|
min-width: 280px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
border: 1px solid var(--primary-accent);
|
border: 1px solid var(--primary-accent);
|
||||||
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
|
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
|
||||||
animation: slideUp 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
animation: slideUp 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
@@ -223,6 +225,12 @@ onUnmounted(() => {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px), (max-height: 600px) {
|
||||||
|
.modal {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
color: var(--primary-accent);
|
color: var(--primary-accent);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export function useSolver() {
|
|||||||
const isPlaying = ref(false);
|
const isPlaying = ref(false);
|
||||||
const isProcessing = ref(false);
|
const isProcessing = ref(false);
|
||||||
const speedIndex = ref(0);
|
const speedIndex = ref(0);
|
||||||
const speeds = [1000, 500, 250, 125, 62];
|
const speeds = [1000, 500, 250, 125, 62, 31, 16];
|
||||||
const speedLabels = ['x1', 'x2', 'x4', 'x8', 'x16'];
|
const speedLabels = ['x1', 'x2', 'x4', 'x8', 'x16', 'x32', 'x64'];
|
||||||
const statusText = ref(t('guide.waiting'));
|
const statusText = ref(t('guide.waiting'));
|
||||||
|
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
|
|||||||
@@ -32,12 +32,15 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
let count = 0;
|
let count = 0;
|
||||||
if (solution.value.length === 0 || playerGrid.value.length === 0) return 0;
|
if (solution.value.length === 0 || playerGrid.value.length === 0) return 0;
|
||||||
|
|
||||||
for (let r = 0; r < size.value; r++) {
|
const rows = solution.value.length;
|
||||||
for (let c = 0; c < size.value; c++) {
|
const cols = solution.value[0].length;
|
||||||
|
|
||||||
|
for (let r = 0; r < rows; r++) {
|
||||||
|
for (let c = 0; c < cols; c++) {
|
||||||
// Zliczamy tylko poprawne wypełnienia (czarne),
|
// Zliczamy tylko poprawne wypełnienia (czarne),
|
||||||
// ale w nonogramach postęp to często: (poprawne_czarne - bledne_czarne) / total_czarne
|
// ale w nonogramach postęp to często: (poprawne_czarne - bledne_czarne) / total_czarne
|
||||||
// Zróbmy prostą wersję: % poprawnie zaznaczonych czarnych - błędnie zaznaczone czarne
|
// Zróbmy prostą wersję: % poprawnie zaznaczonych czarnych - błędnie zaznaczone czarne
|
||||||
if (playerGrid.value[r][c] === 1) {
|
if (playerGrid.value[r] && playerGrid.value[r][c] === 1) {
|
||||||
if (solution.value[r][c] === 1) count++;
|
if (solution.value[r][c] === 1) count++;
|
||||||
else count--; // kara za błąd
|
else count--; // kara za błąd
|
||||||
}
|
}
|
||||||
@@ -93,10 +96,36 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
saveState();
|
saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initFromImage(grid) {
|
||||||
|
stopTimer();
|
||||||
|
currentLevelId.value = 'custom_image';
|
||||||
|
// Use the larger dimension for size to ensure loops cover everything if square-assumption exists
|
||||||
|
// But ideally we should support rectangular.
|
||||||
|
// For now, size.value is used in resetGrid loop.
|
||||||
|
// Let's update resetGrid to handle rectangular.
|
||||||
|
size.value = Math.max(grid.length, grid[0].length);
|
||||||
|
solution.value = grid;
|
||||||
|
|
||||||
|
resetGrid();
|
||||||
|
isGameWon.value = false;
|
||||||
|
hasUsedGuide.value = false;
|
||||||
|
guideUsageCount.value = 0;
|
||||||
|
|
||||||
|
// Calculate density
|
||||||
|
const totalFilled = grid.flat().filter(c => c === 1).length;
|
||||||
|
currentDensity.value = totalFilled / (size.value * size.value);
|
||||||
|
|
||||||
|
elapsedTime.value = 0;
|
||||||
|
startTimer();
|
||||||
|
saveState();
|
||||||
|
}
|
||||||
|
|
||||||
function resetGrid() {
|
function resetGrid() {
|
||||||
playerGrid.value = Array(size.value).fill().map(() => Array(size.value).fill(0));
|
const rows = solution.value.length;
|
||||||
moves.value = 0;
|
const cols = solution.value[0].length;
|
||||||
|
playerGrid.value = Array(rows).fill().map(() => Array(cols).fill(0));
|
||||||
history.value = [];
|
history.value = [];
|
||||||
|
moves.value = 0;
|
||||||
currentTransaction.value = null;
|
currentTransaction.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,11 +227,14 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
|
|
||||||
// Calculate expected hints from solution (truth)
|
// Calculate expected hints from solution (truth)
|
||||||
// We do this dynamically to ensure we always check against the rules of the board
|
// We do this dynamically to ensure we always check against the rules of the board
|
||||||
|
const rows = solution.value.length;
|
||||||
|
const cols = solution.value[0].length;
|
||||||
|
|
||||||
const solutionRows = solution.value;
|
const solutionRows = solution.value;
|
||||||
const solutionCols = Array(size.value).fill().map((_, c) => solution.value.map(r => r[c]));
|
const solutionCols = Array(cols).fill().map((_, c) => solution.value.map(r => r[c]));
|
||||||
|
|
||||||
// Check Rows
|
// Check Rows
|
||||||
for (let r = 0; r < size.value; r++) {
|
for (let r = 0; r < rows; r++) {
|
||||||
const targetHints = calculateLineHints(solutionRows[r]);
|
const targetHints = calculateLineHints(solutionRows[r]);
|
||||||
const playerLine = playerGrid.value[r];
|
const playerLine = playerGrid.value[r];
|
||||||
if (!validateLine(playerLine, targetHints)) {
|
if (!validateLine(playerLine, targetHints)) {
|
||||||
@@ -213,7 +245,7 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
|
|
||||||
if (correct) {
|
if (correct) {
|
||||||
// Check Columns
|
// Check Columns
|
||||||
for (let c = 0; c < size.value; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const targetHints = calculateLineHints(solutionCols[c]);
|
const targetHints = calculateLineHints(solutionCols[c]);
|
||||||
const playerLine = playerGrid.value.map(row => row[c]);
|
const playerLine = playerGrid.value.map(row => row[c]);
|
||||||
if (!validateLine(playerLine, targetHints)) {
|
if (!validateLine(playerLine, targetHints)) {
|
||||||
@@ -332,6 +364,7 @@ export const usePuzzleStore = defineStore('puzzle', () => {
|
|||||||
progressPercentage,
|
progressPercentage,
|
||||||
initGame,
|
initGame,
|
||||||
initCustomGame,
|
initCustomGame,
|
||||||
|
initFromImage,
|
||||||
toggleCell,
|
toggleCell,
|
||||||
setCell,
|
setCell,
|
||||||
resetGame,
|
resetGame,
|
||||||
|
|||||||
27
src/utils/debug_solver.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { solvePuzzle } from './solver';
|
||||||
|
import { calculateHints } from './puzzleUtils';
|
||||||
|
|
||||||
|
describe('Debug Solver', () => {
|
||||||
|
it('should solve the broken grid', () => {
|
||||||
|
const grid = [
|
||||||
|
[0,1,1,1,0,0,1,0,1,1],
|
||||||
|
[1,1,1,0,0,1,1,1,0,0],
|
||||||
|
[1,0,1,0,1,0,0,1,0,0],
|
||||||
|
[1,0,0,0,1,1,1,1,0,1],
|
||||||
|
[1,1,0,1,0,0,0,1,0,1],
|
||||||
|
[1,0,1,0,1,0,0,0,1,0],
|
||||||
|
[1,1,1,0,0,1,1,0,0,0],
|
||||||
|
[0,1,0,0,1,0,1,0,0,0],
|
||||||
|
[0,0,0,1,1,0,0,0,1,0],
|
||||||
|
[1,0,1,1,0,0,1,0,1,1]
|
||||||
|
];
|
||||||
|
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
|
console.log('Solve Result:', result);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
44
src/utils/large_grid_solver.test.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { solvePuzzle } from './solver.js';
|
||||||
|
|
||||||
|
describe('Large Grid Solver', () => {
|
||||||
|
it('should solve a large 55x28 grid without crashing', () => {
|
||||||
|
const rows = 28;
|
||||||
|
const cols = 55;
|
||||||
|
// Create a simple pattern: checkerboard or lines
|
||||||
|
const grid = Array(rows).fill().map((_, r) =>
|
||||||
|
Array(cols).fill().map((_, c) => (r + c) % 2 === 0 ? 1 : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate hints
|
||||||
|
const rowHints = grid.map(row => {
|
||||||
|
const hints = [];
|
||||||
|
let current = 0;
|
||||||
|
row.forEach(cell => {
|
||||||
|
if (cell === 1) current++;
|
||||||
|
else if (current > 0) { hints.push(current); current = 0; }
|
||||||
|
});
|
||||||
|
if (current > 0) hints.push(current);
|
||||||
|
return hints.length ? hints : [0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const colHints = Array(cols).fill().map((_, c) => {
|
||||||
|
const hints = [];
|
||||||
|
let current = 0;
|
||||||
|
for(let r=0; r<rows; r++) {
|
||||||
|
if (grid[r][c] === 1) current++;
|
||||||
|
else if (current > 0) { hints.push(current); current = 0; }
|
||||||
|
}
|
||||||
|
if (current > 0) hints.push(current);
|
||||||
|
return hints.length ? hints : [0];
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Starting solve...');
|
||||||
|
const result = solvePuzzle(rowHints, colHints, (p) => console.log(`Progress: ${p}%`));
|
||||||
|
console.log('Result:', result);
|
||||||
|
|
||||||
|
expect(result.percentSolved).toBeGreaterThan(0);
|
||||||
|
expect(result.difficulty).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,19 +27,20 @@ export function validateLine(line, targetHints) {
|
|||||||
export function calculateHints(grid) {
|
export function calculateHints(grid) {
|
||||||
if (!grid || grid.length === 0) return { rowHints: [], colHints: [] };
|
if (!grid || grid.length === 0) return { rowHints: [], colHints: [] };
|
||||||
|
|
||||||
const size = grid.length;
|
const rows = grid.length;
|
||||||
|
const cols = grid[0].length;
|
||||||
const rowHints = [];
|
const rowHints = [];
|
||||||
const colHints = [];
|
const colHints = [];
|
||||||
|
|
||||||
// Row Hints
|
// Row Hints
|
||||||
for (let r = 0; r < size; r++) {
|
for (let r = 0; r < rows; r++) {
|
||||||
rowHints.push(calculateLineHints(grid[r]));
|
rowHints.push(calculateLineHints(grid[r]));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Col Hints
|
// Col Hints
|
||||||
for (let c = 0; c < size; c++) {
|
for (let c = 0; c < cols; c++) {
|
||||||
const col = [];
|
const col = [];
|
||||||
for (let r = 0; r < size; r++) {
|
for (let r = 0; r < rows; r++) {
|
||||||
col.push(grid[r][c]);
|
col.push(grid[r][c]);
|
||||||
}
|
}
|
||||||
colHints.push(calculateLineHints(col));
|
colHints.push(calculateLineHints(col));
|
||||||
@@ -63,21 +64,23 @@ export function generateRandomGrid(size, density = 0.5) {
|
|||||||
export function calculateDifficulty(density, size = 10) {
|
export function calculateDifficulty(density, size = 10) {
|
||||||
// Data derived from Monte Carlo Simulation (Logical Solver)
|
// Data derived from Monte Carlo Simulation (Logical Solver)
|
||||||
// Format: { size: [solved_pct_at_0.1, ..., solved_pct_at_0.9] }
|
// Format: { size: [solved_pct_at_0.1, ..., solved_pct_at_0.9] }
|
||||||
// Densities: 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9
|
|
||||||
const SIM_DATA = {
|
const SIM_DATA = {
|
||||||
5: [89, 74, 74, 81, 97, 98, 99, 100, 100],
|
5: [88, 76, 71, 80, 90, 98, 99, 100, 100],
|
||||||
10: [57, 20, 16, 54, 92, 100, 100, 100, 100],
|
10: [58, 25, 18, 44, 81, 99, 100, 100, 100],
|
||||||
15: [37, 10, 2, 12, 68, 100, 100, 100, 100],
|
15: [36, 7, 3, 11, 67, 99, 100, 100, 100],
|
||||||
20: [23, 3, 1, 2, 37, 100, 100, 100, 100],
|
20: [24, 3, 0, 3, 48, 99, 100, 100, 100],
|
||||||
25: [16, 0, 0, 1, 19, 99, 100, 100, 100],
|
25: [13, 1, 0, 1, 21, 99, 100, 100, 100],
|
||||||
30: [8, 0, 0, 0, 5, 99, 100, 100, 100],
|
30: [9, 0, 0, 0, 7, 99, 100, 100, 100],
|
||||||
35: [6, 0, 0, 0, 4, 91, 100, 100, 100],
|
35: [5, 0, 0, 0, 5, 97, 100, 100, 100],
|
||||||
40: [3, 0, 0, 0, 2, 91, 100, 100, 100],
|
40: [3, 0, 0, 0, 2, 91, 100, 100, 100],
|
||||||
45: [2, 0, 0, 0, 1, 82, 100, 100, 100],
|
45: [2, 0, 0, 0, 1, 84, 100, 100, 100],
|
||||||
50: [2, 0, 0, 0, 1, 73, 100, 100, 100],
|
50: [1, 0, 0, 0, 0, 65, 100, 100, 100],
|
||||||
|
55: [1, 0, 0, 0, 0, 55, 100, 100, 100],
|
||||||
60: [0, 0, 0, 0, 0, 35, 100, 100, 100],
|
60: [0, 0, 0, 0, 0, 35, 100, 100, 100],
|
||||||
71: [0, 0, 0, 0, 0, 16, 100, 100, 100],
|
65: [0, 0, 0, 0, 0, 20, 100, 100, 100],
|
||||||
80: [0, 0, 0, 0, 0, 1, 100, 100, 100]
|
70: [0, 0, 0, 0, 0, 11, 100, 100, 100],
|
||||||
|
75: [0, 0, 0, 0, 0, 12, 100, 100, 100],
|
||||||
|
80: [0, 0, 0, 0, 0, 4, 100, 100, 100]
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to get interpolated value from array
|
// Helper to get interpolated value from array
|
||||||
@@ -122,17 +125,41 @@ export function calculateDifficulty(density, size = 10) {
|
|||||||
|
|
||||||
const solvedPct = getSimulatedSolvedPct(size, density);
|
const solvedPct = getSimulatedSolvedPct(size, density);
|
||||||
|
|
||||||
// Difficulty Score: Inverse of Solved Percent
|
let value;
|
||||||
// 100% Solved -> 0 Difficulty
|
let level;
|
||||||
// 0% Solved -> 100 Difficulty
|
|
||||||
const value = Math.round(100 - solvedPct);
|
|
||||||
|
|
||||||
// Thresholds
|
// "Hardest" threshold is 99% solvability.
|
||||||
let level = 'easy';
|
// We calculate a base value first, then adjust for solvability.
|
||||||
if (value >= 90) level = 'extreme'; // < 10% Solved
|
|
||||||
else if (value >= 60) level = 'hardest'; // < 40% Solved
|
|
||||||
else if (value >= 30) level = 'harder'; // < 70% Solved
|
|
||||||
else level = 'easy'; // > 70% Solved
|
|
||||||
|
|
||||||
return { level, value };
|
const densityFactor = 1 - 2 * Math.abs(density - 0.5);
|
||||||
|
const complexity = size * (0.4 + 0.6 * densityFactor);
|
||||||
|
|
||||||
|
if (solvedPct < 99) {
|
||||||
|
// Requires guessing / advanced logic.
|
||||||
|
// Base penalty for low solvability: 85 to 100
|
||||||
|
const penaltyBase = 85 + ((99 - solvedPct) / 99) * 15;
|
||||||
|
|
||||||
|
// Scale penalty by size.
|
||||||
|
// Small grids (e.g. 5x5) are trivial even if "unsolvable" by simple logic.
|
||||||
|
// Large grids (e.g. 20x20) are truly extreme if unsolvable.
|
||||||
|
const sizeFactor = Math.min(1, size / 20);
|
||||||
|
|
||||||
|
value = penaltyBase * sizeFactor;
|
||||||
|
|
||||||
|
// Ensure difficulty doesn't drop below structural complexity
|
||||||
|
value = Math.max(value, complexity);
|
||||||
|
} else {
|
||||||
|
// Solvable (>= 99%)
|
||||||
|
// Complexity based on Size and Density
|
||||||
|
// Max size 80.
|
||||||
|
// Formula: size * (0.4 + 0.6 * densityFactor)
|
||||||
|
value = Math.min(85, complexity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value < 25) level = 'easy';
|
||||||
|
else if (value < 55) level = 'harder';
|
||||||
|
else if (value < 85) level = 'hardest';
|
||||||
|
else level = 'extreme';
|
||||||
|
|
||||||
|
return { level, value: Math.round(value) };
|
||||||
}
|
}
|
||||||
|
|||||||
49
src/utils/repro_solver.test.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { solvePuzzle } from './solver';
|
||||||
|
import { calculateHints } from './puzzleUtils';
|
||||||
|
|
||||||
|
describe('Solver Repro', () => {
|
||||||
|
it('should solve a simple generated puzzle', () => {
|
||||||
|
const grid = [
|
||||||
|
[1, 0, 1, 1, 0],
|
||||||
|
[1, 1, 0, 0, 1],
|
||||||
|
[0, 0, 1, 0, 0],
|
||||||
|
[1, 1, 1, 1, 1],
|
||||||
|
[0, 1, 0, 1, 0]
|
||||||
|
];
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not fail on random valid lines', () => {
|
||||||
|
// Test solveLine indirectly via solvePuzzle on small grids
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const size = 10;
|
||||||
|
const grid = [];
|
||||||
|
for(let r=0; r<size; r++) {
|
||||||
|
const row = [];
|
||||||
|
for(let c=0; c<size; c++) row.push(Math.random() > 0.5 ? 1 : 0);
|
||||||
|
grid.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
|
// It might not be 100% solvable without guessing (logic only),
|
||||||
|
// but since our solver HAS backtracking, it MUST be 100% solvable
|
||||||
|
// (unless timeout/max depth reached, but for 10x10 it should solve).
|
||||||
|
|
||||||
|
// If it returns 0% or low %, it implies it failed to find the solution
|
||||||
|
// or found a contradiction (which shouldn't happen for valid hints).
|
||||||
|
|
||||||
|
if (result.percentSolved < 100) {
|
||||||
|
console.log('Failed Grid:', JSON.stringify(grid));
|
||||||
|
console.log('Result:', result);
|
||||||
|
}
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,7 @@ export function buildShareCanvas(data, t, formattedTime) {
|
|||||||
const { grid, size, currentDensity, guideUsageCount } = data;
|
const { grid, size, currentDensity, guideUsageCount } = data;
|
||||||
if (!grid || !grid.length) return null;
|
if (!grid || !grid.length) return null;
|
||||||
|
|
||||||
const appUrl = 'https://nonograms.7u.pl/';
|
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
||||||
const maxBoard = 640;
|
const maxBoard = 640;
|
||||||
const cellSize = Math.max(8, Math.floor(maxBoard / size));
|
const cellSize = Math.max(8, Math.floor(maxBoard / size));
|
||||||
const boardSize = cellSize * size;
|
const boardSize = cellSize * size;
|
||||||
@@ -119,7 +119,7 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
const { grid, size, currentDensity, guideUsageCount } = data;
|
const { grid, size, currentDensity, guideUsageCount } = data;
|
||||||
if (!grid || !grid.length) return null;
|
if (!grid || !grid.length) return null;
|
||||||
|
|
||||||
const appUrl = 'https://nonograms.7u.pl/';
|
const appUrl = typeof __APP_HOMEPAGE__ !== 'undefined' ? __APP_HOMEPAGE__ : '';
|
||||||
const maxBoard = 640;
|
const maxBoard = 640;
|
||||||
const cellSize = Math.max(8, Math.floor(maxBoard / size));
|
const cellSize = Math.max(8, Math.floor(maxBoard / size));
|
||||||
const boardSize = cellSize * size;
|
const boardSize = cellSize * size;
|
||||||
@@ -227,7 +227,11 @@ export function buildShareSVG(data, t, formattedTime) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// URL
|
// URL
|
||||||
svgContent += `<text x="${padding}" y="${height - padding + 6}" font-family="Segoe UI, sans-serif" font-weight="500" font-size="14" fill="${urlColor}">${appUrl}</text>`;
|
svgContent += `
|
||||||
|
<a href="${appUrl}" target="_blank">
|
||||||
|
<text x="${padding}" y="${height - padding + 6}" font-family="Segoe UI, sans-serif" font-weight="500" font-size="14" fill="${urlColor}" style="text-decoration: underline; cursor: pointer;">${appUrl}</text>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
|
||||||
svgContent += '</svg>';
|
svgContent += '</svg>';
|
||||||
return svgContent;
|
return svgContent;
|
||||||
|
|||||||
@@ -6,6 +6,9 @@
|
|||||||
* 1: Filled
|
* 1: Filled
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Memoized helper for checking if hints fit
|
||||||
|
const memo = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Solves a single line (row or column) based on hints and current knowledge.
|
* Solves a single line (row or column) based on hints and current knowledge.
|
||||||
* Uses the "Left-Right Overlap" algorithm to find common filled cells.
|
* Uses the "Left-Right Overlap" algorithm to find common filled cells.
|
||||||
@@ -15,10 +18,13 @@
|
|||||||
* @param {number[]} hints - Array of block lengths
|
* @param {number[]} hints - Array of block lengths
|
||||||
* @returns {number[]} - Updated line (or null if contradiction/impossible - though shouldn't happen for valid puzzles)
|
* @returns {number[]} - Updated line (or null if contradiction/impossible - though shouldn't happen for valid puzzles)
|
||||||
*/
|
*/
|
||||||
function solveLine(currentLine, hints) {
|
export function solveLine(currentLine, hints) {
|
||||||
const length = currentLine.length;
|
const length = currentLine.length;
|
||||||
|
|
||||||
// If no hints, all must be empty
|
// If no hints, all must be empty
|
||||||
|
// Clear memo for this line solve
|
||||||
|
memo.clear();
|
||||||
|
|
||||||
if (hints.length === 0 || (hints.length === 1 && hints[0] === 0)) {
|
if (hints.length === 0 || (hints.length === 1 && hints[0] === 0)) {
|
||||||
return Array(length).fill(0);
|
return Array(length).fill(0);
|
||||||
}
|
}
|
||||||
@@ -45,28 +51,23 @@ function solveLine(currentLine, hints) {
|
|||||||
while (currentIdx <= length - block) {
|
while (currentIdx <= length - block) {
|
||||||
if (canPlace(currentLine, currentIdx, block)) {
|
if (canPlace(currentLine, currentIdx, block)) {
|
||||||
// Verify we can fit remaining blocks
|
// Verify we can fit remaining blocks
|
||||||
// Simple heuristic: do we have enough space?
|
|
||||||
// A full recursive check is better but slower.
|
|
||||||
// For "Logical Solver" we assume valid placement is possible if we respect current constraints.
|
|
||||||
// However, strictly, we need to know if there is *any* valid arrangement starting here.
|
|
||||||
// Let's use a recursive check with memoization for "can fit rest".
|
|
||||||
if (canFitRest(currentLine, currentIdx + block + 1, hints, hIndex + 1)) {
|
if (canFitRest(currentLine, currentIdx + block + 1, hints, hIndex + 1)) {
|
||||||
leftPositions.push(currentIdx);
|
leftPositions.push(currentIdx);
|
||||||
currentIdx += block + 1; // Move past this block + 1 space
|
currentIdx += block + 1; // Move past this block + 1 space
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Cannot skip a filled cell - if we pass a '1', it becomes uncovered
|
||||||
|
if (currentLine[currentIdx] === 1) return null;
|
||||||
currentIdx++;
|
currentIdx++;
|
||||||
}
|
}
|
||||||
if (leftPositions.length <= hIndex) return null; // Impossible
|
if (leftPositions.length <= hIndex) return null; // Impossible
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Calculate Right-Most Positions (by reversing line and hints)
|
// Clear memo for right-side calculation (different line/hints)
|
||||||
// This is symmetrical to Left-Most.
|
memo.clear();
|
||||||
// Instead of implementing reverse logic, we can just reverse inputs, run left-most, and reverse back.
|
|
||||||
// But we need to respect the "currentLine" constraints which might be asymmetric.
|
|
||||||
|
|
||||||
// Actually, "Right-Most" is just "Left-Most" on the reversed grid.
|
// 2. Calculate Right-Most Positions (by reversing line and hints)
|
||||||
const reversedLine = [...currentLine].reverse();
|
const reversedLine = [...currentLine].reverse();
|
||||||
const reversedHints = [...hints].reverse();
|
const reversedHints = [...hints].reverse();
|
||||||
const rightPositionsReversed = [];
|
const rightPositionsReversed = [];
|
||||||
@@ -82,14 +83,14 @@ function solveLine(currentLine, hints) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Cannot skip a filled cell
|
||||||
|
if (reversedLine[currentIdx] === 1) return null;
|
||||||
currentIdx++;
|
currentIdx++;
|
||||||
}
|
}
|
||||||
if (rightPositionsReversed.length <= hIndex) return null;
|
if (rightPositionsReversed.length <= hIndex) return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert reversed positions to actual indices
|
// Convert reversed positions to actual indices
|
||||||
// index in reversed = length - 1 - (original_index + block_size - 1)
|
|
||||||
// original_start = length - 1 - (reversed_start + block_size - 1) = length - reversed_start - block_size
|
|
||||||
const rightPositions = rightPositionsReversed.map((rStart, i) => {
|
const rightPositions = rightPositionsReversed.map((rStart, i) => {
|
||||||
const block = reversedHints[i];
|
const block = reversedHints[i];
|
||||||
return length - rStart - block;
|
return length - rStart - block;
|
||||||
@@ -106,13 +107,6 @@ function solveLine(currentLine, hints) {
|
|||||||
const block = hints[i];
|
const block = hints[i];
|
||||||
|
|
||||||
// If overlap exists: [r, l + block - 1]
|
// If overlap exists: [r, l + block - 1]
|
||||||
// Example: Block 5. Left: 2, Right: 4.
|
|
||||||
// Left: ..XXXXX...
|
|
||||||
// Right: ....XXXXX.
|
|
||||||
// Overlap: ..XXX...
|
|
||||||
// Indices: max(l, r) to min(l+block, r+block) - 1 ?
|
|
||||||
// Range is [r, l + block - 1] (inclusive)
|
|
||||||
|
|
||||||
if (r < l + block) {
|
if (r < l + block) {
|
||||||
for (let k = r; k < l + block; k++) {
|
for (let k = r; k < l + block; k++) {
|
||||||
newLine[k] = 1;
|
newLine[k] = 1;
|
||||||
@@ -121,15 +115,6 @@ function solveLine(currentLine, hints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine Empty cells?
|
// Determine Empty cells?
|
||||||
// A cell is empty if it is not covered by ANY block in ANY valid configuration.
|
|
||||||
// This is harder with just L/R limits.
|
|
||||||
// However, we can use the "Simple Glue" logic:
|
|
||||||
// If a cell is outside the range [LeftLimit[i], RightLimit[i] + block] for ALL i, it's empty.
|
|
||||||
// Wait, indices are not strictly partitioned. Block 1 could be at 0 or 10.
|
|
||||||
// But logic dictates order.
|
|
||||||
// Range of block i is [LeftPositions[i], RightPositions[i] + hints[i]].
|
|
||||||
// If a cell k is not in ANY of these ranges, it is 0.
|
|
||||||
|
|
||||||
// Mask of possible filled cells
|
// Mask of possible filled cells
|
||||||
const possibleFilled = Array(length).fill(false);
|
const possibleFilled = Array(length).fill(false);
|
||||||
for (let i = 0; i < hints.length; i++) {
|
for (let i = 0; i < hints.length; i++) {
|
||||||
@@ -148,8 +133,7 @@ function solveLine(currentLine, hints) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Memoized helper for checking if hints fit
|
// Memoized helper for checking if hints fit
|
||||||
const memo = new Map();
|
export function canFitRest(line, startIndex, hints, hintIndex) {
|
||||||
function canFitRest(line, startIndex, hints, hintIndex) {
|
|
||||||
// Optimization: If hints are empty, we just need to check if remaining line has no '1's
|
// Optimization: If hints are empty, we just need to check if remaining line has no '1's
|
||||||
if (hintIndex >= hints.length) {
|
if (hintIndex >= hints.length) {
|
||||||
for (let i = startIndex; i < line.length; i++) {
|
for (let i = startIndex; i < line.length; i++) {
|
||||||
@@ -158,23 +142,32 @@ function canFitRest(line, startIndex, hints, hintIndex) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key for memoization (primitive approach)
|
// Memoization key
|
||||||
// In a full solver, we'd pass a cache. For single line, maybe overkill, but safe.
|
const key = `${startIndex}-${hintIndex}`;
|
||||||
// let key = `${startIndex}-${hintIndex}`;
|
if (memo.has(key)) return memo.get(key);
|
||||||
// Skipping memo for now as line lengths are small (<80) and recursion depth is low.
|
|
||||||
|
|
||||||
const remainingLen = line.length - startIndex;
|
const remainingLen = line.length - startIndex;
|
||||||
// Min space needed: sum of hints + (hints.length - 1) spaces
|
// Min space needed: sum of hints + (hints.length - 1) spaces
|
||||||
// Calculate lazily or precalc?
|
|
||||||
let minSpace = 0;
|
let minSpace = 0;
|
||||||
for(let i=hintIndex; i<hints.length; i++) minSpace += hints[i] + (i < hints.length - 1 ? 1 : 0);
|
for(let i=hintIndex; i<hints.length; i++) minSpace += hints[i] + (i < hints.length - 1 ? 1 : 0);
|
||||||
|
|
||||||
if (remainingLen < minSpace) return false;
|
if (remainingLen < minSpace) {
|
||||||
|
memo.set(key, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const block = hints[hintIndex];
|
const block = hints[hintIndex];
|
||||||
// Try to find *any* valid placement for this block
|
// Try to find *any* valid placement for this block
|
||||||
// We only need ONE valid path to return true.
|
for (let i = startIndex; i <= line.length - minSpace; i++) {
|
||||||
for (let i = startIndex; i <= line.length - minSpace; i++) { // Optimization on upper bound?
|
// If we skipped a '1', we went too far. All 1s must be covered by blocks.
|
||||||
|
// Since we are placing the *next* block, any 1s between startIndex and i are uncovered.
|
||||||
|
// Thus, if we find a 1 in [startIndex, i-1], we must stop.
|
||||||
|
let skippedOne = false;
|
||||||
|
for (let x = startIndex; x < i; x++) {
|
||||||
|
if (line[x] === 1) { skippedOne = true; break; }
|
||||||
|
}
|
||||||
|
if (skippedOne) break;
|
||||||
|
|
||||||
// Check placement
|
// Check placement
|
||||||
let valid = true;
|
let valid = true;
|
||||||
// Block
|
// Block
|
||||||
@@ -183,96 +176,276 @@ function canFitRest(line, startIndex, hints, hintIndex) {
|
|||||||
}
|
}
|
||||||
if (!valid) continue;
|
if (!valid) continue;
|
||||||
|
|
||||||
// Boundary before (checked by loop start usually, but strictly:
|
// Boundary before
|
||||||
if (i > 0 && line[i-1] === 1) valid = false; // Should have been handled by caller or skip
|
if (i > 0 && line[i-1] === 1) valid = false;
|
||||||
// Wait, the caller (loop) iterates i.
|
|
||||||
// If i > startIndex, we implied space at i-1.
|
|
||||||
// If line[i-1] is 1, we can't place here if we skipped it.
|
|
||||||
// Actually, if we skip a '1', that's invalid.
|
|
||||||
// So we can't just skip '1's.
|
|
||||||
|
|
||||||
// Correct logic:
|
// Boundary after (check implicit in next block placement or end of line, but we need to check i+block cell)
|
||||||
// We iterate i. If we pass a '1' at index < i, that 1 is orphaned -> Invalid path.
|
|
||||||
// So we can only scan forward as long as we don't skip a '1'.
|
|
||||||
|
|
||||||
let skippedOne = false;
|
|
||||||
for (let x = startIndex; x < i; x++) {
|
|
||||||
if (line[x] === 1) { skippedOne = true; break; }
|
|
||||||
}
|
|
||||||
if (skippedOne) break; // Cannot go further right, we left a 1 behind.
|
|
||||||
|
|
||||||
// Boundary after
|
|
||||||
if (i + block < line.length && line[i+block] === 1) valid = false;
|
if (i + block < line.length && line[i+block] === 1) valid = false;
|
||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
// Recurse
|
// Recurse
|
||||||
if (canFitRest(line, i + block + 1, hints, hintIndex + 1)) return true;
|
if (canFitRest(line, i + block + 1, hints, hintIndex + 1)) {
|
||||||
|
memo.set(key, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
memo.set(key, false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Solves the puzzle using logical iteration.
|
* Main solver function that attempts to solve the puzzle using logic and backtracking (DFS).
|
||||||
|
* Uses an efficient propagation queue and undo stack to avoid deep copying the grid.
|
||||||
|
*
|
||||||
* @param {number[][]} rowHints
|
* @param {number[][]} rowHints
|
||||||
* @param {number[][]} colHints
|
* @param {number[][]} colHints
|
||||||
* @returns {object} { solvedGrid: number[][], percentSolved: number }
|
* @param {function} onProgress - Optional callback for progress reporting (percent)
|
||||||
|
* @param {number[][]} initialGrid - Optional initial grid state
|
||||||
|
* @param {boolean} logicOnly - If true, stops after logical propagation (no guessing)
|
||||||
|
* @returns {object} result
|
||||||
*/
|
*/
|
||||||
export function solvePuzzle(rowHints, colHints) {
|
export function solvePuzzle(rowHints, colHints, onProgress, initialGrid = null, logicOnly = false) {
|
||||||
const rows = rowHints.length;
|
const rows = rowHints.length;
|
||||||
const cols = colHints.length;
|
const cols = colHints.length;
|
||||||
|
const totalCells = rows * cols;
|
||||||
|
|
||||||
// Initialize grid with -1
|
// Grid initialization: -1 (Unknown), 0 (Empty), 1 (Filled)
|
||||||
let grid = Array(rows).fill(null).map(() => Array(cols).fill(-1));
|
// Use initialGrid if provided (deep copy to be safe)
|
||||||
|
const grid = initialGrid
|
||||||
|
? initialGrid.map(row => [...row])
|
||||||
|
: Array(rows).fill().map(() => Array(cols).fill(-1));
|
||||||
|
|
||||||
let changed = true;
|
// Stats
|
||||||
let iterations = 0;
|
let iterations = 0; // Total calls to solve()
|
||||||
const MAX_ITER = 100; // Safety break
|
let maxDepth = 0; // Max recursion depth
|
||||||
|
let backtracks = 0; // Failed guesses
|
||||||
|
let logicSteps = 0; // Cells filled by propagation
|
||||||
|
let lastProgressTime = 0;
|
||||||
|
|
||||||
while (changed && iterations < MAX_ITER) {
|
function reportProgress() {
|
||||||
changed = false;
|
if (!onProgress) return;
|
||||||
iterations++;
|
const now = Date.now();
|
||||||
|
if (now - lastProgressTime >= 50) {
|
||||||
// Rows
|
let filled = 0;
|
||||||
for (let r = 0; r < rows; r++) {
|
for(let r=0; r<rows; r++) {
|
||||||
const newLine = solveLine(grid[r], rowHints[r]);
|
for(let c=0; c<cols; c++) {
|
||||||
if (newLine) {
|
if(grid[r][c] !== -1) filled++;
|
||||||
for (let c = 0; c < cols; c++) {
|
|
||||||
if (grid[r][c] !== newLine[c]) {
|
|
||||||
grid[r][c] = newLine[c];
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cols
|
|
||||||
for (let c = 0; c < cols; c++) {
|
|
||||||
const currentCol = grid.map(row => row[c]);
|
|
||||||
const newCol = solveLine(currentCol, colHints[c]);
|
|
||||||
if (newCol) {
|
|
||||||
for (let r = 0; r < rows; r++) {
|
|
||||||
if (grid[r][c] !== newCol[r]) {
|
|
||||||
grid[r][c] = newCol[r];
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onProgress(Math.floor((filled/totalCells) * 100));
|
||||||
|
lastProgressTime = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate solved %
|
// Queue for propagation (Set of strings "r{i}" or "c{i}")
|
||||||
|
const queue = new Set();
|
||||||
|
for(let r=0; r<rows; r++) queue.add(`r${r}`);
|
||||||
|
for(let c=0; c<cols; c++) queue.add(`c${c}`);
|
||||||
|
|
||||||
|
// Helper: Undo changes from a propagation step
|
||||||
|
function undo(changes) {
|
||||||
|
for(let i=changes.length-1; i>=0; i--) {
|
||||||
|
const {r, c, old} = changes[i];
|
||||||
|
grid[r][c] = old;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Propagate logic constraints until fixed point or contradiction
|
||||||
|
// Returns list of changes made, or null if contradiction found
|
||||||
|
function propagate() {
|
||||||
|
const changes = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
while(queue.size > 0) {
|
||||||
|
reportProgress();
|
||||||
|
|
||||||
|
const item = queue.values().next().value;
|
||||||
|
queue.delete(item);
|
||||||
|
|
||||||
|
const type = item[0];
|
||||||
|
const idx = parseInt(item.slice(1));
|
||||||
|
|
||||||
|
let currentLine, hints;
|
||||||
|
if (type === 'r') {
|
||||||
|
currentLine = grid[idx]; // Reference for row (fast)
|
||||||
|
hints = rowHints[idx];
|
||||||
|
} else {
|
||||||
|
currentLine = grid.map(row => row[idx]); // Copy for col (slower)
|
||||||
|
hints = colHints[idx];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLine = solveLine(currentLine, hints);
|
||||||
|
|
||||||
|
if (!newLine) throw 'contradiction';
|
||||||
|
|
||||||
|
// Apply changes
|
||||||
|
for(let i=0; i<newLine.length; i++) {
|
||||||
|
if (currentLine[i] !== newLine[i]) {
|
||||||
|
// If we try to change a known value to something else -> Contradiction
|
||||||
|
if (currentLine[i] !== -1 && currentLine[i] !== newLine[i]) throw 'contradiction';
|
||||||
|
|
||||||
|
if (currentLine[i] === -1) {
|
||||||
|
const r = type === 'r' ? idx : i;
|
||||||
|
const c = type === 'r' ? i : idx;
|
||||||
|
|
||||||
|
// Double check if already set by orthogonal update in same loop?
|
||||||
|
// (Should be handled by -1 check above, as grid is shared)
|
||||||
|
if (grid[r][c] === -1) {
|
||||||
|
grid[r][c] = newLine[i];
|
||||||
|
changes.push({r, c, old: -1});
|
||||||
|
logicSteps++;
|
||||||
|
|
||||||
|
// Add orthogonal line to queue
|
||||||
|
if (type === 'r') queue.add(`c${c}`);
|
||||||
|
else queue.add(`r${r}`);
|
||||||
|
} else if (grid[r][c] !== newLine[i]) {
|
||||||
|
console.log('Contradiction: Orthogonal Conflict at', r, c, 'Grid:', grid[r][c], 'New:', newLine[i]);
|
||||||
|
throw 'contradiction';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Revert changes from this failed propagation
|
||||||
|
undo(changes);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DFS Solver
|
||||||
|
function solve(depth) {
|
||||||
|
maxDepth = Math.max(maxDepth, depth);
|
||||||
|
iterations++;
|
||||||
|
|
||||||
|
reportProgress();
|
||||||
|
|
||||||
|
// 1. Propagate Logic
|
||||||
|
const changes = propagate();
|
||||||
|
if (!changes) return false; // Contradiction
|
||||||
|
|
||||||
|
// 2. Find Best Branch Candidate
|
||||||
|
let bestR = -1, bestC = -1;
|
||||||
|
let minUnknowns = Infinity;
|
||||||
|
let isComplete = true;
|
||||||
|
|
||||||
|
// Scan for unknowns and pick heuristic
|
||||||
|
// Heuristic: Line with fewest unknowns (most constrained)
|
||||||
|
for(let r=0; r<rows; r++) {
|
||||||
|
let unknowns = 0;
|
||||||
|
let firstUnknownC = -1;
|
||||||
|
for(let c=0; c<cols; c++) {
|
||||||
|
if(grid[r][c] === -1) {
|
||||||
|
unknowns++;
|
||||||
|
if (firstUnknownC === -1) firstUnknownC = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unknowns > 0) {
|
||||||
|
isComplete = false;
|
||||||
|
if (unknowns < minUnknowns) {
|
||||||
|
minUnknowns = unknowns;
|
||||||
|
bestR = r;
|
||||||
|
bestC = firstUnknownC;
|
||||||
|
}
|
||||||
|
if (minUnknowns === 1) break; // Optimal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isComplete) return true; // Solved!
|
||||||
|
|
||||||
|
// 3. Branching (Guessing)
|
||||||
|
// Try 1
|
||||||
|
grid[bestR][bestC] = 1;
|
||||||
|
queue.add(`r${bestR}`);
|
||||||
|
queue.add(`c${bestC}`);
|
||||||
|
|
||||||
|
if (solve(depth + 1)) return true;
|
||||||
|
|
||||||
|
// Backtrack from 1
|
||||||
|
grid[bestR][bestC] = -1; // Undo guess
|
||||||
|
// (Recursive call already undid its propagation changes)
|
||||||
|
|
||||||
|
// Try 0
|
||||||
|
grid[bestR][bestC] = 0;
|
||||||
|
queue.add(`r${bestR}`);
|
||||||
|
queue.add(`c${bestC}`);
|
||||||
|
|
||||||
|
if (solve(depth + 1)) return true;
|
||||||
|
|
||||||
|
// Backtrack from 0
|
||||||
|
grid[bestR][bestC] = -1; // Undo guess
|
||||||
|
backtracks++;
|
||||||
|
|
||||||
|
// Undo propagation from this level
|
||||||
|
undo(changes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start Solving
|
||||||
|
if (logicOnly) {
|
||||||
|
// Just logical propagation without guessing
|
||||||
|
if (initialGrid) {
|
||||||
|
// If resuming, ensure queue has all lines to check consistency
|
||||||
|
queue.clear();
|
||||||
|
for(let r=0; r<rows; r++) queue.add(`r${r}`);
|
||||||
|
for(let c=0; c<cols; c++) queue.add(`c${c}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propagate logic constraints
|
||||||
|
propagate();
|
||||||
|
// No DFS, so iterations/backtracks remain 0
|
||||||
|
} else if (!initialGrid) {
|
||||||
|
// Normal start (full solver)
|
||||||
|
solve(0);
|
||||||
|
} else {
|
||||||
|
// Resume from provided state (full solver)
|
||||||
|
// We need to populate the queue with all rows/cols since we don't know what changed
|
||||||
|
queue.clear();
|
||||||
|
for(let r=0; r<rows; r++) queue.add(`r${r}`);
|
||||||
|
for(let c=0; c<cols; c++) queue.add(`c${c}`);
|
||||||
|
solve(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate Percent Solved
|
||||||
let solvedCount = 0;
|
let solvedCount = 0;
|
||||||
for (let r = 0; r < rows; r++) {
|
grid.forEach(r => r.forEach(c => { if(c !== -1) solvedCount++; }));
|
||||||
for (let c = 0; c < cols; c++) {
|
let percentSolved = (solvedCount / totalCells) * 100;
|
||||||
if (grid[r][c] !== -1) solvedCount++;
|
|
||||||
|
if (onProgress) onProgress(Math.floor(percentSolved));
|
||||||
|
|
||||||
|
// Difficulty Calculation
|
||||||
|
// Logic:
|
||||||
|
// - Base: 0-20% based on size/density
|
||||||
|
// - Logic: 0-30% based on iterations needed (depth 0)
|
||||||
|
// - Guessing: 0-50% based on backtracks/depth
|
||||||
|
|
||||||
|
let difficultyScore = 0;
|
||||||
|
const effectiveSize = Math.sqrt(totalCells);
|
||||||
|
|
||||||
|
if (percentSolved < 100) {
|
||||||
|
difficultyScore = 100; // Unsolvable (or timed out/too hard)
|
||||||
|
} else {
|
||||||
|
if (maxDepth === 0) {
|
||||||
|
// Pure logic
|
||||||
|
difficultyScore = Math.min(30, effectiveSize);
|
||||||
|
} else {
|
||||||
|
// Required guessing
|
||||||
|
// Simple heuristic: 30 + backtracks * 5 + depth * 2
|
||||||
|
difficultyScore = 30 + (backtracks * 2) + (maxDepth * 5);
|
||||||
|
difficultyScore = Math.min(100, difficultyScore);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
solvedGrid: grid,
|
percentSolved,
|
||||||
percentSolved: (solvedCount / (rows * cols)) * 100
|
difficultyScore: Math.round(difficultyScore),
|
||||||
|
lookaheadUsed: maxDepth > 0,
|
||||||
|
iterations,
|
||||||
|
maxDepth,
|
||||||
|
backtracks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/utils/solver.test.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
it('solves a puzzle requiring guessing (Backtracking)', () => {
|
||||||
|
// A puzzle that logic alone cannot start usually has multiple solutions or requires a guess.
|
||||||
|
// Example: The "domino" or "ambiguous" pattern, but we need a unique solution that requires lookahead.
|
||||||
|
// Or just a very hard unique puzzle.
|
||||||
|
// A simple case where line logic is stuck but global constraints solve it.
|
||||||
|
//
|
||||||
|
// 0 1 1 0
|
||||||
|
// 1 0 0 1
|
||||||
|
// 1 0 0 1
|
||||||
|
// 0 1 1 0
|
||||||
|
// Hints:
|
||||||
|
// R: 2, 1 1, 1 1, 2
|
||||||
|
// C: 2, 1 1, 1 1, 2
|
||||||
|
// This is a "ring". It might be solvable by logic if corners are forced.
|
||||||
|
// Let's try a known "hard" small pattern.
|
||||||
|
//
|
||||||
|
// 0 0 0
|
||||||
|
// 0 1 0
|
||||||
|
// 0 0 0
|
||||||
|
// R: 0, 1, 0
|
||||||
|
// C: 0, 1, 0
|
||||||
|
// Logic solves this instantly.
|
||||||
|
|
||||||
|
// Let's trust the logic works for general backtracking by forcing a guess.
|
||||||
|
// We can mock solveLine to return "no change" to force backtracking? No, integration test is better.
|
||||||
|
|
||||||
|
// Let's just ensure it returns a valid result structure for a solvable puzzle.
|
||||||
|
const rowHints = [[1], [1], [1]];
|
||||||
|
const colHints = [[1], [1], [1]];
|
||||||
|
// 3x3 diagonal?
|
||||||
|
// 1 0 0
|
||||||
|
// 0 1 0
|
||||||
|
// 0 0 1
|
||||||
|
// Hints: 1, 1, 1
|
||||||
|
// Cols: 1, 1, 1
|
||||||
|
// This has multiple solutions (diagonal or anti-diagonal or others).
|
||||||
|
// Our solver should find ONE of them and return 100%.
|
||||||
|
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
// It might use lookahead because logic can't decide.
|
||||||
|
// Actually for this specific case, logic does nothing (all empty or all full are not possible, but many perms).
|
||||||
|
// So it MUST branch.
|
||||||
|
expect(result.maxDepth).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stress test: should solve 100 random valid 10x10 grids', () => {
|
||||||
|
// This ensures the solver is robust and doesn't fail on valid puzzles.
|
||||||
|
// Using a fixed seed or just running a loop.
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
const size = 10;
|
||||||
|
const grid = generateRandomGrid(size, 0.5);
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
|
if (result.percentSolved < 100) {
|
||||||
|
console.error('Failed Grid:', JSON.stringify(grid));
|
||||||
|
console.error('Result:', result);
|
||||||
|
}
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
170
src/utils/workerPool.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import SolverWorker from '../workers/solver.worker.js?worker';
|
||||||
|
|
||||||
|
class WorkerPool {
|
||||||
|
constructor() {
|
||||||
|
this.workers = [];
|
||||||
|
this.queue = [];
|
||||||
|
this.active = 0;
|
||||||
|
this.poolSize = navigator.hardwareConcurrency || 4;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.poolSize; i++) {
|
||||||
|
const worker = new SolverWorker();
|
||||||
|
worker.onmessage = (e) => this.handleWorkerMessage(worker, e);
|
||||||
|
worker.onerror = (e) => this.handleWorkerError(worker, e);
|
||||||
|
this.workers.push({ worker, busy: false, id: i });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run(taskData, onProgress) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const task = { data: taskData, resolve, reject, onProgress };
|
||||||
|
const freeWorker = this.workers.find(w => !w.busy);
|
||||||
|
|
||||||
|
if (freeWorker) {
|
||||||
|
this.execute(freeWorker, task);
|
||||||
|
} else {
|
||||||
|
this.queue.push(task);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
runRace(tasks) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let activeCount = tasks.length;
|
||||||
|
let resolved = false;
|
||||||
|
|
||||||
|
tasks.forEach(taskData => {
|
||||||
|
this.run(taskData.data, taskData.onProgress)
|
||||||
|
.then(result => {
|
||||||
|
if (resolved) return;
|
||||||
|
|
||||||
|
// Heuristic: If solved 100%, we have a winner
|
||||||
|
if (result.solvability === 100) {
|
||||||
|
resolved = true;
|
||||||
|
resolve(result);
|
||||||
|
// Cancel others (optional but good for perf)
|
||||||
|
// We can't easily cancel *specific* other tasks in this pool implementation without IDs
|
||||||
|
// But since this is a "Race", we assume the caller will handle cleanup or we just let them finish
|
||||||
|
} else {
|
||||||
|
// If not fully solved, we wait for others?
|
||||||
|
// Or maybe we collect all results and pick best?
|
||||||
|
// For "Race", we usually want the first *Success*.
|
||||||
|
// If all fail (finish without 100%), we reject or return best.
|
||||||
|
activeCount--;
|
||||||
|
if (activeCount === 0) {
|
||||||
|
// All finished, none 100%. Return the last one (or logic to pick best)
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (resolved) return;
|
||||||
|
activeCount--;
|
||||||
|
if (activeCount === 0) {
|
||||||
|
reject(new Error('All workers failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
execute(workerObj, task) {
|
||||||
|
workerObj.busy = true;
|
||||||
|
workerObj.currentTask = task;
|
||||||
|
this.active++;
|
||||||
|
workerObj.worker.postMessage(task.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWorkerMessage(worker, e) {
|
||||||
|
const workerObj = this.workers.find(w => w.worker === worker);
|
||||||
|
if (workerObj && workerObj.currentTask) {
|
||||||
|
if (e.data.type === 'progress') {
|
||||||
|
if (workerObj.currentTask.onProgress) {
|
||||||
|
workerObj.currentTask.onProgress(e.data.percent);
|
||||||
|
}
|
||||||
|
return; // Don't resolve yet
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.data.error) {
|
||||||
|
workerObj.currentTask.reject(new Error(e.data.error));
|
||||||
|
} else {
|
||||||
|
workerObj.currentTask.resolve(e.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
workerObj.currentTask = null;
|
||||||
|
workerObj.busy = false;
|
||||||
|
this.active--;
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleWorkerError(worker, e) {
|
||||||
|
const workerObj = this.workers.find(w => w.worker === worker);
|
||||||
|
if (workerObj && workerObj.currentTask) {
|
||||||
|
workerObj.currentTask.reject(e);
|
||||||
|
workerObj.currentTask = null;
|
||||||
|
workerObj.busy = false;
|
||||||
|
this.active--;
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processQueue() {
|
||||||
|
if (this.queue.length > 0) {
|
||||||
|
const freeWorker = this.workers.find(w => !w.busy);
|
||||||
|
if (freeWorker) {
|
||||||
|
const task = this.queue.shift();
|
||||||
|
this.execute(freeWorker, task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearQueue() {
|
||||||
|
this.queue.forEach(task => {
|
||||||
|
task.reject(new Error('Cancelled'));
|
||||||
|
});
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelAll() {
|
||||||
|
this.clearQueue();
|
||||||
|
|
||||||
|
// Terminate and restart busy workers
|
||||||
|
this.workers.forEach((w, index) => {
|
||||||
|
if (w.busy) {
|
||||||
|
w.worker.terminate();
|
||||||
|
|
||||||
|
if (w.currentTask) {
|
||||||
|
w.currentTask.reject(new Error('Terminated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create replacement
|
||||||
|
const newWorker = new SolverWorker();
|
||||||
|
newWorker.onmessage = (e) => this.handleWorkerMessage(newWorker, e);
|
||||||
|
newWorker.onerror = (e) => this.handleWorkerError(newWorker, e);
|
||||||
|
|
||||||
|
// Replace in array
|
||||||
|
this.workers[index] = { worker: newWorker, busy: false, id: w.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset active count since all busy workers were replaced with idle ones
|
||||||
|
this.active = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
this.workers.forEach(w => w.worker.terminate());
|
||||||
|
this.workers = [];
|
||||||
|
this.queue = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
let poolInstance = null;
|
||||||
|
|
||||||
|
export const getWorkerPool = () => {
|
||||||
|
if (!poolInstance) {
|
||||||
|
poolInstance = new WorkerPool();
|
||||||
|
}
|
||||||
|
return poolInstance;
|
||||||
|
};
|
||||||
62
src/workers/solver.worker.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { calculateHints } from '../utils/puzzleUtils';
|
||||||
|
import { solvePuzzle } from '../utils/solver';
|
||||||
|
|
||||||
|
self.onmessage = (e) => {
|
||||||
|
const { id, grid, initialGrid } = e.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!grid || grid.length === 0) {
|
||||||
|
self.postMessage({ id, error: 'Empty grid' });
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { calculateHints } from '../utils/puzzleUtils.js';
|
import { calculateHints } from '../utils/puzzleUtils.js';
|
||||||
|
import { solveLine } from '../utils/solver.js';
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
pl: {
|
pl: {
|
||||||
@@ -40,195 +41,33 @@ const t = (locale, key, params) => {
|
|||||||
return typeof value === 'string' ? format(value, params) : key;
|
return typeof value === 'string' ? format(value, params) : key;
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildPrefix = (lineState) => {
|
|
||||||
const n = lineState.length;
|
|
||||||
const filled = new Array(n + 1).fill(0);
|
|
||||||
const cross = new Array(n + 1).fill(0);
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
filled[i + 1] = filled[i] + (lineState[i] === 1 ? 1 : 0);
|
|
||||||
cross[i + 1] = cross[i] + (lineState[i] === 2 ? 1 : 0);
|
|
||||||
}
|
|
||||||
return { filled, cross };
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildSuffixMin = (hints) => {
|
|
||||||
const m = hints.length;
|
|
||||||
const suffixMin = new Array(m + 1).fill(0);
|
|
||||||
let sumHints = 0;
|
|
||||||
for (let i = m - 1; i >= 0; i--) {
|
|
||||||
sumHints += hints[i];
|
|
||||||
const separators = m - i - 1;
|
|
||||||
suffixMin[i] = sumHints + separators;
|
|
||||||
}
|
|
||||||
return suffixMin;
|
|
||||||
};
|
|
||||||
|
|
||||||
const solveLineLogic = (lineState, hints) => {
|
const solveLineLogic = (lineState, hints) => {
|
||||||
const n = lineState.length;
|
// Map Store format (0=Unk, 1=Fill, 2=Cross) to Solver format (-1=Unk, 1=Fill, 0=Empty)
|
||||||
const m = hints.length;
|
const solverLine = lineState.map(cell => {
|
||||||
if (m === 0) {
|
if (cell === 0) return -1; // Unknown
|
||||||
for (let i = 0; i < n; i++) {
|
if (cell === 1) return 1; // Filled
|
||||||
if (lineState[i] === 0) return { index: i, state: 2 };
|
if (cell === 2) return 0; // Empty/Cross
|
||||||
}
|
return -1;
|
||||||
return { index: -1 };
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const { filled, cross } = buildPrefix(lineState);
|
// Call robust solver
|
||||||
const suffixMin = buildSuffixMin(hints);
|
const resultLine = solveLine(solverLine, hints);
|
||||||
|
|
||||||
const hasFilled = (a, b) => filled[b] - filled[a] > 0;
|
// Check for new info
|
||||||
const hasCross = (a, b) => cross[b] - cross[a] > 0;
|
if (!resultLine) return { index: -1 }; // Contradiction or error
|
||||||
|
|
||||||
const memoSuffix = Array.from({ length: n + 1 }, () => Array(m + 1).fill(null));
|
for (let i = 0; i < lineState.length; i++) {
|
||||||
const memoPrefix = Array.from({ length: n + 1 }, () => Array(m + 1).fill(null));
|
// We only care about cells that are currently 0 (Unknown) in Store
|
||||||
|
if (lineState[i] === 0) {
|
||||||
const canPlaceSuffix = (pos, hintIndex) => {
|
if (resultLine[i] === 1) {
|
||||||
const cached = memoSuffix[pos][hintIndex];
|
return { index: i, state: 1 }; // Suggest Fill
|
||||||
if (cached !== null) return cached;
|
|
||||||
if (hintIndex === m) {
|
|
||||||
const result = !hasFilled(pos, n);
|
|
||||||
memoSuffix[pos][hintIndex] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const len = hints[hintIndex];
|
|
||||||
// maxStart logic: we need enough space for this block (len) + subsequent blocks/gaps (suffixMin[hintIndex+1])
|
|
||||||
// suffixMin[hintIndex] = len + (m - hintIndex - 1) + suffixMin[hintIndex+1]
|
|
||||||
// Actually suffixMin[hintIndex] already includes everything needed from here to end.
|
|
||||||
// So if we place block at start, end is start + len.
|
|
||||||
// Total space needed is suffixMin[hintIndex].
|
|
||||||
// So start can go up to n - suffixMin[hintIndex].
|
|
||||||
const maxStart = n - suffixMin[hintIndex];
|
|
||||||
|
|
||||||
for (let start = pos; start <= maxStart; start++) {
|
|
||||||
if (hasFilled(pos, start)) continue; // Must be empty before this block
|
|
||||||
if (hasCross(start, start + len)) continue; // Block space must be free of crosses
|
|
||||||
|
|
||||||
// If not the last block, we need a gap after
|
|
||||||
if (hintIndex < m - 1) {
|
|
||||||
if (start + len < n && lineState[start + len] === 1) continue; // Gap must not be filled
|
|
||||||
// We can assume gap is at start + len. Next block starts at least at start + len + 1
|
|
||||||
const nextPos = start + len + 1;
|
|
||||||
if (canPlaceSuffix(nextPos, hintIndex + 1)) {
|
|
||||||
memoSuffix[pos][hintIndex] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Last block
|
|
||||||
// Check if we can fill the rest with empty
|
|
||||||
if (hasFilled(start + len, n)) continue;
|
|
||||||
memoSuffix[pos][hintIndex] = true;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
if (resultLine[i] === 0) {
|
||||||
memoSuffix[pos][hintIndex] = false;
|
return { index: i, state: 2 }; // Suggest Cross
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canPlacePrefix = (pos, hintCount) => {
|
|
||||||
const cached = memoPrefix[pos][hintCount];
|
|
||||||
if (cached !== null) return cached;
|
|
||||||
if (hintCount === 0) {
|
|
||||||
const result = !hasFilled(0, pos);
|
|
||||||
memoPrefix[pos][hintCount] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
const len = hints[hintCount - 1];
|
|
||||||
|
|
||||||
// Logic for prefix:
|
|
||||||
// We are placing the (hintCount-1)-th block ending at 'start + len' <= pos.
|
|
||||||
// So 'start' <= pos - len.
|
|
||||||
// But we also need to ensure there is space for previous blocks.
|
|
||||||
// However, the simple constraint is just iterating backwards.
|
|
||||||
|
|
||||||
// maxStart: if this is the only block, maxStart = pos - len.
|
|
||||||
// If there are previous blocks, we need a gap before this block.
|
|
||||||
// So previous block ended at start - 1.
|
|
||||||
// Actually the recursive call will handle space check.
|
|
||||||
// But for the gap check:
|
|
||||||
// If we place block at 'start', we need lineState[start-1] != 1 (if start > 0).
|
|
||||||
// And we recursively check canPlacePrefix(start-1, count-1).
|
|
||||||
// But if start=0 and count > 1, impossible.
|
|
||||||
|
|
||||||
const maxStart = pos - len; // Simplified, loop condition handles rest
|
|
||||||
|
|
||||||
for (let start = maxStart; start >= 0; start--) {
|
|
||||||
if (hasCross(start, start + len)) continue;
|
|
||||||
if (hasFilled(start + len, pos)) continue; // Must be empty after this block up to pos
|
|
||||||
|
|
||||||
// Check gap before
|
|
||||||
if (hintCount > 1) {
|
|
||||||
if (start === 0) continue; // No space for previous blocks
|
|
||||||
if (lineState[start - 1] === 1) continue; // Gap must not be filled
|
|
||||||
const prevPos = start - 1;
|
|
||||||
if (canPlacePrefix(prevPos, hintCount - 1)) {
|
|
||||||
memoPrefix[pos][hintCount] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// First block
|
|
||||||
if (hasFilled(0, start)) continue; // Before first block must be empty
|
|
||||||
memoPrefix[pos][hintCount] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
memoPrefix[pos][hintCount] = false;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const possibleStarts = [];
|
|
||||||
for (let i = 0; i < m; i++) {
|
|
||||||
const len = hints[i];
|
|
||||||
const starts = [];
|
|
||||||
for (let start = 0; start <= n - len; start++) {
|
|
||||||
if (i === 0) {
|
|
||||||
if (!canPlacePrefix(start, 0)) continue;
|
|
||||||
} else {
|
|
||||||
if (start === 0) continue;
|
|
||||||
if (lineState[start - 1] === 1) continue;
|
|
||||||
if (!canPlacePrefix(start - 1, i)) continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasCross(start, start + len)) continue;
|
|
||||||
if (start + len < n && lineState[start + len] === 1) continue;
|
|
||||||
const nextPos = start + len < n ? start + len + 1 : start + len;
|
|
||||||
if (!canPlaceSuffix(nextPos, i + 1)) continue;
|
|
||||||
starts.push(start);
|
|
||||||
}
|
|
||||||
possibleStarts.push(starts);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mustFill = new Array(n).fill(false);
|
|
||||||
const coverage = new Array(n).fill(false);
|
|
||||||
|
|
||||||
for (let i = 0; i < m; i++) {
|
|
||||||
const starts = possibleStarts[i];
|
|
||||||
const len = hints[i];
|
|
||||||
if (starts.length === 0) return { index: -1 };
|
|
||||||
let earliest = starts[0];
|
|
||||||
let latest = starts[0];
|
|
||||||
for (let j = 1; j < starts.length; j++) {
|
|
||||||
earliest = Math.min(earliest, starts[j]);
|
|
||||||
latest = Math.max(latest, starts[j]);
|
|
||||||
}
|
|
||||||
const startOverlap = Math.max(earliest, latest);
|
|
||||||
const endOverlap = Math.min(earliest + len - 1, latest + len - 1);
|
|
||||||
for (let k = startOverlap; k <= endOverlap; k++) {
|
|
||||||
if (k >= 0 && k < n) mustFill[k] = true;
|
|
||||||
}
|
|
||||||
for (let s = 0; s < starts.length; s++) {
|
|
||||||
const start = starts[s];
|
|
||||||
for (let k = start; k < start + len; k++) {
|
|
||||||
coverage[k] = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
if (lineState[i] === 0 && mustFill[i]) return { index: i, state: 1 };
|
|
||||||
}
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
if (lineState[i] === 0 && !coverage[i]) return { index: i, state: 2 };
|
|
||||||
}
|
|
||||||
return { index: -1 };
|
return { index: -1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import path from 'path'
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
define: {
|
define: {
|
||||||
'__APP_VERSION__': JSON.stringify(process.env.npm_package_version)
|
'__APP_VERSION__': JSON.stringify(process.env.npm_package_version),
|
||||||
|
'__APP_HOMEPAGE__': JSON.stringify(process.env.npm_package_homepage)
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
@@ -30,14 +31,14 @@ export default defineConfig({
|
|||||||
theme_color: '#00f2fe',
|
theme_color: '#00f2fe',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/pwa-192x192.svg',
|
src: '/pwa-192x192.png',
|
||||||
sizes: '192x192',
|
sizes: '192x192',
|
||||||
type: 'image/svg+xml'
|
type: 'image/png'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/pwa-512x512.svg',
|
src: '/pwa-512x512.png',
|
||||||
sizes: '512x512',
|
sizes: '512x512',
|
||||||
type: 'image/svg+xml'
|
type: 'image/png'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -47,5 +48,8 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src')
|
'@': path.resolve(__dirname, './src')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
allowedHosts: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||