28 Commits

Author SHA1 Message Date
430f370196 1.9.0 2026-02-11 05:31:50 +01:00
c8789741fc feat(guide): update playback speed options (remove 3x, add 8x/16x) 2026-02-11 05:31:49 +01:00
53ef63f25f 1.8.5 2026-02-11 05:24:12 +01:00
9b869bdb5f perf(puzzle): optimize undo logic and dragging performance 2026-02-11 05:22:32 +01:00
764763016b 1.8.4 2026-02-11 05:00:18 +01:00
5cfabf40e8 feat(i18n): add simulation translations for all languages 2026-02-11 05:00:16 +01:00
460de9fac9 1.8.3 2026-02-11 04:50:21 +01:00
a264ac75e3 perf(CustomGameModal): optimize difficulty map rendering with caching 2026-02-11 04:50:13 +01:00
43822d03ac 1.8.2 2026-02-11 04:47:09 +01:00
2e83d3ea9f feat(i18n): add translations for difficulty simulation feature 2026-02-11 04:47:02 +01:00
0023190f5a 1.8.1 2026-02-11 04:32:39 +01:00
2552ea9423 fix(CustomGameModal): improve difficulty map UX - resize and drag outside 2026-02-11 04:32:32 +01:00
e08be0574d 1.8.0 2026-02-11 04:14:12 +01:00
3797e7715f feat(difficulty): implement Monte Carlo simulation for accurate difficulty calculation 2026-02-11 04:14:06 +01:00
19c4516d22 1.7.0 2026-02-11 03:47:30 +01:00
06c345e8f0 fix(CustomGameModal): improve layout of difficulty level and percentage 2026-02-11 03:47:24 +01:00
d9ae630fe5 1.6.4 2026-02-11 03:03:15 +01:00
132c4ebced feat: enhance custom game difficulty calculation and UI 2026-02-11 03:03:14 +01:00
0b8dcacd18 1.6.3 2026-02-11 02:16:01 +01:00
4ef4f2b251 fix: solver logic and feat: save custom game settings 2026-02-11 02:15:59 +01:00
17d8cbfedd 1.6.2 2026-02-11 01:20:38 +01:00
a41e337c43 fix: improve solver gap enforcement and overlapping block detection 2026-02-11 01:20:29 +01:00
1f1de61044 1.6.1 2026-02-11 01:02:00 +01:00
7f22aa9198 fix: enforce gap between blocks in solver logic 2026-02-11 01:01:53 +01:00
133a676682 1.6.0 2026-02-11 00:47:20 +01:00
30318fafaf feat: improve victory sound effect 2026-02-11 00:47:07 +01:00
5549e24c17 1.5.0 2026-02-11 00:27:50 +01:00
ca3193d07e feat: display hint usage percentage in win screen 2026-02-11 00:27:43 +01:00
19 changed files with 4007 additions and 212 deletions

37
check_i18n.js Normal file
View File

@@ -0,0 +1,37 @@
const fs = require('fs');
const fileContent = fs.readFileSync('src/composables/useI18n.js', 'utf8');
// Extract the messages object
const match = fileContent.match(/const messages = ({[\s\S]*?});/);
if (!match) {
console.error('Could not find messages object');
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 messages = eval(`(${messagesStr})`);
const enKeys = Object.keys(messages.en);
const languages = Object.keys(messages);
const missing = {};
languages.forEach(lang => {
if (lang === 'en') return;
const langKeys = Object.keys(messages[lang]);
const missingKeys = enKeys.filter(k => !langKeys.includes(k));
if (missingKeys.length > 0) {
missing[lang] = missingKeys;
}
});
console.log(JSON.stringify(missing, null, 2));

View File

@@ -0,0 +1,118 @@
size,density,avg_solved_percent,min_solved_percent,max_solved_percent,avg_time_ms
5,0.1,89.40,36.00,100.00,0.03
5,0.2,74.20,8.00,100.00,0.04
5,0.3,74.20,0.00,100.00,0.04
5,0.4,80.80,8.00,100.00,0.03
5,0.5,96.80,68.00,100.00,0.03
5,0.6,97.60,84.00,100.00,0.03
5,0.7,99.20,84.00,100.00,0.03
5,0.8,100.00,100.00,100.00,0.02
5,0.9,100.00,100.00,100.00,0.02
10,0.1,56.60,19.00,86.00,0.04
10,0.2,19.80,0.00,51.00,0.05
10,0.3,15.75,0.00,73.00,0.07
10,0.4,54.05,0.00,100.00,0.13
10,0.5,91.80,59.00,100.00,0.17
10,0.6,99.80,96.00,100.00,0.07
10,0.7,99.80,96.00,100.00,0.05
10,0.8,100.00,100.00,100.00,0.04
10,0.9,100.00,100.00,100.00,0.02
15,0.1,37.04,13.33,61.78,0.05
15,0.2,9.78,0.00,26.67,0.03
15,0.3,1.89,0.00,8.00,0.03
15,0.4,11.82,0.00,61.33,0.08
15,0.5,68.20,2.67,100.00,0.14
15,0.6,99.56,96.44,100.00,0.09
15,0.7,99.78,97.33,100.00,0.07
15,0.8,100.00,100.00,100.00,0.05
15,0.9,100.00,100.00,100.00,0.03
20,0.1,22.59,5.00,41.50,0.04
20,0.2,3.25,0.00,14.50,0.04
20,0.3,0.56,0.00,5.00,0.04
20,0.4,1.46,0.00,3.25,0.07
20,0.5,36.75,1.25,99.00,0.26
20,0.6,99.80,99.00,100.00,0.23
20,0.7,99.95,99.00,100.00,0.13
20,0.8,100.00,100.00,100.00,0.09
20,0.9,100.00,100.00,100.00,0.05
25,0.1,16.00,7.84,32.80,0.06
25,0.2,0.00,0.00,0.00,0.04
25,0.3,0.05,0.00,0.64,0.06
25,0.4,0.89,0.00,9.44,0.12
25,0.5,19.13,1.12,96.48,0.35
25,0.6,99.25,94.88,100.00,0.50
25,0.7,99.90,99.36,100.00,0.24
25,0.8,100.00,100.00,100.00,0.15
25,0.9,99.97,99.36,100.00,0.08
30,0.1,7.99,0.00,16.00,0.08
30,0.2,0.17,0.00,3.33,0.07
30,0.3,0.00,0.00,0.00,0.08
30,0.4,0.38,0.00,4.11,0.18
30,0.5,5.42,0.78,21.44,0.41
30,0.6,99.42,94.78,100.00,0.94
30,0.7,99.93,99.56,100.00,0.42
30,0.8,100.00,100.00,100.00,0.22
30,0.9,100.00,100.00,100.00,0.13
35,0.1,5.65,0.00,13.80,0.11
35,0.2,0.14,0.00,2.86,0.10
35,0.3,0.00,0.00,0.00,0.13
35,0.4,0.13,0.00,0.57,0.22
35,0.5,4.42,0.41,23.18,0.56
35,0.6,91.11,8.41,100.00,1.58
35,0.7,99.97,99.67,100.00,0.60
35,0.8,100.00,100.00,100.00,0.33
35,0.9,100.00,100.00,100.00,0.17
40,0.1,2.73,0.00,9.81,0.13
40,0.2,0.00,0.00,0.00,0.13
40,0.3,0.00,0.00,0.00,0.18
40,0.4,0.03,0.00,0.31,0.27
40,0.5,2.14,0.00,9.56,0.69
40,0.6,91.44,22.31,100.00,2.92
40,0.7,99.99,99.75,100.00,0.84
40,0.8,100.00,100.00,100.00,0.43
40,0.9,100.00,100.00,100.00,0.24
45,0.1,1.78,0.00,4.69,0.17
45,0.2,0.00,0.00,0.00,0.17
45,0.3,0.00,0.00,0.00,0.24
45,0.4,0.01,0.00,0.15,0.33
45,0.5,1.44,0.40,5.14,0.96
45,0.6,81.72,6.02,100.00,5.28
45,0.7,99.94,99.41,100.00,1.28
45,0.8,100.00,100.00,100.00,0.64
45,0.9,100.00,100.00,100.00,0.30
50,0.1,1.70,0.00,5.92,0.20
50,0.2,0.00,0.00,0.00,0.23
50,0.3,0.00,0.00,0.00,0.30
50,0.4,0.01,0.00,0.16,0.38
50,0.5,0.51,0.00,1.80,0.90
50,0.6,73.26,5.60,100.00,7.94
50,0.7,99.96,99.76,100.00,1.63
50,0.8,100.00,100.00,100.00,0.83
50,0.9,100.00,100.00,100.00,0.41
60,0.1,0.17,0.00,1.67,0.24
60,0.2,0.00,0.00,0.00,0.35
60,0.3,0.00,0.00,0.00,0.50
60,0.4,0.00,0.00,0.06,0.64
60,0.5,0.23,0.00,1.67,1.24
60,0.6,35.02,1.33,100.00,10.76
60,0.7,99.97,99.83,100.00,2.96
60,0.8,100.00,100.00,100.00,1.27
60,0.9,100.00,100.00,100.00,0.62
70,0.1,0.14,0.00,1.43,0.34
70,0.2,0.00,0.00,0.00,0.54
70,0.3,0.00,0.00,0.00,0.76
70,0.4,0.00,0.00,0.00,0.96
70,0.5,0.04,0.00,0.29,1.49
70,0.6,16.40,1.00,99.71,22.21
70,0.7,99.97,99.73,100.00,4.92
70,0.8,100.00,99.92,100.00,2.03
70,0.9,100.00,100.00,100.00,0.89
80,0.1,0.00,0.00,0.00,0.44
80,0.2,0.00,0.00,0.00,0.78
80,0.3,0.00,0.00,0.00,1.07
80,0.4,0.00,0.00,0.00,1.34
80,0.5,0.01,0.00,0.08,1.97
80,0.6,1.21,0.41,2.30,3.92
80,0.7,99.98,99.94,100.00,7.79
80,0.8,100.00,100.00,100.00,3.14
80,0.9,100.00,100.00,100.00,1.31
1 size density avg_solved_percent min_solved_percent max_solved_percent avg_time_ms
2 5 0.1 89.40 36.00 100.00 0.03
3 5 0.2 74.20 8.00 100.00 0.04
4 5 0.3 74.20 0.00 100.00 0.04
5 5 0.4 80.80 8.00 100.00 0.03
6 5 0.5 96.80 68.00 100.00 0.03
7 5 0.6 97.60 84.00 100.00 0.03
8 5 0.7 99.20 84.00 100.00 0.03
9 5 0.8 100.00 100.00 100.00 0.02
10 5 0.9 100.00 100.00 100.00 0.02
11 10 0.1 56.60 19.00 86.00 0.04
12 10 0.2 19.80 0.00 51.00 0.05
13 10 0.3 15.75 0.00 73.00 0.07
14 10 0.4 54.05 0.00 100.00 0.13
15 10 0.5 91.80 59.00 100.00 0.17
16 10 0.6 99.80 96.00 100.00 0.07
17 10 0.7 99.80 96.00 100.00 0.05
18 10 0.8 100.00 100.00 100.00 0.04
19 10 0.9 100.00 100.00 100.00 0.02
20 15 0.1 37.04 13.33 61.78 0.05
21 15 0.2 9.78 0.00 26.67 0.03
22 15 0.3 1.89 0.00 8.00 0.03
23 15 0.4 11.82 0.00 61.33 0.08
24 15 0.5 68.20 2.67 100.00 0.14
25 15 0.6 99.56 96.44 100.00 0.09
26 15 0.7 99.78 97.33 100.00 0.07
27 15 0.8 100.00 100.00 100.00 0.05
28 15 0.9 100.00 100.00 100.00 0.03
29 20 0.1 22.59 5.00 41.50 0.04
30 20 0.2 3.25 0.00 14.50 0.04
31 20 0.3 0.56 0.00 5.00 0.04
32 20 0.4 1.46 0.00 3.25 0.07
33 20 0.5 36.75 1.25 99.00 0.26
34 20 0.6 99.80 99.00 100.00 0.23
35 20 0.7 99.95 99.00 100.00 0.13
36 20 0.8 100.00 100.00 100.00 0.09
37 20 0.9 100.00 100.00 100.00 0.05
38 25 0.1 16.00 7.84 32.80 0.06
39 25 0.2 0.00 0.00 0.00 0.04
40 25 0.3 0.05 0.00 0.64 0.06
41 25 0.4 0.89 0.00 9.44 0.12
42 25 0.5 19.13 1.12 96.48 0.35
43 25 0.6 99.25 94.88 100.00 0.50
44 25 0.7 99.90 99.36 100.00 0.24
45 25 0.8 100.00 100.00 100.00 0.15
46 25 0.9 99.97 99.36 100.00 0.08
47 30 0.1 7.99 0.00 16.00 0.08
48 30 0.2 0.17 0.00 3.33 0.07
49 30 0.3 0.00 0.00 0.00 0.08
50 30 0.4 0.38 0.00 4.11 0.18
51 30 0.5 5.42 0.78 21.44 0.41
52 30 0.6 99.42 94.78 100.00 0.94
53 30 0.7 99.93 99.56 100.00 0.42
54 30 0.8 100.00 100.00 100.00 0.22
55 30 0.9 100.00 100.00 100.00 0.13
56 35 0.1 5.65 0.00 13.80 0.11
57 35 0.2 0.14 0.00 2.86 0.10
58 35 0.3 0.00 0.00 0.00 0.13
59 35 0.4 0.13 0.00 0.57 0.22
60 35 0.5 4.42 0.41 23.18 0.56
61 35 0.6 91.11 8.41 100.00 1.58
62 35 0.7 99.97 99.67 100.00 0.60
63 35 0.8 100.00 100.00 100.00 0.33
64 35 0.9 100.00 100.00 100.00 0.17
65 40 0.1 2.73 0.00 9.81 0.13
66 40 0.2 0.00 0.00 0.00 0.13
67 40 0.3 0.00 0.00 0.00 0.18
68 40 0.4 0.03 0.00 0.31 0.27
69 40 0.5 2.14 0.00 9.56 0.69
70 40 0.6 91.44 22.31 100.00 2.92
71 40 0.7 99.99 99.75 100.00 0.84
72 40 0.8 100.00 100.00 100.00 0.43
73 40 0.9 100.00 100.00 100.00 0.24
74 45 0.1 1.78 0.00 4.69 0.17
75 45 0.2 0.00 0.00 0.00 0.17
76 45 0.3 0.00 0.00 0.00 0.24
77 45 0.4 0.01 0.00 0.15 0.33
78 45 0.5 1.44 0.40 5.14 0.96
79 45 0.6 81.72 6.02 100.00 5.28
80 45 0.7 99.94 99.41 100.00 1.28
81 45 0.8 100.00 100.00 100.00 0.64
82 45 0.9 100.00 100.00 100.00 0.30
83 50 0.1 1.70 0.00 5.92 0.20
84 50 0.2 0.00 0.00 0.00 0.23
85 50 0.3 0.00 0.00 0.00 0.30
86 50 0.4 0.01 0.00 0.16 0.38
87 50 0.5 0.51 0.00 1.80 0.90
88 50 0.6 73.26 5.60 100.00 7.94
89 50 0.7 99.96 99.76 100.00 1.63
90 50 0.8 100.00 100.00 100.00 0.83
91 50 0.9 100.00 100.00 100.00 0.41
92 60 0.1 0.17 0.00 1.67 0.24
93 60 0.2 0.00 0.00 0.00 0.35
94 60 0.3 0.00 0.00 0.00 0.50
95 60 0.4 0.00 0.00 0.06 0.64
96 60 0.5 0.23 0.00 1.67 1.24
97 60 0.6 35.02 1.33 100.00 10.76
98 60 0.7 99.97 99.83 100.00 2.96
99 60 0.8 100.00 100.00 100.00 1.27
100 60 0.9 100.00 100.00 100.00 0.62
101 70 0.1 0.14 0.00 1.43 0.34
102 70 0.2 0.00 0.00 0.00 0.54
103 70 0.3 0.00 0.00 0.00 0.76
104 70 0.4 0.00 0.00 0.00 0.96
105 70 0.5 0.04 0.00 0.29 1.49
106 70 0.6 16.40 1.00 99.71 22.21
107 70 0.7 99.97 99.73 100.00 4.92
108 70 0.8 100.00 99.92 100.00 2.03
109 70 0.9 100.00 100.00 100.00 0.89
110 80 0.1 0.00 0.00 0.00 0.44
111 80 0.2 0.00 0.00 0.00 0.78
112 80 0.3 0.00 0.00 0.00 1.07
113 80 0.4 0.00 0.00 0.00 1.34
114 80 0.5 0.01 0.00 0.08 1.97
115 80 0.6 1.21 0.41 2.30 3.92
116 80 0.7 99.98 99.94 100.00 7.79
117 80 0.8 100.00 100.00 100.00 3.14
118 80 0.9 100.00 100.00 100.00 1.31

View File

@@ -0,0 +1,938 @@
[
{
"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
}
]

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "vue-nonograms-solid", "name": "vue-nonograms-solid",
"version": "1.4.0", "version": "1.9.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "vue-nonograms-solid", "name": "vue-nonograms-solid",
"version": "1.4.0", "version": "1.9.0",
"dependencies": { "dependencies": {
"fireworks-js": "^2.10.8", "fireworks-js": "^2.10.8",
"flag-icons": "^7.5.0", "flag-icons": "^7.5.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "vue-nonograms-solid", "name": "vue-nonograms-solid",
"version": "1.4.0", "version": "1.9.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -0,0 +1,61 @@
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, '../src/composables/useI18n.js');
let content = fs.readFileSync(filePath, 'utf8');
const newKeys = {
'custom.simulationHelp': 'How is this calculated?',
'custom.hideMap': 'Hide difficulty map',
'custom.showMap': 'Show difficulty map',
'simulation.title': 'Difficulty Simulation',
'simulation.status.ready': 'Ready',
'simulation.status.stopped': 'Stopped',
'simulation.status.completed': 'Completed',
'simulation.status.simulating': 'Simulating {size}x{size} @ {density}%',
'simulation.start': 'Start Simulation',
'simulation.stop': 'Stop',
'simulation.table.size': 'Size',
'simulation.table.density': 'Density',
'simulation.table.solved': 'Solved (Logic)',
'simulation.empty': 'Press Start to run Monte Carlo simulation'
};
const lines = content.split('\n');
const processedLines = [];
let currentLang = null;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// Detect start of language block
const startMatch = line.match(/^\s{2}(['"]?[\w-]+['"]?): \{/);
if (startMatch) {
currentLang = startMatch[1].replace(/['"]/g, '');
}
// Detect end of language block
if (currentLang && (line.trim() === '},' || line.trim() === '}')) {
if (currentLang !== 'pl' && currentLang !== 'en') {
// Ensure previous line has comma
if (processedLines.length > 0) {
let lastLine = processedLines[processedLines.length - 1];
if (!lastLine.trim().endsWith(',') && !lastLine.trim().endsWith('{')) {
processedLines[processedLines.length - 1] = lastLine + ',';
}
}
// Append new keys
Object.entries(newKeys).forEach(([key, value]) => {
processedLines.push(` '${key}': '${value}',`);
});
}
currentLang = null;
}
processedLines.push(line);
}
const finalContent = processedLines.join('\n');
fs.writeFileSync(filePath, finalContent);
console.log('Successfully added simulation translations to all languages.');

View File

@@ -0,0 +1,97 @@
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, '../src/composables/useI18n.js');
let content = fs.readFileSync(filePath, 'utf8');
const newKeys = {
'custom.simulationHelp': 'How is this calculated?',
'simulation.title': 'Difficulty Simulation',
'simulation.status.ready': 'Ready',
'simulation.status.stopped': 'Stopped',
'simulation.status.completed': 'Completed',
'simulation.status.simulating': 'Simulating {size}x{size} @ {density}%',
'simulation.start': 'Start Simulation',
'simulation.stop': 'Stop',
'simulation.table.size': 'Size',
'simulation.table.density': 'Density',
'simulation.table.solved': 'Solved (Logic)',
'simulation.empty': 'Press Start to run Monte Carlo simulation'
};
// Regex to match the end of a language block
// Matches " }," or " }" at the start of a line
const blockEndRegex = /^(\s{2})\},?$/gm;
let newContent = content.replace(blockEndRegex, (match, indent, offset) => {
// Determine which language we are closing
const precedingText = content.substring(0, offset);
const langMatch = precedingText.match(/^\s{2}(\w+([-]\w+)?): \{/gm);
if (!langMatch) return match;
const currentLangLine = langMatch[langMatch.length - 1];
const currentLang = currentLangLine.match(/^\s{2}(\w+([-]\w+)?): \{/)[1];
// Skip pl and en as they are already updated
if (currentLang === 'pl' || currentLang === 'en') {
return match;
}
// Check if the previous line has a comma
// We need to look at the lines before the match
// This is tricky with replace callback.
// Easier strategy: Just insert the keys.
// If the file is well formatted, the last item might or might not have a comma.
// But we can ensure *our* inserted block starts with a comma if needed?
// No, standard JS objects need comma after previous item.
// Let's assume we simply inject before the closing brace.
// We'll add a comma to the previous line if it doesn't have one?
// That's hard with regex replace on the closing brace only.
// Alternative: Split by lines and process.
return match; // Placeholder, we will process by lines below.
});
const lines = content.split('\n');
const processedLines = [];
let currentLang = null;
for (let i = 0; i < lines.length; i++) {
let line = lines[i];
// Detect start of language block
const startMatch = line.match(/^\s{2}(['"]?[\w-]+['"]?): \{/);
if (startMatch) {
currentLang = startMatch[1].replace(/['"]/g, '');
}
// Detect end of language block
if (currentLang && (line.trim() === '},' || line.trim() === '}')) {
if (currentLang !== 'pl' && currentLang !== 'en') {
// Ensure previous line has comma
if (processedLines.length > 0) {
let lastLine = processedLines[processedLines.length - 1];
if (!lastLine.trim().endsWith(',') && !lastLine.trim().endsWith('{')) {
processedLines[processedLines.length - 1] = lastLine + ',';
}
}
// Append new keys
Object.entries(newKeys).forEach(([key, value]) => {
processedLines.push(` '${key}': '${value}',`);
});
// Remove trailing comma from last inserted item if we want strictly JSON-like (but JS allows it)
// It's fine to leave it.
}
currentLang = null;
}
processedLines.push(line);
}
const finalContent = processedLines.join('\n');
fs.writeFileSync(filePath, finalContent);
console.log('Successfully added simulation translations to all languages.');

View File

@@ -0,0 +1,75 @@
import fs from 'fs';
import path from 'path';
import { generateRandomGrid, calculateHints } from '../src/utils/puzzleUtils.js';
import { solvePuzzle } from '../src/utils/solver.js';
const OUTPUT_FILE = 'difficulty_simulation_results.json';
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 SAMPLES_PER_POINT = 20; // Adjust based on time/accuracy needs
console.log('Starting Monte Carlo Simulation for Nonogram Difficulty...');
console.log(`Config: Sizes=${SIZES.length}, Densities=${DENSITIES.length}, Samples=${SAMPLES_PER_POINT}`);
const results = [];
const csvRows = ['size,density,avg_solved_percent,min_solved_percent,max_solved_percent,avg_time_ms'];
const startTime = Date.now();
for (const size of SIZES) {
for (const density of DENSITIES) {
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_PER_POINT; i++) {
const t0 = performance.now();
// 1. Generate
const grid = generateRandomGrid(size, density);
const { rowHints, colHints } = calculateHints(grid);
// 2. Solve
const { percentSolved } = solvePuzzle(rowHints, colHints);
const t1 = performance.now();
totalSolved += percentSolved;
minSolved = Math.min(minSolved, percentSolved);
maxSolved = Math.max(maxSolved, percentSolved);
totalTime += (t1 - t0);
}
const avgSolved = totalSolved / SAMPLES_PER_POINT;
const avgTime = totalTime / SAMPLES_PER_POINT;
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)}%`);
}
}
const totalDuration = (Date.now() - startTime) / 1000;
console.log(`Simulation complete in ${totalDuration.toFixed(1)}s`);
// 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}`);

View File

@@ -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 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';
@@ -15,6 +16,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 showSimulation = ref(false);
const showGuide = ref(false); const showGuide = ref(false);
const deferredPrompt = ref(null); const deferredPrompt = ref(null);
const canInstall = ref(false); const canInstall = ref(false);
@@ -173,7 +175,8 @@ onUnmounted(() => {
<!-- Modals Teleport --> <!-- Modals Teleport -->
<Teleport to="body"> <Teleport to="body">
<WinModal v-if="store.isGameWon" /> <WinModal v-if="store.isGameWon" />
<CustomGameModal v-if="showCustomModal" @close="showCustomModal = false" /> <CustomGameModal v-if="showCustomModal" @close="showCustomModal = false" @open-simulation="showSimulation = true" />
<SimulationView v-if="showSimulation" @close="showSimulation = false" />
<ReloadPrompt /> <ReloadPrompt />
</Teleport> </Teleport>
</main> </main>

View File

@@ -1,16 +1,201 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { usePuzzleStore } from '@/stores/puzzle'; import { usePuzzleStore } from '@/stores/puzzle';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { calculateDifficulty } from '@/utils/puzzleUtils'; import { calculateDifficulty } from '@/utils/puzzleUtils';
import { HelpCircle } from 'lucide-vue-next';
const emit = defineEmits(['close']); const emit = defineEmits(['close', 'open-simulation']);
const store = usePuzzleStore(); const store = usePuzzleStore();
const { t } = useI18n(); const { t } = useI18n();
const customSize = ref(10); const customSize = ref(10);
const fillRate = ref(50); const fillRate = ref(50);
const errorMsg = ref(''); const errorMsg = ref('');
const difficultyCanvas = ref(null);
const isDragging = ref(false);
const cachedBackground = ref(null);
const drawMap = () => {
const canvas = difficultyCanvas.value;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const width = canvas.width;
const height = canvas.height;
// Clear
ctx.clearRect(0, 0, width, height);
// Use cached background if available
if (cachedBackground.value) {
ctx.putImageData(cachedBackground.value, 0, 0);
} else {
// Draw Gradient Background (Heavy calculation)
const imgData = ctx.createImageData(width, height);
const data = imgData.data;
// Ranges:
// X: Fill Rate 10% -> 90%
// Y: Size 5 -> 80
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const normalizedX = x / width;
const normalizedY = 1 - (y / height); // 0 at bottom, 1 at top
const fRate = 0.1 + normalizedX * 0.8; // 0.1 to 0.9
const sSize = 5 + normalizedY * 75; // 5 to 80
const { value } = calculateDifficulty(fRate, sSize);
// Color Mapping
const hue = 120 * (1 - value / 100);
const [r, g, b] = hslToRgb(hue / 360, 1, 0.5);
const index = (y * width + x) * 4;
data[index] = r;
data[index + 1] = g;
data[index + 2] = b;
data[index + 3] = 255; // Alpha
}
}
ctx.putImageData(imgData, 0, 0);
cachedBackground.value = imgData;
}
// Draw current position
// Map current fillRate/size to x,y
// Fill: 10..90. Size: 5..80.
const currentFill = Math.max(10, Math.min(90, fillRate.value));
const currentSize = Math.max(5, Math.min(80, customSize.value));
const posX = ((currentFill - 10) / 80) * width;
const posY = (1 - (currentSize - 5) / 75) * height;
// Draw Crosshair/Circle
ctx.beginPath();
ctx.arc(posX, posY, 6, 0, Math.PI * 2);
ctx.fillStyle = '#fff';
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = '#000';
ctx.stroke();
};
const hslToRgb = (h, s, l) => {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
};
const updateFromEvent = (e) => {
const canvas = difficultyCanvas.value;
if (!canvas) return;
const rect = canvas.getBoundingClientRect();
// Handle Touch or Mouse
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
let x = clientX - rect.left;
let y = clientY - rect.top;
// Clamp
x = Math.max(0, Math.min(rect.width, x));
y = Math.max(0, Math.min(rect.height, y));
// Reverse Map
// x / width -> fillRate (10..90)
// 1 - y / height -> size (5..80)
const normalizedX = x / rect.width;
const normalizedY = 1 - (y / rect.height);
const newFill = 10 + normalizedX * 80;
const newSize = 5 + normalizedY * 75;
fillRate.value = Math.round(newFill);
customSize.value = Math.round(newSize);
};
const startDrag = (e) => {
isDragging.value = true;
updateFromEvent(e);
// Add global listeners for mouse to handle dragging outside canvas
window.addEventListener('mousemove', onDrag);
window.addEventListener('mouseup', stopDrag);
};
const onDrag = (e) => {
if (!isDragging.value) return;
updateFromEvent(e);
};
const stopDrag = () => {
isDragging.value = false;
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mouseup', stopDrag);
};
onUnmounted(() => {
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mouseup', stopDrag);
});
const showAdvanced = ref(false);
const toggleAdvanced = () => {
showAdvanced.value = !showAdvanced.value;
if (showAdvanced.value) {
// Reset cache when opening to ensure size is correct if canvas resized
cachedBackground.value = null;
nextTick(drawMap);
}
};
onMounted(() => {
const savedSize = localStorage.getItem('nonograms_custom_size');
if (savedSize && !isNaN(savedSize)) {
customSize.value = Math.max(5, Math.min(80, Number(savedSize)));
}
const savedFillRate = localStorage.getItem('nonograms_custom_fill_rate');
if (savedFillRate && !isNaN(savedFillRate)) {
fillRate.value = Math.max(10, Math.min(90, Number(savedFillRate)));
}
// Don't draw map initially if hidden
});
watch([customSize, fillRate], () => {
if (showAdvanced.value) {
drawMap();
}
});
watch(customSize, (newVal) => {
localStorage.setItem('nonograms_custom_size', newVal);
});
watch(fillRate, (newVal) => {
localStorage.setItem('nonograms_custom_fill_rate', newVal);
});
const snapToStep = (value, step) => { const snapToStep = (value, step) => {
const rounded = Math.round(value / step) * step; const rounded = Math.round(value / step) * step;
@@ -21,12 +206,12 @@ const handleSnap = () => {
customSize.value = snapToStep(Number(customSize.value), 5); customSize.value = snapToStep(Number(customSize.value), 5);
}; };
const difficultyLevel = computed(() => { const difficultyInfo = computed(() => {
return calculateDifficulty(fillRate.value / 100); return calculateDifficulty(fillRate.value / 100, customSize.value);
}); });
const difficultyColor = computed(() => { const difficultyColor = computed(() => {
switch(difficultyLevel.value) { switch(difficultyInfo.value.level) {
case 'extreme': return '#ff3333'; case 'extreme': return '#ff3333';
case 'hardest': return '#ff9933'; case 'hardest': return '#ff9933';
case 'harder': return '#ffff33'; case 'harder': return '#ffff33';
@@ -51,45 +236,77 @@ const confirm = () => {
<div class="modal-overlay" @click.self="emit('close')"> <div class="modal-overlay" @click.self="emit('close')">
<div class="modal glass-panel"> <div class="modal glass-panel">
<h2>{{ t('custom.title') }}</h2> <h2>{{ t('custom.title') }}</h2>
<p>{{ t('custom.prompt') }}</p>
<div class="input-group"> <div class="modal-content">
<div class="range-value">{{ customSize }}</div> <div class="sliders-section">
<input <div class="slider-container">
type="range" <p>{{ t('custom.prompt') }}</p>
v-model="customSize" <div class="input-group">
min="5" <div class="range-value">{{ customSize }}</div>
max="80" <input
step="1" type="range"
@change="handleSnap" v-model="customSize"
/> min="5"
<div class="range-scale"> max="80"
<span>5</span> step="1"
<span>80</span> @change="handleSnap"
</div> />
</div> <div class="range-scale">
<span>5</span>
<span>80</span>
</div>
</div>
</div>
<p>{{ t('custom.fillRate') }}</p> <div class="slider-container">
<div class="input-group"> <p>{{ t('custom.fillRate') }}</p>
<div class="range-value">{{ fillRate }}%</div> <div class="input-group">
<input <div class="range-value">{{ fillRate }}%</div>
type="range" <input
v-model="fillRate" type="range"
min="10" v-model="fillRate"
max="90" min="10"
step="5" max="90"
/> step="1"
<div class="range-scale"> />
<span>10%</span> <div class="range-scale">
<span>90%</span> <span>10%</span>
<span>90%</span>
</div>
</div>
</div>
</div>
<div class="map-section" v-if="showAdvanced">
<canvas
ref="difficultyCanvas"
width="400"
height="400"
@mousedown="startDrag"
@touchstart.prevent="startDrag"
@touchmove.prevent="onDrag"
@touchend="stopDrag"
></canvas>
</div> </div>
</div> </div>
<div class="difficulty-indicator"> <div class="difficulty-indicator">
<span class="label">{{ t('custom.difficulty') }}:</span> <div class="label-row">
<span class="value" :style="{ color: difficultyColor }"> <div class="label">{{ t('custom.difficulty') }}</div>
{{ t(`difficulty.${difficultyLevel}`) }} <button class="help-btn" @click="emit('open-simulation')" :title="t('custom.simulationHelp') || 'How is this calculated?'">
</span> <HelpCircle class="icon-sm" />
</button>
</div>
<div class="difficulty-row">
<div class="level" :style="{ color: difficultyColor }">{{ t(`difficulty.${difficultyInfo.level}`) }}</div>
<div class="percentage" :style="{ color: difficultyColor }">({{ difficultyInfo.value }}%)</div>
</div>
</div>
<div class="advanced-toggle">
<button class="btn-text" @click="toggleAdvanced">
{{ showAdvanced ? t('custom.hideMap') : t('custom.showMap') }}
</button>
</div> </div>
<p v-if="errorMsg" class="error">{{ errorMsg }}</p> <p v-if="errorMsg" class="error">{{ errorMsg }}</p>
@@ -121,7 +338,7 @@ const confirm = () => {
.modal { .modal {
padding: 40px; padding: 40px;
text-align: center; text-align: center;
max-width: 400px; max-width: 800px;
width: 90%; width: 90%;
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);
@@ -129,6 +346,54 @@ const confirm = () => {
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
} }
.modal-content {
display: flex;
flex-direction: row;
gap: 40px;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
@media (max-width: 700px) {
.modal-content {
flex-direction: column;
gap: 20px;
}
}
.sliders-section {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.map-section {
flex: 0 0 auto;
display: flex;
justify-content: center;
align-items: center;
}
canvas {
width: 400px;
height: 400px;
border: 2px solid var(--panel-border);
border-radius: 8px;
box-shadow: 0 0 20px rgba(0, 242, 255, 0.1);
cursor: crosshair;
background: #000;
}
@media (max-width: 600px) {
canvas {
width: 100%;
height: auto;
aspect-ratio: 1;
}
}
h2 { h2 {
font-size: 2rem; font-size: 2rem;
color: var(--accent-cyan); color: var(--accent-cyan);
@@ -142,6 +407,11 @@ p {
margin-bottom: 20px; margin-bottom: 20px;
} }
.slider-container {
width: 100%;
margin-bottom: 10px;
}
.input-group { .input-group {
margin-bottom: 20px; margin-bottom: 20px;
display: flex; display: flex;
@@ -202,14 +472,66 @@ input[type="range"]::-moz-range-thumb {
} }
.difficulty-indicator { .difficulty-indicator {
margin: 20px 0; margin: 20px 0 40px 0;
font-size: 1.2rem;
display: flex; display: flex;
justify-content: center; flex-direction: column;
gap: 10px;
align-items: center; align-items: center;
gap: 5px;
}
.label-row {
display: flex;
align-items: center;
gap: 8px;
}
.help-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
padding: 4px;
border-radius: 50%;
transition: color 0.3s, background 0.3s;
}
.help-btn:hover {
color: var(--accent-cyan);
background: rgba(0, 242, 255, 0.1);
}
.icon-sm {
width: 16px;
height: 16px;
}
.difficulty-row {
display: flex;
flex-direction: row;
gap: 8px;
align-items: baseline;
justify-content: center;
white-space: nowrap; white-space: nowrap;
height: 1.5em; /* Reserve space for one line of text */ flex-wrap: nowrap;
}
.label {
font-size: 1rem;
color: var(--text-muted);
}
.level {
font-size: 1.4rem;
font-weight: bold;
text-transform: uppercase;
line-height: 1.2;
}
.percentage {
font-size: 1rem;
font-weight: bold;
} }
.difficulty-indicator .label { .difficulty-indicator .label {
@@ -231,6 +553,25 @@ input[type="range"]::-moz-range-thumb {
font-size: 0.9rem; font-size: 0.9rem;
} }
.btn-text {
background: none;
border: none;
color: var(--accent-cyan);
font-size: 0.9rem;
cursor: pointer;
text-decoration: underline;
opacity: 0.8;
transition: opacity 0.3s;
}
.btn-text:hover {
opacity: 1;
}
.advanced-toggle {
margin-bottom: 10px;
}
.actions { .actions {
display: flex; display: flex;
gap: 15px; gap: 15px;

View File

@@ -0,0 +1,320 @@
<script setup>
import { ref, computed } from 'vue';
import { generateRandomGrid, calculateHints } from '@/utils/puzzleUtils';
import { solvePuzzle } from '@/utils/solver';
import { useI18n } from '@/composables/useI18n';
import { X, Play, Square, RotateCcw } from 'lucide-vue-next';
const emit = defineEmits(['close']);
const { t } = useI18n();
const SIZES = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50];
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 isRunning = ref(false);
const progress = ref(0);
const currentStatus = ref('');
const results = ref([]);
const simulationSpeed = ref(1); // 1 = Normal, 2 = Fast (less render updates)
let stopRequested = false;
const displayStatus = computed(() => {
if (!currentStatus.value) return t('simulation.status.ready');
return currentStatus.value;
});
const startSimulation = async () => {
if (isRunning.value) return;
isRunning.value = true;
stopRequested = false;
results.value = [];
progress.value = 0;
const totalSteps = SIZES.length * DENSITIES.length;
let stepCount = 0;
for (const size of SIZES) {
for (const density of DENSITIES) {
if (stopRequested) {
currentStatus.value = t('simulation.status.stopped');
isRunning.value = false;
return;
}
currentStatus.value = t('simulation.status.simulating', {
size: size,
density: (density * 100).toFixed(0)
});
let totalSolved = 0;
// Run samples
for (let i = 0; i < SAMPLES_PER_POINT; i++) {
const grid = generateRandomGrid(size, density);
const { rowHints, colHints } = calculateHints(grid);
const { percentSolved } = solvePuzzle(rowHints, colHints);
totalSolved += percentSolved;
// Yield to UI every few samples to keep it responsive
if (i % 2 === 0) await new Promise(r => setTimeout(r, 0));
}
const avgSolved = totalSolved / SAMPLES_PER_POINT;
results.value.unshift({
size,
density,
avgSolved: avgSolved.toFixed(1)
});
stepCount++;
progress.value = (stepCount / totalSteps) * 100;
}
}
isRunning.value = false;
currentStatus.value = t('simulation.status.completed');
};
const stopSimulation = () => {
stopRequested = true;
};
const getRowColor = (solved) => {
if (solved >= 90) return 'color-easy';
if (solved >= 60) return 'color-harder';
if (solved >= 30) return 'color-hardest';
return 'color-extreme';
};
</script>
<template>
<div class="modal-overlay" @click.self="emit('close')">
<div class="modal glass-panel">
<div class="header">
<h2>{{ t('simulation.title') }}</h2>
<button class="close-btn" @click="emit('close')">
<X />
</button>
</div>
<div class="content">
<div class="controls">
<div class="status-bar">
<div class="status-text">{{ displayStatus }}</div>
<div class="progress-track">
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
</div>
</div>
<div class="actions">
<button v-if="!isRunning" class="btn-neon" @click="startSimulation">
<Play class="icon" /> {{ t('simulation.start') }}
</button>
<button v-else class="btn-neon secondary" @click="stopSimulation">
<Square class="icon" /> {{ t('simulation.stop') }}
</button>
</div>
</div>
<div class="results-container">
<table class="results-table">
<thead>
<tr>
<th>{{ t('simulation.table.size') }}</th>
<th>{{ t('simulation.table.density') }}</th>
<th>{{ t('simulation.table.solved') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in results" :key="idx" :class="getRowColor(row.avgSolved)">
<td>{{ row.size }}x{{ row.size }}</td>
<td>{{ (row.density * 100).toFixed(0) }}%</td>
<td>{{ row.avgSolved }}%</td>
</tr>
</tbody>
</table>
<div v-if="results.length === 0" class="empty-state">
{{ t('simulation.empty') }}
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: var(--modal-overlay);
backdrop-filter: blur(5px);
display: flex;
justify-content: center;
align-items: center;
z-index: 3000;
animation: fadeIn 0.3s ease;
}
.modal {
padding: 30px;
width: 90%;
max-width: 600px;
height: 80vh;
display: flex;
flex-direction: column;
border: 1px solid var(--accent-cyan);
box-shadow: 0 0 50px rgba(0, 242, 255, 0.2);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
h2 {
color: var(--accent-cyan);
margin: 0;
font-size: 1.5rem;
}
.close-btn {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 5px;
}
.close-btn:hover {
color: var(--text-color);
}
.content {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
overflow: hidden;
}
.controls {
display: flex;
flex-direction: column;
gap: 15px;
padding-bottom: 15px;
border-bottom: 1px solid var(--panel-border);
}
.status-bar {
display: flex;
flex-direction: column;
gap: 5px;
}
.status-text {
font-size: 0.9rem;
color: var(--text-muted);
}
.progress-track {
width: 100%;
height: 4px;
background: var(--panel-bg-strong);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent-cyan);
transition: width 0.3s ease;
}
.actions {
display: flex;
justify-content: flex-end;
}
.btn-neon {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 0.9rem;
}
.icon {
width: 16px;
height: 16px;
}
.results-container {
flex: 1;
overflow-y: auto;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
padding: 10px;
}
.results-table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
}
.results-table th {
text-align: left;
padding: 8px;
color: var(--text-muted);
border-bottom: 1px solid var(--panel-border);
position: sticky;
top: 0;
background: var(--panel-bg);
}
.results-table td {
padding: 8px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.color-easy { color: #33ff33; }
.color-harder { color: #ffff33; }
.color-hardest { color: #ff9933; }
.color-extreme { color: #ff3333; }
.empty-state {
padding: 40px;
text-align: center;
color: var(--text-muted);
font-style: italic;
}
/* Scrollbar styling */
.results-container::-webkit-scrollbar {
width: 8px;
}
.results-container::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
.results-container::-webkit-scrollbar-thumb {
background: var(--panel-border);
border-radius: 4px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>

View File

@@ -4,7 +4,7 @@ import { Fireworks } from 'fireworks-js';
import { usePuzzleStore } from '@/stores/puzzle'; import { usePuzzleStore } from '@/stores/puzzle';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useTimer } from '@/composables/useTimer'; import { useTimer } from '@/composables/useTimer';
import { Download } from 'lucide-vue-next'; import { Download, FileCode } from 'lucide-vue-next';
import { calculateDifficulty } from '@/utils/puzzleUtils'; import { calculateDifficulty } from '@/utils/puzzleUtils';
const store = usePuzzleStore(); const store = usePuzzleStore();
@@ -41,30 +41,43 @@ const playFanfare = async () => {
} }
} }
masterGain = audioContext.createGain(); masterGain = audioContext.createGain();
masterGain.gain.value = 0.18; masterGain.gain.value = 0.25; // Slightly louder but softer tone
masterGain.connect(audioContext.destination); masterGain.connect(audioContext.destination);
const notes = [
{ time: 0.0, dur: 0.18, freqs: [523.25, 659.25, 783.99] },
{ time: 0.2, dur: 0.18, freqs: [587.33, 740.0, 880.0] },
{ time: 0.4, dur: 0.22, freqs: [659.25, 830.61, 987.77] },
{ time: 0.7, dur: 0.35, freqs: [698.46, 880.0, 1046.5] }
];
const now = audioContext.currentTime; const now = audioContext.currentTime;
notes.forEach(({ time, dur, freqs }) => {
freqs.forEach((freq) => { const playNote = (freq, startTime, duration) => {
const osc = audioContext.createOscillator(); const osc = audioContext.createOscillator();
const gain = audioContext.createGain(); const gain = audioContext.createGain();
osc.type = 'triangle';
osc.frequency.value = freq; // Mix of sine and triangle for a bell-like quality
gain.gain.setValueAtTime(0.0001, now + time); osc.type = 'sine';
gain.gain.linearRampToValueAtTime(0.8, now + time + 0.02); osc.frequency.value = freq;
gain.gain.exponentialRampToValueAtTime(0.0001, now + time + dur);
osc.connect(gain); // Envelope for elegant bell/chime sound
gain.connect(masterGain); gain.gain.setValueAtTime(0, startTime);
osc.start(now + time); gain.gain.linearRampToValueAtTime(0.4, startTime + 0.05); // Soft attack
osc.stop(now + time + dur + 0.05); gain.gain.exponentialRampToValueAtTime(0.01, startTime + duration); // Long release
});
}); osc.connect(gain);
gain.connect(masterGain);
osc.start(startTime);
osc.stop(startTime + duration + 0.1);
};
// C Major 7 Arpeggio sequence (C5, E5, G5, B5, C6) - Elegant & Uplifting
const sequence = [
{ freq: 523.25, time: 0.0, dur: 0.8 }, // C5
{ freq: 659.25, time: 0.1, dur: 0.8 }, // E5
{ freq: 783.99, time: 0.2, dur: 0.8 }, // G5
{ freq: 987.77, time: 0.3, dur: 0.8 }, // B5 (Maj7)
{ freq: 1046.50, time: 0.4, dur: 2.0 }, // C6 (High C resolve)
// Add a bass root note at the end for fullness
{ freq: 523.25, time: 0.4, dur: 2.0 } // C5
];
sequence.forEach(note => playNote(note.freq, now + note.time, note.dur));
}; };
const triggerVibration = () => { const triggerVibration = () => {
@@ -175,7 +188,11 @@ const buildShareCanvas = () => {
if (store.guideUsageCount > 0) { if (store.guideUsageCount > 0) {
ctx.fillStyle = '#ff4d4d'; ctx.fillStyle = '#ff4d4d';
ctx.font = '600 14px "Segoe UI", sans-serif'; ctx.font = '600 14px "Segoe UI", sans-serif';
const guideText = t('win.usedGuide', { count: store.guideUsageCount });
const totalCells = store.size * store.size;
const percent = Math.min(100, Math.round((store.guideUsageCount / totalCells) * 100));
const guideText = t('win.usedGuide', { count: store.guideUsageCount, percent });
ctx.fillText(`⚠️ ${guideText}`, padding, height - padding - footerHeight + 10); ctx.fillText(`⚠️ ${guideText}`, padding, height - padding - footerHeight + 10);
} }
@@ -185,6 +202,125 @@ const buildShareCanvas = () => {
return canvas; return canvas;
}; };
const buildShareSVG = () => {
const grid = store.playerGrid;
if (!grid || !grid.length) return null;
const appUrl = 'https://nonograms.7u.pl/';
const size = store.size;
const maxBoard = 640;
const cellSize = Math.max(8, Math.floor(maxBoard / size));
const boardSize = cellSize * size;
const padding = 28;
const headerHeight = 64;
const footerHeight = 28;
const infoHeight = 40;
const width = boardSize + padding * 2;
const height = boardSize + padding * 2 + headerHeight + footerHeight + infoHeight;
// Colors
const bgGradientStart = '#1b2a4a';
const bgGradientEnd = '#0a1324';
const overlayColor = 'rgba(0, 0, 0, 0.35)';
const textColor = '#e8fbff';
const gridColor = 'rgba(255, 255, 255, 0.06)';
const gridLineColor = 'rgba(255, 255, 255, 0.12)';
const filledColor = '#00f2fe';
const crossColor = 'rgba(255, 255, 255, 0.5)';
const urlColor = 'rgba(255, 255, 255, 0.75)';
// Difficulty Logic
const densityPercent = Math.round(store.currentDensity * 100);
const diffInfo = calculateDifficulty(store.currentDensity, store.size);
const difficultyKey = diffInfo.level;
let diffColor = '#33ff33';
if (difficultyKey === 'extreme') diffColor = '#ff3333';
else if (difficultyKey === 'hardest') diffColor = '#ff9933';
else if (difficultyKey === 'harder') diffColor = '#ffff33';
const difficultyText = t(`difficulty.${difficultyKey}`);
const diffLabel = `${t('win.difficulty')} ${difficultyText} (${densityPercent}%)`;
let svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">`;
// Background
svgContent += `
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="${bgGradientStart}"/>
<stop offset="100%" stop-color="${bgGradientEnd}"/>
</linearGradient>
</defs>
<rect width="100%" height="100%" fill="url(#bg)"/>
<rect x="12" y="12" width="${width - 24}" height="${height - 24}" fill="${overlayColor}"/>
`;
// Text: Title & Time
svgContent += `
<text x="${padding}" y="${padding + 28}" font-family="Segoe UI, sans-serif" font-weight="700" font-size="26" fill="${textColor}">${t('app.title')}</text>
<text x="${padding}" y="${padding + 56}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="16" fill="${textColor}">${t('win.time')} ${formattedTime.value}</text>
`;
// Text: Difficulty (Right Aligned - manual approx or end anchor)
svgContent += `
<text x="${width - padding}" y="${padding + 56}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="${diffColor}" text-anchor="end">${diffLabel}</text>
`;
const gridX = padding;
const gridY = padding + headerHeight;
// Grid Background
svgContent += `<rect x="${gridX}" y="${gridY}" width="${boardSize}" height="${boardSize}" fill="${gridColor}"/>`;
// Grid Lines
let gridLines = '';
for (let i = 0; i <= size; i++) {
const pos = i * cellSize;
// Vertical
gridLines += `<line x1="${gridX + pos}" y1="${gridY}" x2="${gridX + pos}" y2="${gridY + boardSize}" stroke="${gridLineColor}" stroke-width="1"/>`;
// Horizontal
gridLines += `<line x1="${gridX}" y1="${gridY + pos}" x2="${gridX + boardSize}" y2="${gridY + pos}" stroke="${gridLineColor}" stroke-width="1"/>`;
}
svgContent += gridLines;
// Cells
let cells = '';
const lineWidth = Math.max(1.5, Math.floor(cellSize * 0.12));
for (let r = 0; r < size; r++) {
for (let c = 0; c < size; c++) {
const state = grid[r]?.[c];
const cx = gridX + c * cellSize;
const cy = gridY + r * cellSize;
if (state === 1) { // Filled
cells += `<rect x="${cx + 1}" y="${cy + 1}" width="${cellSize - 2}" height="${cellSize - 2}" fill="${filledColor}"/>`;
} else if (state === 2) { // Cross
const d = cellSize * 0.6;
const off = cellSize * 0.2;
cells += `
<path d="M${cx + off} ${cy + off} L${cx + off + d} ${cy + off + d} M${cx + off + d} ${cy + off} L${cx + off} ${cy + off + d}"
stroke="${crossColor}" stroke-width="${lineWidth}" stroke-linecap="round"/>
`;
}
}
}
svgContent += cells;
// Guide Usage
if (store.guideUsageCount > 0) {
const totalCells = store.size * store.size;
const percent = Math.min(100, Math.round((store.guideUsageCount / totalCells) * 100));
const guideText = t('win.usedGuide', { count: store.guideUsageCount, percent });
svgContent += `<text x="${padding}" y="${height - padding - footerHeight + 10}" font-family="Segoe UI, sans-serif" font-weight="600" font-size="14" fill="#ff4d4d">⚠️ ${guideText}</text>`;
}
// 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 += '</svg>';
return svgContent;
};
const canvasToBlob = (canvas) => new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), 'image/png')); const canvasToBlob = (canvas) => new Promise((resolve) => canvas.toBlob((blob) => resolve(blob), 'image/png'));
const createShareBlob = async () => { const createShareBlob = async () => {
@@ -193,6 +329,20 @@ const createShareBlob = async () => {
return canvasToBlob(canvas); return canvasToBlob(canvas);
}; };
const downloadShareSVG = () => {
const svgString = buildShareSVG();
if (!svgString) return;
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `nonogram-${store.size}x${store.size}.svg`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
};
const downloadShareImage = async () => { const downloadShareImage = async () => {
const blob = await createShareBlob(); const blob = await createShareBlob();
if (!blob) return; if (!blob) return;
@@ -341,6 +491,10 @@ onUnmounted(() => {
<button class="btn-neon secondary share-btn" :disabled="shareInProgress" :aria-label="t('win.shareDownload')" @click="downloadShareImage"> <button class="btn-neon secondary share-btn" :disabled="shareInProgress" :aria-label="t('win.shareDownload')" @click="downloadShareImage">
<Download :size="20" /> <Download :size="20" />
</button> </button>
<!-- Download SVG (Compact) -->
<button class="btn-neon secondary share-btn" :disabled="shareInProgress" aria-label="Download SVG" @click="downloadShareSVG">
<FileCode :size="20" />
</button>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff

View File

@@ -44,6 +44,7 @@ export function useNonogram() {
}; };
const stopDrag = () => { const stopDrag = () => {
store.endInteraction();
isDragging.value = false; isDragging.value = false;
dragMode.value = null; dragMode.value = null;
}; };

View File

@@ -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]; const speeds = [1000, 500, 250, 125, 62];
const speedLabels = ['x1', 'x2', 'x3', 'x4']; const speedLabels = ['x1', 'x2', 'x4', 'x8', 'x16'];
const statusText = ref(t('guide.waiting')); const statusText = ref(t('guide.waiting'));
let intervalId = null; let intervalId = null;

View File

@@ -75,6 +75,7 @@ export const usePuzzleStore = defineStore('puzzle', () => {
// History for undo // History for undo
const history = ref([]); const history = ref([]);
const currentTransaction = ref(null);
// Progress State // Progress State
const totalCellsToFill = computed(() => { const totalCellsToFill = computed(() => {
@@ -150,18 +151,39 @@ export const usePuzzleStore = defineStore('puzzle', () => {
playerGrid.value = Array(size.value).fill().map(() => Array(size.value).fill(0)); playerGrid.value = Array(size.value).fill().map(() => Array(size.value).fill(0));
moves.value = 0; moves.value = 0;
history.value = []; history.value = [];
currentTransaction.value = null;
} }
function pushHistory() { function startInteraction() {
const gridCopy = playerGrid.value.map(row => [...row]); currentTransaction.value = [];
history.value.push(gridCopy); }
if (history.value.length > 50) history.value.shift();
function endInteraction() {
if (currentTransaction.value && currentTransaction.value.length > 0) {
history.value.push(currentTransaction.value);
if (history.value.length > 50) history.value.shift();
saveState();
}
currentTransaction.value = null;
} }
function undo() { function undo() {
if (history.value.length === 0 || isGameWon.value) return; if (history.value.length === 0 || isGameWon.value) return;
const previousState = history.value.pop();
playerGrid.value = previousState; const transaction = history.value.pop();
// Handle legacy history (full grid snapshot)
if (!Array.isArray(transaction) || (transaction.length > 0 && Array.isArray(transaction[0]))) {
playerGrid.value = transaction;
} else {
// Handle new history (list of changes)
// Revert changes in reverse order
for (let i = transaction.length - 1; i >= 0; i--) {
const { r, c, oldVal } = transaction[i];
playerGrid.value[r][c] = oldVal;
}
}
moves.value++; moves.value++;
saveState(); saveState();
} }
@@ -169,8 +191,6 @@ export const usePuzzleStore = defineStore('puzzle', () => {
function toggleCell(r, c, isRightClick = false) { function toggleCell(r, c, isRightClick = false) {
if (isGameWon.value) return; if (isGameWon.value) return;
pushHistory();
const currentState = playerGrid.value[r][c]; const currentState = playerGrid.value[r][c];
let newState; let newState;
@@ -182,20 +202,48 @@ export const usePuzzleStore = defineStore('puzzle', () => {
newState = currentState === 1 ? 0 : 1; newState = currentState === 1 ? 0 : 1;
} }
playerGrid.value[r][c] = newState; // This triggers reactivity if (currentState === newState) return;
// Apply change
playerGrid.value[r][c] = newState;
// Record history
const change = { r, c, oldVal: currentState, newVal: newState };
if (currentTransaction.value) {
currentTransaction.value.push(change);
} else {
// Atomic change if no interaction started
history.value.push([change]);
if (history.value.length > 50) history.value.shift();
saveState();
}
moves.value++; moves.value++;
checkWin(); checkWin();
saveState(); // saveState(); // Moved to endInteraction or atomic block
} }
function setCell(r, c, state) { function setCell(r, c, state) {
if (isGameWon.value) return; if (isGameWon.value) return;
if (playerGrid.value[r][c] !== state) { const currentState = playerGrid.value[r][c];
pushHistory();
if (currentState !== state) {
// Apply change
playerGrid.value[r][c] = state; playerGrid.value[r][c] = state;
// Record history
const change = { r, c, oldVal: currentState, newVal: state };
if (currentTransaction.value) {
currentTransaction.value.push(change);
} else {
history.value.push([change]);
if (history.value.length > 50) history.value.shift();
saveState();
}
moves.value++; moves.value++;
checkWin(); checkWin();
saveState(); // saveState(); // Moved to endInteraction or atomic block
} }
} }
@@ -343,7 +391,9 @@ export const usePuzzleStore = defineStore('puzzle', () => {
hasUsedGuide, hasUsedGuide,
guideUsageCount, guideUsageCount,
currentDensity, currentDensity,
markGuideUsed markGuideUsed,
startInteraction,
endInteraction
}; };
}); });

View File

@@ -52,24 +52,79 @@ export function generateRandomGrid(size, density = 0.5) {
return grid; return grid;
} }
export function calculateDifficulty(density) { export function calculateDifficulty(density, size = 10) {
// Shannon Entropy: H(x) = -x*log2(x) - (1-x)*log2(1-x) // Data derived from Monte Carlo Simulation (Logical Solver)
// Normalized to 0-1 range (since max entropy at 0.5 is 1) // 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
// Avoid log(0) const SIM_DATA = {
if (density <= 0 || density >= 1) return 'easy'; 5: [89, 74, 74, 81, 97, 98, 99, 100, 100],
10: [57, 20, 16, 54, 92, 100, 100, 100, 100],
const entropy = -density * Math.log2(density) - (1 - density) * Math.log2(1 - density); 15: [37, 10, 2, 12, 68, 100, 100, 100, 100],
20: [23, 3, 1, 2, 37, 100, 100, 100, 100],
// Thresholds based on entropy 25: [16, 0, 0, 1, 19, 99, 100, 100, 100],
// 0.5 density -> entropy 1.0 (Extreme) 30: [8, 0, 0, 0, 5, 99, 100, 100, 100],
// 0.4/0.6 density -> entropy ~0.97 (Extreme) 35: [6, 0, 0, 0, 4, 91, 100, 100, 100],
// 0.3/0.7 density -> entropy ~0.88 (Hardest) 40: [3, 0, 0, 0, 2, 91, 100, 100, 100],
// 0.2/0.8 density -> entropy ~0.72 (Harder) 45: [2, 0, 0, 0, 1, 82, 100, 100, 100],
// <0.2/>0.8 density -> entropy <0.72 (Easy) 50: [2, 0, 0, 0, 1, 73, 100, 100, 100],
60: [0, 0, 0, 0, 0, 35, 100, 100, 100],
70: [0, 0, 0, 0, 0, 16, 100, 100, 100],
80: [0, 0, 0, 0, 0, 1, 100, 100, 100]
};
if (entropy >= 0.96) return 'extreme'; // approx 38% - 62% // Helper to get interpolated value from array
if (entropy >= 0.85) return 'hardest'; // approx 28% - 38% & 62% - 72% const getSimulatedSolvedPct = (s, d) => {
if (entropy >= 0.65) return 'harder'; // approx 17% - 28% & 72% - 83% // Find closest sizes
return 'easy'; const sizes = Object.keys(SIM_DATA).map(Number).sort((a, b) => a - b);
let sLower = sizes[0];
let sUpper = sizes[sizes.length - 1];
for (let i = 0; i < sizes.length - 1; i++) {
if (s >= sizes[i] && s <= sizes[i+1]) {
sLower = sizes[i];
sUpper = sizes[i+1];
break;
}
}
// Clamp density to 0.1 - 0.9
const dClamped = Math.max(0.1, Math.min(0.9, d));
// Index in array: 0.1 -> 0, 0.9 -> 8
const dIndex = (dClamped - 0.1) * 10;
const dLowerIdx = Math.floor(dIndex);
const dUpperIdx = Math.ceil(dIndex);
const dFraction = dIndex - dLowerIdx;
// Bilinear Interpolation
// 1. Interpolate Density for Lower Size
const rowLower = SIM_DATA[sLower];
const valLower = rowLower[dLowerIdx] * (1 - dFraction) + (rowLower[dUpperIdx] || rowLower[dLowerIdx]) * dFraction;
// 2. Interpolate Density for Upper Size
const rowUpper = SIM_DATA[sUpper];
const valUpper = rowUpper[dLowerIdx] * (1 - dFraction) + (rowUpper[dUpperIdx] || rowUpper[dLowerIdx]) * dFraction;
// 3. Interpolate Size
if (sLower === sUpper) return valLower;
const sFraction = (s - sLower) / (sUpper - sLower);
return valLower * (1 - sFraction) + valUpper * sFraction;
};
const solvedPct = getSimulatedSolvedPct(size, density);
// Difficulty Score: Inverse of Solved Percent
// 100% Solved -> 0 Difficulty
// 0% Solved -> 100 Difficulty
const value = Math.round(100 - solvedPct);
// Thresholds
let level = 'easy';
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 };
} }

278
src/utils/solver.js Normal file
View File

@@ -0,0 +1,278 @@
/**
* Represents the state of a cell in the solver.
* -1: Unknown
* 0: Empty
* 1: Filled
*/
/**
* Solves a single line (row or column) based on hints and current knowledge.
* Uses the "Left-Right Overlap" algorithm to find common filled cells.
* Also identifies definitely empty cells (reachable by no block).
*
* @param {number[]} currentLine - Array of -1, 0, 1
* @param {number[]} hints - Array of block lengths
* @returns {number[]} - Updated line (or null if contradiction/impossible - though shouldn't happen for valid puzzles)
*/
function solveLine(currentLine, hints) {
const length = currentLine.length;
// If no hints, all must be empty
if (hints.length === 0 || (hints.length === 1 && hints[0] === 0)) {
return Array(length).fill(0);
}
// Helper to check if a block can be placed at start index
const canPlace = (line, start, blockSize) => {
if (start + blockSize > line.length) return false;
// Check if any cell in block is 0 (Empty) -> Invalid
for (let i = start; i < start + blockSize; i++) {
if (line[i] === 0) return false;
}
// Check boundaries (must be separated by empty or edge)
if (start > 0 && line[start - 1] === 1) return false;
if (start + blockSize < line.length && line[start + blockSize] === 1) return false;
return true;
};
// 1. Calculate Left-Most Positions
const leftPositions = [];
let currentIdx = 0;
for (let hIndex = 0; hIndex < hints.length; hIndex++) {
const block = hints[hIndex];
// Find first valid position
while (currentIdx <= length - block) {
if (canPlace(currentLine, currentIdx, block)) {
// 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)) {
leftPositions.push(currentIdx);
currentIdx += block + 1; // Move past this block + 1 space
break;
}
}
currentIdx++;
}
if (leftPositions.length <= hIndex) return null; // Impossible
}
// 2. Calculate Right-Most Positions (by reversing line and hints)
// This is symmetrical to Left-Most.
// 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.
const reversedLine = [...currentLine].reverse();
const reversedHints = [...hints].reverse();
const rightPositionsReversed = [];
currentIdx = 0;
for (let hIndex = 0; hIndex < reversedHints.length; hIndex++) {
const block = reversedHints[hIndex];
while (currentIdx <= length - block) {
if (canPlace(reversedLine, currentIdx, block)) {
if (canFitRest(reversedLine, currentIdx + block + 1, reversedHints, hIndex + 1)) {
rightPositionsReversed.push(currentIdx);
currentIdx += block + 1;
break;
}
}
currentIdx++;
}
if (rightPositionsReversed.length <= hIndex) return null;
}
// 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 block = reversedHints[i];
return length - rStart - block;
}).reverse();
// 3. Intersect
const newLine = [...currentLine];
// Fill intersection
for (let i = 0; i < hints.length; i++) {
const l = leftPositions[i];
const r = rightPositions[i];
const block = hints[i];
// 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) {
for (let k = r; k < l + block; k++) {
newLine[k] = 1;
}
}
}
// 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
const possibleFilled = Array(length).fill(false);
for (let i = 0; i < hints.length; i++) {
for (let k = leftPositions[i]; k < rightPositions[i] + hints[i]; k++) {
possibleFilled[k] = true;
}
}
for (let k = 0; k < length; k++) {
if (!possibleFilled[k]) {
newLine[k] = 0;
}
}
return newLine;
}
// Memoized helper for checking if hints fit
const memo = new Map();
function canFitRest(line, startIndex, hints, hintIndex) {
// Optimization: If hints are empty, we just need to check if remaining line has no '1's
if (hintIndex >= hints.length) {
for (let i = startIndex; i < line.length; i++) {
if (line[i] === 1) return false;
}
return true;
}
// Key for memoization (primitive approach)
// In a full solver, we'd pass a cache. For single line, maybe overkill, but safe.
// let key = `${startIndex}-${hintIndex}`;
// Skipping memo for now as line lengths are small (<80) and recursion depth is low.
const remainingLen = line.length - startIndex;
// Min space needed: sum of hints + (hints.length - 1) spaces
// Calculate lazily or precalc?
let minSpace = 0;
for(let i=hintIndex; i<hints.length; i++) minSpace += hints[i] + (i < hints.length - 1 ? 1 : 0);
if (remainingLen < minSpace) return false;
const block = hints[hintIndex];
// 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++) { // Optimization on upper bound?
// Check placement
let valid = true;
// Block
for (let k = 0; k < block; k++) {
if (line[i+k] === 0) { valid = false; break; }
}
if (!valid) continue;
// Boundary before (checked by loop start usually, but strictly:
if (i > 0 && line[i-1] === 1) valid = false; // Should have been handled by caller or skip
// 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:
// 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 (valid) {
// Recurse
if (canFitRest(line, i + block + 1, hints, hintIndex + 1)) return true;
}
}
return false;
}
/**
* Solves the puzzle using logical iteration.
* @param {number[][]} rowHints
* @param {number[][]} colHints
* @returns {object} { solvedGrid: number[][], percentSolved: number }
*/
export function solvePuzzle(rowHints, colHints) {
const rows = rowHints.length;
const cols = colHints.length;
// Initialize grid with -1
let grid = Array(rows).fill(null).map(() => Array(cols).fill(-1));
let changed = true;
let iterations = 0;
const MAX_ITER = 100; // Safety break
while (changed && iterations < MAX_ITER) {
changed = false;
iterations++;
// 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;
}
}
}
}
// 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;
}
}
}
}
}
// Calculate solved %
let solvedCount = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] !== -1) solvedCount++;
}
}
return {
solvedGrid: grid,
percentSolved: (solvedCount / (rows * cols)) * 100
};
}

View File

@@ -91,15 +91,33 @@ const solveLineLogic = (lineState, hints) => {
return result; return result;
} }
const len = hints[hintIndex]; 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]; const maxStart = n - suffixMin[hintIndex];
for (let start = pos; start <= maxStart; start++) { for (let start = pos; start <= maxStart; start++) {
if (hasFilled(pos, start)) continue; if (hasFilled(pos, start)) continue; // Must be empty before this block
if (hasCross(start, start + len)) continue; if (hasCross(start, start + len)) continue; // Block space must be free of crosses
if (start + len < n && lineState[start + len] === 1) continue;
const nextPos = start + len < n ? start + len + 1 : start + len; // If not the last block, we need a gap after
if (canPlaceSuffix(nextPos, hintIndex + 1)) { if (hintIndex < m - 1) {
memoSuffix[pos][hintIndex] = true; if (start + len < n && lineState[start + len] === 1) continue; // Gap must not be filled
return true; // 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;
} }
} }
memoSuffix[pos][hintIndex] = false; memoSuffix[pos][hintIndex] = false;
@@ -115,16 +133,42 @@ const solveLineLogic = (lineState, hints) => {
return result; return result;
} }
const len = hints[hintCount - 1]; const len = hints[hintCount - 1];
const maxStart = pos - len;
// 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--) { for (let start = maxStart; start >= 0; start--) {
if (hasCross(start, start + len)) continue; if (hasCross(start, start + len)) continue;
if (start + len < pos && lineState[start + len] === 1) continue; if (hasFilled(start + len, pos)) continue; // Must be empty after this block up to pos
if (hasFilled(start + len, pos)) continue;
if (start > 0 && lineState[start - 1] === 1) continue; // Check gap before
const prevPos = start > 0 ? start - 1 : 0; if (hintCount > 1) {
if (canPlacePrefix(prevPos, hintCount - 1)) { if (start === 0) continue; // No space for previous blocks
memoPrefix[pos][hintCount] = true; if (lineState[start - 1] === 1) continue; // Gap must not be filled
return true; 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; memoPrefix[pos][hintCount] = false;
@@ -136,7 +180,14 @@ const solveLineLogic = (lineState, hints) => {
const len = hints[i]; const len = hints[i];
const starts = []; const starts = [];
for (let start = 0; start <= n - len; start++) { for (let start = 0; start <= n - len; start++) {
if (!canPlacePrefix(start, i)) continue; 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 (hasCross(start, start + len)) continue;
if (start + len < n && lineState[start + len] === 1) continue; if (start + len < n && lineState[start + len] === 1) continue;
const nextPos = start + len < n ? start + len + 1 : start + len; const nextPos = start + len < n ? start + len + 1 : start + len;