From 48def6c4005ea009ef51e2383876691288dd493c Mon Sep 17 00:00:00 2001 From: Grzegorz Kucmierz Date: Fri, 13 Feb 2026 02:23:44 +0100 Subject: [PATCH] feat: enhance image import and solvability calculation (v1.13.0) - Implement non-linear threshold slider (histogram percentile method) - Add real-time solvability calculation with progress indicator - Improve solvability logic with generative lookahead (smash) - Update ImageImportModal UI (alpha preview, grid size 5-80) - Add missing translations and difficulty labels - Optimize web worker pool with queue clearing and progress reporting - Fix mobile camera support and UI layout --- dev-dist/sw.js | 2 +- difficulty_simulation_results.json | 938 ---------------------------- package.json | 2 +- src/App.vue | 11 + src/components/ImageImportModal.vue | 823 ++++++++++++++++++++++++ src/components/NavBar.vue | 20 +- src/composables/useI18n.js | 23 + src/stores/puzzle.js | 21 + src/utils/puzzleUtils.js | 9 +- src/utils/solver.js | 190 +++++- src/utils/workerPool.js | 99 +++ src/workers/solver.worker.js | 61 ++ 12 files changed, 1228 insertions(+), 971 deletions(-) delete mode 100644 difficulty_simulation_results.json create mode 100644 src/components/ImageImportModal.vue create mode 100644 src/utils/workerPool.js create mode 100644 src/workers/solver.worker.js diff --git a/dev-dist/sw.js b/dev-dist/sw.js index 3b5e5ef..b5f7741 100644 --- a/dev-dist/sw.js +++ b/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict'; */ workbox.precacheAndRoute([{ "url": "index.html", - "revision": "0.0dmrmul42fg" + "revision": "0.geiftdl7j9o" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/difficulty_simulation_results.json b/difficulty_simulation_results.json deleted file mode 100644 index e1ddf22..0000000 --- a/difficulty_simulation_results.json +++ /dev/null @@ -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 - } -] \ No newline at end of file diff --git a/package.json b/package.json index c42b6c0..87bef88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-nonograms-solid", - "version": "1.12.11", + "version": "1.13.0", "homepage": "https://nonograms.7u.pl/", "type": "module", "scripts": { diff --git a/src/App.vue b/src/App.vue index f4cbf63..e843a56 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,7 @@ import StatusPanel from './components/StatusPanel.vue'; import GuidePanel from './components/GuidePanel.vue'; import WinModal from './components/WinModal.vue'; import CustomGameModal from './components/CustomGameModal.vue'; +import ImageImportModal from './components/ImageImportModal.vue'; import SimulationView from './components/SimulationView.vue'; import FixedBar from './components/FixedBar.vue'; import ReloadPrompt from './components/ReloadPrompt.vue'; @@ -16,6 +17,7 @@ import ReloadPrompt from './components/ReloadPrompt.vue'; const store = usePuzzleStore(); const { t, locale, setLocale, locales } = useI18n(); const showCustomModal = ref(false); +const showImageImportModal = ref(false); const showSimulation = ref(false); const showGuide = ref(false); const deferredPrompt = ref(null); @@ -39,6 +41,10 @@ const onKeyDownGlobal = (e) => { showCustomModal.value = false; return; } + if (showImageImportModal.value) { + showImageImportModal.value = false; + return; + } if (store.isGameWon) { store.closeWinModal(); } @@ -158,6 +164,7 @@ onUnmounted(() => {
@@ -207,6 +214,10 @@ onUnmounted(() => { + diff --git a/src/components/ImageImportModal.vue b/src/components/ImageImportModal.vue new file mode 100644 index 0000000..6ad1d7c --- /dev/null +++ b/src/components/ImageImportModal.vue @@ -0,0 +1,823 @@ + + + + + diff --git a/src/components/NavBar.vue b/src/components/NavBar.vue index dd1fa6a..afe2199 100644 --- a/src/components/NavBar.vue +++ b/src/components/NavBar.vue @@ -2,12 +2,12 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; import { usePuzzleStore } from '@/stores/puzzle'; 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 } from 'lucide-vue-next'; const store = usePuzzleStore(); 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 isThemeOpen = ref(false); @@ -180,6 +180,12 @@ const openCustom = () => { closeMobileMenu(); }; +const openImageImport = () => { + emit('open-image-import'); + isGameOpen.value = false; + closeMobileMenu(); +}; + const setTheme = (theme) => { emit('set-theme', theme); isThemeOpen.value = false; @@ -254,7 +260,10 @@ watch(isMobileMenuOpen, (val) => { {{ lvl.label }} + @@ -351,7 +360,10 @@ watch(isMobileMenuOpen, (val) => { {{ lvl.label }} + diff --git a/src/composables/useI18n.js b/src/composables/useI18n.js index f946f24..55126c8 100644 --- a/src/composables/useI18n.js +++ b/src/composables/useI18n.js @@ -7,6 +7,8 @@ const messages = { 'level.medium': 'ŚREDNI 10X10', 'level.hard': 'TRUDNY 15X15', 'level.custom': 'WŁASNY', + 'level.custom_random': 'WŁASNY LOSOWY', + 'level.custom_image': 'WŁASNY Z OBRAZU', 'level.guide': 'PODPOWIEDŹ ❓', 'actions.reset': 'RESET', 'actions.random': 'NOWA LOSOWA', @@ -31,10 +33,29 @@ const messages = { 'custom.sizeError': 'Rozmiar musi być między 5 a 80!', 'custom.fillRate': 'Wypełnienie', 'custom.difficulty': 'Poziom trudności', + 'image.title': 'IMPORTUJ OBRAZ', + 'image.drop': 'Przeciągnij obraz tutaj', + 'image.select': 'Wybierz plik', + 'image.camera': 'Zrób zdjęcie', + 'image.capture': 'Zrób zdjęcie', + 'image.switch': 'Przełącz aparat', + 'image.cameraError': 'Nie można uzyskać dostępu do kamery', + 'image.change': 'Zmień obraz', + 'image.size': 'Rozmiar siatki', + 'image.threshold': 'Próg czerni', + 'image.solvability': 'Rozwiązywalność', + 'image.warning': 'Ten obraz może wymagać zgadywania!', + 'image.difficulty': 'Trudność', + 'image.calculating': 'Obliczanie...', + 'image.calculatingSolvability': 'Obliczanie rozwiązywalności...', + 'image.create': 'STWÓRZ NONOGRAM', 'difficulty.easy': 'Łatwy', + 'difficulty.medium': 'Średni', + 'difficulty.hard': 'Trudny', 'difficulty.harder': 'Trudniejszy', 'difficulty.hardest': 'Najtrudniejszy', 'difficulty.extreme': 'Ekstremalny', + 'difficulty.unknown': 'Nieznany', 'win.title': 'GRATULACJE!', 'win.message': 'Rozwiązałeś zagadkę!', 'win.time': 'Czas:', @@ -181,6 +202,8 @@ const messages = { 'level.medium': 'MEDIUM 10X10', 'level.hard': 'HARD 15X15', 'level.custom': 'CUSTOM', + 'level.custom_random': 'CUSTOM RANDOM', + 'level.custom_image': 'CUSTOM FROM IMAGE', 'level.guide': 'GUIDE ❓', 'actions.reset': 'RESET', 'actions.random': 'NEW RANDOM', diff --git a/src/stores/puzzle.js b/src/stores/puzzle.js index 94b59de..995dfc1 100644 --- a/src/stores/puzzle.js +++ b/src/stores/puzzle.js @@ -93,6 +93,26 @@ export const usePuzzleStore = defineStore('puzzle', () => { saveState(); } + function initFromImage(grid) { + stopTimer(); + currentLevelId.value = 'custom_image'; + size.value = grid.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() { playerGrid.value = Array(size.value).fill().map(() => Array(size.value).fill(0)); moves.value = 0; @@ -332,6 +352,7 @@ export const usePuzzleStore = defineStore('puzzle', () => { progressPercentage, initGame, initCustomGame, + initFromImage, toggleCell, setCell, resetGame, diff --git a/src/utils/puzzleUtils.js b/src/utils/puzzleUtils.js index 86d5a20..e954ac9 100644 --- a/src/utils/puzzleUtils.js +++ b/src/utils/puzzleUtils.js @@ -27,19 +27,20 @@ export function validateLine(line, targetHints) { export function calculateHints(grid) { if (!grid || grid.length === 0) return { rowHints: [], colHints: [] }; - const size = grid.length; + const rows = grid.length; + const cols = grid[0].length; const rowHints = []; const colHints = []; // Row Hints - for (let r = 0; r < size; r++) { + for (let r = 0; r < rows; r++) { rowHints.push(calculateLineHints(grid[r])); } // Col Hints - for (let c = 0; c < size; c++) { + for (let c = 0; c < cols; c++) { const col = []; - for (let r = 0; r < size; r++) { + for (let r = 0; r < rows; r++) { col.push(grid[r][c]); } colHints.push(calculateLineHints(col)); diff --git a/src/utils/solver.js b/src/utils/solver.js index eb18d9b..811378e 100644 --- a/src/utils/solver.js +++ b/src/utils/solver.js @@ -218,14 +218,15 @@ function canFitRest(line, startIndex, hints, hintIndex) { * Solves the puzzle using logical iteration. * @param {number[][]} rowHints * @param {number[][]} colHints - * @returns {object} { solvedGrid: number[][], percentSolved: number } + * @param {number[][]} initialGrid - Optional starting state + * @returns {object} { grid: number[][], changed: boolean } */ -export function solvePuzzle(rowHints, colHints) { +function solveLogically(rowHints, colHints, initialGrid) { const rows = rowHints.length; const cols = colHints.length; - // Initialize grid with -1 - let grid = Array(rows).fill(null).map(() => Array(cols).fill(-1)); + // Initialize grid with -1 if not provided + let grid = initialGrid ? initialGrid.map(row => [...row]) : Array(rows).fill(null).map(() => Array(cols).fill(-1)); let changed = true; let iterations = 0; @@ -238,12 +239,15 @@ export function solvePuzzle(rowHints, colHints) { // Rows for (let r = 0; r < rows; r++) { const newLine = solveLine(grid[r], rowHints[r]); - if (newLine) { - for (let c = 0; c < cols; c++) { - if (grid[r][c] !== newLine[c]) { - grid[r][c] = newLine[c]; - changed = true; - } + if (!newLine) return { grid, contradiction: true }; // Contradiction found + + for (let c = 0; c < cols; c++) { + if (grid[r][c] !== newLine[c]) { + // If we try to overwrite a known value with a different one -> Contradiction + if (grid[r][c] !== -1 && grid[r][c] !== newLine[c]) return { grid, contradiction: true }; + + grid[r][c] = newLine[c]; + changed = true; } } } @@ -252,27 +256,167 @@ export function solvePuzzle(rowHints, colHints) { 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; - } + if (!newCol) return { grid, contradiction: true }; // Contradiction found + + for (let r = 0; r < rows; r++) { + if (grid[r][c] !== newCol[r]) { + if (grid[r][c] !== -1 && grid[r][c] !== newCol[r]) return { grid, contradiction: true }; + + grid[r][c] = newCol[r]; + changed = true; } } } } - // Calculate solved % + return { grid, changed: iterations > 1, iterations, contradiction: false }; +} + +/** + * Main solver function that attempts to solve the puzzle using logic and lookahead. + * @param {number[][]} rowHints + * @param {number[][]} colHints + * @param {function} onProgress - Optional callback for progress reporting (percent) + * @returns {object} result + */ +export function solvePuzzle(rowHints, colHints, onProgress) { + const rows = rowHints.length; + const cols = colHints.length; + const totalCells = rows * cols; + + // 1. Basic Logical Solve + let { grid, iterations, contradiction } = solveLogically(rowHints, colHints); + + // Count solved let solvedCount = 0; - for (let r = 0; r < rows; r++) { - for (let c = 0; c < cols; c++) { - if (grid[r][c] !== -1) solvedCount++; + grid.forEach(r => r.forEach(c => { if(c !== -1) solvedCount++; })); + let percentSolved = (solvedCount / totalCells) * 100; + + if (onProgress) onProgress(Math.floor(percentSolved)); + + // Difficulty calculation + // Base: complexity of grid + let difficultyScore = 0; + + // If simple logic failed to solve completely, try Lookahead (Smash) + let lookaheadUsed = false; + + if (percentSolved < 100 && !contradiction) { + // Lookahead loop + // Find an unknown cell, try 0 and 1. If one leads to contradiction, the other is true. + let progress = true; + while (progress && percentSolved < 100) { + progress = false; + + // Find unknown cells (optimize: sort by most constrained?) + // For now, just scan. + let candidates = []; + for(let r=0; r lastReportedPercent || checkedCount % 10 === 0) { + lastReportedPercent = currentScanPercent; + onProgress(currentScanPercent); + } + } + + // Try assuming 1 + // We need to clone the grid for simulation + const gridCopy1 = grid.map(row => [...row]); + gridCopy1[r][c] = 1; + const res1 = solveLogically(rowHints, colHints, gridCopy1); + + // Try assuming 0 + const gridCopy0 = grid.map(row => [...row]); + gridCopy0[r][c] = 0; + const res0 = solveLogically(rowHints, colHints, gridCopy0); + + let deduced = null; + + if (res1.contradiction && !res0.contradiction) { + deduced = 0; // Must be 0 + } else if (!res1.contradiction && res0.contradiction) { + deduced = 1; // Must be 1 + } + + if (deduced !== null) { + grid[r][c] = deduced; + progress = true; + lookaheadUsed = true; + difficultyScore += 5; // Penalty for requiring lookahead + + // Run logic again to propagate this new info + const updated = solveLogically(rowHints, colHints, grid); + if (updated.contradiction) break; // Should not happen if logic is sound + grid = updated.grid; + + break; // Restart loop to use new info + } + } + + // Recalculate percent (this is for loop exit condition) + solvedCount = 0; + grid.forEach(row => row.forEach(c => { if(c !== -1) solvedCount++; })); + percentSolved = (solvedCount / totalCells) * 100; + // Note: we don't report percentSolved here because we want the spinner to show SCAN progress (0-100% of current pass) + // If we reported percentSolved, the user might see the spinner jump from 100% (scan done) to 5% (solved amount), which is confusing. } } - return { - solvedGrid: grid, - percentSolved: (solvedCount / (rows * cols)) * 100 + // Final Difficulty Calculation + // Factors: + // 1. Size (rows * cols) + // 2. Iterations (how many passes of line logic) + // 3. Lookahead (did we need it?) + + const effectiveSize = Math.sqrt(rows * cols); + // iterations is usually 2-20. + // difficultyScore accumulates lookahead steps. + + // Normalize iterations + const iterScore = Math.min(20, iterations) * 2; + + // Base difficulty + let totalScore = effectiveSize + iterScore + difficultyScore; + + // If not fully solved, massive penalty + if (percentSolved < 100) { + // Unsolvable by logic+lookahead + // This is "Extreme" or "Guessing Required" + totalScore = 100; // Cap at max + } else { + // Solved + // Normalize score 0-100 (approximately) + // Max theoretical "normal" score ~ 80 (size 80) + 40 (iter) + 20 (lookahead) = 140? + // Let's scale it. + totalScore = Math.min(100, totalScore); + } + + return { + percentSolved, + difficultyScore: totalScore, + lookaheadUsed, + iterations }; } diff --git a/src/utils/workerPool.js b/src/utils/workerPool.js new file mode 100644 index 0000000..f6d7c7e --- /dev/null +++ b/src/utils/workerPool.js @@ -0,0 +1,99 @@ +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); + } + }); + } + + 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 + } + + 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 = []; + } + + 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; +}; diff --git a/src/workers/solver.worker.js b/src/workers/solver.worker.js new file mode 100644 index 0000000..2139c72 --- /dev/null +++ b/src/workers/solver.worker.js @@ -0,0 +1,61 @@ +import { calculateHints } from '../utils/puzzleUtils'; +import { solvePuzzle } from '../utils/solver'; + +self.onmessage = (e) => { + const { id, grid } = e.data; + + try { + if (!grid || grid.length === 0) { + self.postMessage({ id, error: 'Empty grid' }); + return; + } + + const rows = grid.length; + const cols = grid[0].length; + const size = Math.max(rows, cols); + const density = grid.flat().filter(c => c === 1).length / (rows * cols); + + // 1. Calculate Hints + 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); + + // 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 }); + } +};