Compare commits

..

140 Commits

Author SHA1 Message Date
mike
20994d46d8 redo 2026-01-24 04:27:42 +01:00
mike
26cd6fb284 redo 2026-01-24 04:03:52 +01:00
mike
f61d04bb61 redo 2026-01-24 01:43:41 +01:00
mike
b8fc6581e1 redo 2026-01-24 01:24:49 +01:00
mike
3cc6570cdc redo 2026-01-24 00:46:10 +01:00
mike
2a5b70896e redo 2026-01-23 23:13:12 +01:00
mike
282ec56f19 redo 2026-01-23 22:17:13 +01:00
mike
d9b565301f redo 2026-01-23 22:04:13 +01:00
mike
061065c9a7 redo 2026-01-23 21:44:12 +01:00
mike
e9d787bbb9 redo 2026-01-23 21:20:08 +01:00
mike
7e34276726 redo 2026-01-23 20:36:57 +01:00
mike
73192f5905 redo 2026-01-23 20:33:36 +01:00
mike
fa3e8ef1ed redo 2026-01-23 20:04:02 +01:00
mike
c21ad114dc redo 2026-01-23 13:16:48 +01:00
mike
d0b64fac5f redo 2026-01-23 13:12:30 +01:00
mike
e5db5b8d96 redo 2026-01-23 12:51:13 +01:00
mike
7db439a9dc redo 2026-01-23 11:58:57 +01:00
mike
2aa08fedb0 redo 2026-01-23 06:31:56 +01:00
mike
2c39e82b00 redo 2026-01-23 05:23:11 +01:00
mike
4109c51cbe redo 2026-01-23 04:02:51 +01:00
mike
ed7cade1c7 redo 2026-01-23 03:37:54 +01:00
mike
4b61205bbb redo 2026-01-23 02:56:14 +01:00
mike
dc45ad45c9 redo 2026-01-23 01:55:12 +01:00
mike
2295a7d97c introduce bitloops 2026-01-22 18:47:04 +01:00
mike
a659bd5162 introduce bitloops 2026-01-21 07:03:41 +01:00
mike
1e13d39153 introduce bitloops 2026-01-21 06:38:25 +01:00
mike
dd53009e69 introduce bitloops 2026-01-21 06:19:49 +01:00
mike
ebcbc9b33c introduce bitloops 2026-01-21 06:12:05 +01:00
mike
386777e576 introduce bitloops 2026-01-21 05:40:14 +01:00
mike
f203f2106e introduce bitloops 2026-01-21 05:13:39 +01:00
mike
92a736aa0a introduce bitloops 2026-01-21 00:56:18 +01:00
mike
78f72a024e introduce bitloops 2026-01-21 00:09:37 +01:00
mike
46b2bb04dc introduce bitloops 2026-01-20 23:24:24 +01:00
mike
7f15ab8ff1 introduce bitloops 2026-01-20 23:18:29 +01:00
mike
85fa317eec introduce bitloops 2026-01-20 22:48:38 +01:00
mike
b66437bb70 introduce bitloops 2026-01-20 21:19:39 +01:00
mike
ddce9addb5 introduce bitloops 2026-01-20 19:06:59 +01:00
mike
dadde53f76 introduce bitloops 2026-01-20 19:00:27 +01:00
mike
58b8b57688 introduce bitloops 2026-01-20 12:08:48 +01:00
mike
8780a26451 introduce bitloops 2026-01-20 10:42:00 +01:00
mike
5d0da1cf6b introduce bitloops 2026-01-20 10:35:10 +01:00
mike
3cd17d170a introduce bitloops 2026-01-20 10:21:38 +01:00
mike
e8e0344212 introduce bitloops 2026-01-20 10:17:47 +01:00
mike
fde6f15b24 introduce bitloops 2026-01-20 10:00:30 +01:00
mike
30ca46ac03 introduce bitloops 2026-01-20 09:48:51 +01:00
mike
8b7827cfc2 introduce bitloops 2026-01-20 09:47:55 +01:00
mike
a764f45041 introduce bitloops 2026-01-20 07:08:31 +01:00
mike
47ead135d3 introduce bitloops 2026-01-20 05:32:58 +01:00
mike
a1061f5eb9 introduce bitloops 2026-01-20 05:06:34 +01:00
mike
ba90818b66 introduce bitloops 2026-01-20 04:51:32 +01:00
mike
0dcfbebcb0 introduce bitloops 2026-01-20 03:43:02 +01:00
mike
28f448d178 introduce bitloops 2026-01-20 02:45:16 +01:00
mike
d1c448e1cb introduce bitloops 2026-01-20 01:57:21 +01:00
mike
7e5e363a3e introduce bitloops 2026-01-20 01:35:34 +01:00
mike
5678af332e introduce bitloops 2026-01-19 20:45:28 +01:00
mike
5d186ae0ba introduce bitloops 2026-01-19 19:11:31 +01:00
mike
1fa112ab65 introduce bitloops 2026-01-19 16:31:33 +01:00
mike
37581d15b4 introduce bitloops 2026-01-18 04:25:41 +01:00
mike
948730d7be introduce bitloops 2026-01-18 04:11:43 +01:00
mike
b026ebfbd2 introduce bitloops 2026-01-18 03:32:34 +01:00
mike
6daab5ef4e introduce bitloops 2026-01-18 02:58:37 +01:00
mike
112c16c525 introduce bitloops 2026-01-18 01:29:46 +01:00
mike
20617cda57 introduce bitloops 2026-01-17 21:43:20 +01:00
mike
19812d81e5 introduce bitloops 2026-01-17 21:43:17 +01:00
mike
938d2ac66b introduce bitloops 2026-01-17 20:24:45 +01:00
mike
8ff9d661e3 introduce bitloops 2026-01-17 18:39:19 +01:00
mike
9367833407 introduce bitloops 2026-01-17 16:37:48 +01:00
mike
c76d463c8c introduce bitloops 2026-01-17 16:17:18 +01:00
mike
bfa19ec585 introduce bitloops 2026-01-17 16:03:16 +01:00
mike
9bd85c81a3 introduce bitloops 2026-01-17 14:21:53 +01:00
mike
bd25f65194 introduce bitloops 2026-01-17 14:21:50 +01:00
mike
9102dcb922 introduce bitloops 2026-01-17 13:22:04 +01:00
mike
0c56fafeaa introduce bitloops 2026-01-17 04:57:42 +01:00
mike
3bd7a0f958 introduce bitloops 2026-01-17 04:35:53 +01:00
mike
47b33af09d introduce bitloops 2026-01-17 04:18:35 +01:00
mike
81ea708345 introduce bitloops 2026-01-17 03:42:33 +01:00
mike
d3c7128cf9 introduce bitloops 2026-01-17 03:37:38 +01:00
mike
44f53801a3 introduce bitloops 2026-01-17 03:14:13 +01:00
mike
57be64c37e introduce bitloops 2026-01-17 02:43:32 +01:00
mike
a2134f0dce introduce bitloops 2026-01-17 01:30:50 +01:00
mike
4585c1f2eb introduce bitloops 2026-01-17 01:15:03 +01:00
mike
60b7509bf6 introduce bitloops 2026-01-16 23:48:36 +01:00
mike
aceaa0fc18 introduce bitloops 2026-01-16 23:42:50 +01:00
mike
5c7d1120db introduce bitloops 2026-01-16 22:52:48 +01:00
mike
91722ecc60 introduce bitloops 2026-01-16 22:46:04 +01:00
mike
ecbc408cce introduce bitloops 2026-01-15 01:18:44 +01:00
mike
e8711b30a1 introduce bitloops 2026-01-14 23:26:54 +01:00
mike
04e3844732 introduce bitloops 2026-01-14 23:15:52 +01:00
mike
5849f543c5 introduce bitloops 2026-01-14 20:04:14 +01:00
mike
70b2009723 introduce bitloops 2026-01-14 18:49:52 +01:00
mike
75f599318a introduce bitloops 2026-01-14 18:42:13 +01:00
mike
29aceb2180 introduce bitloops 2026-01-14 14:17:01 +01:00
mike
b29831859b introduce bitloops 2026-01-14 13:43:02 +01:00
mike
b1f54c2cae introduce bitloops 2026-01-14 13:12:21 +01:00
mike
0b7a59b769 introduce bitloops 2026-01-14 12:53:43 +01:00
mike
ecf4ae913e introduce bitloops 2026-01-14 12:45:08 +01:00
mike
dfb4679da8 introduce bitloops 2026-01-14 12:28:07 +01:00
mike
6afe675a9d introduce bitloops 2026-01-14 12:01:13 +01:00
mike
8e049b3fa5 introduce bitloops 2026-01-14 11:37:40 +01:00
mike
c1706e1bf7 introduce bitloops 2026-01-14 11:03:28 +01:00
mike
66bd8193ef introduce bitloops 2026-01-14 10:39:06 +01:00
mike
cb2935e0f5 introduce bitloops 2026-01-14 10:24:55 +01:00
mike
69af69a8b8 introduce bitloops 2026-01-14 09:59:24 +01:00
mike
1a56297986 introduce bitloops 2026-01-14 09:28:51 +01:00
mike
afd4a7f53c introduce bitloops 2026-01-14 09:14:03 +01:00
mike
bd52bd0ef0 introduce bitloops 2026-01-14 08:36:31 +01:00
mike
6e2ecae082 introduce bitloops 2026-01-14 08:24:58 +01:00
mike
aafa5d4d43 introduce bitloops 2026-01-14 08:24:53 +01:00
mike
352e8c79ca introduce bitloops 2026-01-14 08:04:17 +01:00
mike
bcad930bfc introduce bitloops 2026-01-14 07:50:35 +01:00
mike
c529ce90c7 introduce bitloops 2026-01-14 07:25:46 +01:00
mike
5d34893ef1 introduce bitloops 2026-01-14 05:06:23 +01:00
mike
0405d11753 introduce bitloops 2026-01-14 04:22:46 +01:00
mike
eeae90a886 introduce bitloops 2026-01-14 03:46:36 +01:00
mike
19f235ae59 introduce bitloops 2026-01-14 03:30:19 +01:00
mike
b0b10d356a introduce bitloops 2026-01-14 03:03:34 +01:00
mike
107dfab0c7 introduce bitloops 2026-01-14 02:18:40 +01:00
mike
1d731334d9 introduce bitloops 2026-01-14 00:57:41 +01:00
mike
2430dbdfb9 introduce bitloops 2026-01-13 23:29:32 +01:00
mike
6ab6791d9c all 2026-01-13 20:03:19 +01:00
mike
81ae0aa84c introduce bitloops 2026-01-13 01:17:08 +01:00
mike
6119722867 introduce bitloops 2026-01-13 00:03:39 +01:00
mike
61d246e551 introduce bitloops 2026-01-12 23:25:59 +01:00
mike
a9b4dfb422 introduce bitloops 2026-01-12 22:22:19 +01:00
mike
88a61e6f4d introduce bitloops 2026-01-12 21:47:27 +01:00
mike
8e7b29a2d3 introduce bitloops 2026-01-12 21:29:06 +01:00
mike
4784fa7180 introduce bitloops 2026-01-12 21:12:38 +01:00
mike
b3b1921414 introduce bitloops 2026-01-12 20:03:31 +01:00
mike
368473c80f introduce bitloops 2026-01-12 19:32:05 +01:00
mike
c7d67bf778 introduce bitloops 2026-01-12 18:56:26 +01:00
mike
c2e94fb02e introduce bitloops 2026-01-12 18:52:34 +01:00
mike
16676d633a introduce bitloops 2026-01-12 18:28:52 +01:00
mike
a0862fcc43 introduce bitloops 2026-01-12 12:02:16 +01:00
mike
b6351a6fb2 introduce bitloops 2026-01-12 11:46:38 +01:00
mike
6ef007522c introduce bitloops 2026-01-12 10:38:24 +01:00
mike
67eedb773b introduce bitloops 2026-01-12 09:42:13 +01:00
mike
84e832df40 introduce bitloops 2026-01-12 09:23:38 +01:00
mike
fdd1c76bae introduce bitloops 2026-01-12 08:43:13 +01:00
mike
9c1be77d76 introduce bitloops 2026-01-12 08:28:48 +01:00
mike
f8f1a67a61 introduce bitloops 2026-01-12 07:07:49 +01:00
35 changed files with 100354 additions and 3325 deletions

View File

@@ -8,7 +8,7 @@ OPENAI_API_KEY="local"
# Constants for testing
CLUE_SIZE=4
MIN_LEN=2
MAX_TRIES_PER_SLOT=2000
MAX_TRIES_PER_SLOT=1000
MAX_LEN=8
PUZZLE_ROWS=3
PUZZLE_COLS=3

View File

@@ -4,7 +4,7 @@
<module name="tools" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="puzzle.*" />
<option name="PATTERN" value="precomp.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>

Binary file not shown.

97245
nl_score_hints_v4.csv Normal file

File diff suppressed because it is too large Load Diff

428
package-lock.json generated
View File

@@ -1,428 +0,0 @@
{
"name": "puzzle-generator",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "puzzle-generator",
"version": "1.0.0",
"dependencies": {
"better-sqlite3": "^12.5.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/better-sqlite3": {
"version": "12.5.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.5.0.tgz",
"integrity": "sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==",
"hasInstallScript": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"engines": {
"node": ">=6"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="
},
"node_modules/node-abi": {
"version": "3.85.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pump": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
"integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}

View File

@@ -1,13 +0,0 @@
{
"name": "puzzle-generator",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"private": true,
"dependencies": {
"better-sqlite3": "^12.5.0"
}
}

18
pom.xml
View File

@@ -29,11 +29,13 @@
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
@@ -63,10 +65,16 @@
</dependency>
<dependency>
<groupId>mike.processor</groupId>
<artifactId>puzzle-processor</artifactId>
<artifactId>puzzle-processor</artifactId>
<version>1.7-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- <dependency>
<groupId>mike.plugin</groupId>
<artifactId>plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>-->
</dependencies>
<build>
@@ -92,6 +100,13 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<!-- <compilerArgs>
<arg>&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-Xplugin:autogetter</arg>
</compilerArgs>-->
<annotationProcessorPaths>
<!-- Lombok processor -->
<path>
@@ -109,6 +124,7 @@
</annotationProcessorPaths>
<source>25</source>
<target>25</target>
<release>25</release>
</configuration>
</plugin>
<plugin>

View File

@@ -1,13 +0,0 @@
package puzzle;
/**
* Generated constants from pom.xml during build via templating-maven-plugin.
*/
public final class Config {
public static final int CLUE_SIZE = ${CLUE_SIZE};
public static final int MIN_LEN = ${MIN_LEN};
public static final int MAX_TRIES_PER_SLOT = ${MAX_TRIES_PER_SLOT};
public static final int MAX_LEN = ${MAX_LEN};
public static final int PUZZLE_ROWS = ${PUZZLE_ROWS};
public static final int PUZZLE_COLS = ${PUZZLE_COLS};
}

View File

@@ -0,0 +1,28 @@
package precomp;
import puzzle.Masker.Slot;
public sealed interface Mask
permits Const9x8.Cell, Const3x4.Cell, Mask.Masker {
record Masker(long lo, long hi, int index, int r, int c, int place, byte d)
implements precomp.Mask { }
default Mask or(Mask o) { return new Masker(o.lo() | lo(), o.hi() | hi(), 0, 0, 0, 0, (byte) 0); }
default Mask and(Mask o) { return new Masker(o.lo() & lo(), o.hi() & hi(), 0, 0, 0, 0, (byte) 0); }
long hi();
long lo();
int index();
int r();
int c();
int place();
byte d();
default byte letter() { return (byte) (d() | 64); }
default void letter(byte[] template, int minR, int minC, int height, int width) {
int rr = r() - minR;
int cc = c() - minC;
if (rr >= 0 && rr < height && cc >= 0 && cc < width) {
template[rr * (width + 1) + cc] = letter();
}
}
default byte clueChar() { return (byte) (d() | 48); }
}

View File

@@ -0,0 +1,85 @@
package puzzle;
import anno.ConstGen;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.Accessors;
import precomp.Mask;
import static java.lang.Long.bitCount;
import static puzzle.SwedishGenerator.X;
//@formatter:on
@AllArgsConstructor
@Accessors(fluent = true)
@ConstGen(C = 3, R = 4, packageName = "precomp", className = "Const3x4")
public final class Clues {
@Getter long lo, hi, vlo, vhi, rlo, rhi, xlo, xhi;
public static Clues createEmpty() { return new Clues(0, 0, 0, 0, 0, 0, 0, 0); }
public Clues setClue(Mask cell) {
if ((cell.index() & 64) == 0) setClueLo(cell.lo(), cell.d());
else setClueHi(cell.hi(), cell.d());
return this;
}
public void setClueLo(long mask, byte idx) {
lo |= mask;
if ((idx & 1) != 0) vlo |= mask;
else vlo &= ~mask;
if ((idx & 2) != 0) rlo |= mask;
else rlo &= ~mask;
if ((idx & 4) != 0) xlo |= mask;
else xlo &= ~mask;
}
public void setClueHi(long mask, byte idx) {
hi |= mask;
if ((idx & 1) != 0) vhi |= mask;
else vhi &= ~mask;
if ((idx & 2) != 0) rhi |= mask;
else rhi &= ~mask;
if ((idx & 4) != 0) xhi |= mask;
else xhi &= ~mask;
}
public void clearClueLo(long mask) {
lo &= mask;
vlo &= mask;
rlo &= mask;
xlo &= mask;
}
public void clearClueHi(long mask) {
hi &= mask;
vhi &= mask;
rhi &= mask;
xhi &= mask;
}
public boolean isClueLo(int index) { return ((lo >>> index) & 1L) != X; }
public boolean isClueHi(int index) { return ((hi >>> (index & 63)) & 1L) != X; }
public boolean notClue(int index) { return ((index & 64) == 0) ? ((lo >>> index) & 1L) == X : ((hi >>> (index & 63)) & 1L) == X; }
public int clueCount() { return bitCount(lo) + bitCount(hi); }
public Clues from(Clues best) {
lo = best.lo;
hi = best.hi;
vlo = best.vlo;
vhi = best.vhi;
rlo = best.rlo;
rhi = best.rhi;
xlo = best.xlo;
xhi = best.xhi;
return this;
}
public byte getDir(int index) {
if ((index & 64) == 0) {
var v = (vlo & (1L << index)) != 0 ? 1 : 0;
var r = (rlo & (1L << index)) != 0 ? 1 : 0;
var x = (xlo & (1L << index)) != 0 ? 1 : 0;
return (byte) ((x << 2) | (r << 1) | v);
} else {
var v = (vhi & (1L << (index & 63))) != 0 ? 1 : 0;
var r = (rhi & (1L << (index & 63))) != 0 ? 1 : 0;
var x = (xhi & (1L << (index & 63))) != 0 ? 1 : 0;
return (byte) ((x << 2) | (r << 1) | v);
}
}
}

View File

@@ -1,253 +0,0 @@
package puzzle;
import com.google.gson.Gson;
import puzzle.SwedishGenerator.Lemma;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.Arrays;
import java.util.function.Consumer;
public final class CsvIndexService
implements Closeable {
static final ScopedValue<CsvIndexService> SC = ScopedValue.newInstance();
static final Gson GSON = new Gson();
private static final int MAGIC = 0x4C494458; // "LIDX"
private static final int VERSION = 1;
static int SIMPEL_IDX = 3;
private final Path csvPath;
private final Path idxPath;
private volatile long[] offsets; // lazy
private volatile FileChannel csvChannel; // open once
private final Object lock = new Object();
public CsvIndexService(Path csvPath, Path idxPath) {
this.csvPath = csvPath;
this.idxPath = idxPath;
}
public static int lineToSimpel(String line) {
var parts = line.split(",", 5);
return Integer.parseInt(parts[SIMPEL_IDX].trim());
}
public static String[] lineToClue(String line) {
if (line.isBlank()) throw new RuntimeException("Empty line");
var parts = line.split(",", 5);
var rawClue = parts[4].trim();
if (rawClue.startsWith("\"") && rawClue.endsWith("\"")) {
rawClue = rawClue.substring(1, rawClue.length() - 1).replace("\"\"", "\"");
}
return GSON.fromJson(rawClue, String[].class);
}
public static void lineToLemma(String line, Consumer<Lemma> ok) {
if (line.isBlank()) {
throw new RuntimeException("Empty line");
}
var parts = line.split(",", 5);
var id = Integer.parseInt(parts[0].trim());
var word = parts[1].trim();
if (!word.matches("^[A-Z]{2,8}$")) {
throw new RuntimeException("Invalid word:" + line);
}
int score = Integer.parseInt(parts[2].trim());
if (score < 1) {
if (Main.VERBOSE) System.err.println("Word too complex: " + line);
return;
}
ok.accept(new Lemma(id, word));
}
public static int simpel(int index) {
try {
if (SC.isBound())
return lineToSimpel(SC.get().getLine(index));
return -1;
} catch (Exception e) {
throw new RuntimeException("Failed to get clues for index " + index, e);
}
}
public static String[] clues(int index) {
try {
if (SC.isBound())
return lineToClue(SC.get().getLine(index));
return new String[0];
} catch (Exception e) {
throw new RuntimeException("Failed to get clues for index " + index, e);
}
}
/** Haal één regel op (0-based line index), met self-healing index (1x rebuild). */
public String getLine(int lineIndex) throws IOException {
ensureLoaded();
var line = readLineAt(lineIndex);
if (startsWithIndex(line, lineIndex)) return line;
// mismatch => rebuild index en nog 1x proberen
synchronized (lock) {
rebuildIndexLocked();
line = readLineAt(lineIndex);
if (startsWithIndex(line, lineIndex)) return line;
}
throw new RuntimeException("Index mismatch after rebuild. Requested=" + lineIndex + ", got line=" + preview(line));
}
private void ensureLoaded() throws IOException {
if (offsets != null && csvChannel != null && csvChannel.isOpen()) return;
synchronized (lock) {
if (offsets != null && csvChannel != null && csvChannel.isOpen()) return;
csvChannel = FileChannel.open(csvPath, StandardOpenOption.READ);
if (Files.exists(idxPath)) {
try {
offsets = readIndex(idxPath);
return;
} catch (IOException badIndex) {
// fall-through -> rebuild
}
}
rebuildIndexLocked();
}
}
private void rebuildIndexLocked() throws IOException {
var built = buildOffsets(csvPath);
writeIndex(idxPath, built);
offsets = built;
}
private String readLineAt(int lineIndex) throws IOException {
var local = offsets;
if (lineIndex < 0 || lineIndex >= local.length) {
throw new IndexOutOfBoundsException("lineIndex=" + lineIndex + ", max=" + (local.length - 1));
}
var start = local[lineIndex];
csvChannel.position(start);
// lees in blokjes (sneller dan 1 byte) tot newline
var buf = new byte[8192];
var total = 0;
var out = new byte[256];
while (true) {
var bb = ByteBuffer.wrap(buf);
var n = csvChannel.read(bb);
if (n < 0) break; // EOF
var end = n;
for (var i = 0; i < end; i++) {
var b = buf[i];
if (b == (byte) '\n') {
// reposition kanaal op byte na newline
long back = (end - i - 1);
csvChannel.position(csvChannel.position() - back);
return new String(out, 0, total, StandardCharsets.UTF_8);
}
if (b == (byte) '\r') continue;
if (total == out.length) out = Arrays.copyOf(out, out.length * 2);
out[total++] = b;
}
}
return new String(out, 0, total, StandardCharsets.UTF_8);
}
/** Check: begint de regel met "<lineIndex>," */
private static boolean startsWithIndex(String line, int lineIndex) {
if (line == null || line.isEmpty()) return false;
var comma = line.indexOf(',');
if (comma <= 0) return false;
// snelle parse zonder split
long v = 0;
for (var i = 0; i < comma; i++) {
var c = line.charAt(i);
if (c < '0' || c > '9') return false;
v = (v * 10) + (c - '0');
if (v > Integer.MAX_VALUE) return false;
}
return v == lineIndex;
}
private static String preview(String s) {
if (s == null) return "null";
return s.length() <= 120 ? s : s.substring(0, 120) + "...";
}
/** Bouw offsets door newlines te scannen. Resultaat is exact getrimd. */
public static long[] buildOffsets(Path path) throws IOException {
try (var ch = FileChannel.open(path, StandardOpenOption.READ)) {
var offs = new long[131072]; // start-capacity, groeit indien nodig
var c = 0;
offs[c++] = 0L;
var buf = ByteBuffer.allocateDirect(1 << 20);
long pos = 0;
while (true) {
buf.clear();
var n = ch.read(buf);
if (n < 0) break;
buf.flip();
for (var i = 0; i < n; i++) {
if (buf.get(i) == (byte) '\n') {
if (c == offs.length) offs = Arrays.copyOf(offs, offs.length * 2);
offs[c++] = pos + i + 1;
}
}
pos += n;
}
return Arrays.copyOf(offs, c);
}
}
public static void writeIndex(Path out, long[] offsets) throws IOException {
try (var dos = new DataOutputStream(new BufferedOutputStream(Files.newOutputStream(
out, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)))) {
dos.writeInt(MAGIC);
dos.writeInt(VERSION);
dos.writeInt(offsets.length);
for (var v : offsets) dos.writeLong(v);
}
}
public static long[] readIndex(Path in) throws IOException {
try (var dis = new DataInputStream(new BufferedInputStream(Files.newInputStream(in)))) {
var magic = dis.readInt();
if (magic != MAGIC) throw new IOException("Not a LIDX file");
var version = dis.readInt();
if (version != VERSION) throw new IOException("Unsupported version: " + version);
var n = dis.readInt();
if (n < 0) throw new IOException("Corrupt length: " + n);
var offsets = new long[n];
for (var i = 0; i < n; i++) offsets[i] = dis.readLong();
return offsets;
}
}
@Override
public void close() throws IOException {
synchronized (lock) {
if (csvChannel != null) csvChannel.close();
csvChannel = null;
offsets = null;
}
}
}

View File

@@ -1,238 +1,142 @@
package puzzle;
import module java.base;
import anno.GenerateShapedCopies;
import anno.Shaped;
import lombok.experimental.Delegate;
import puzzle.SwedishGenerator.Dict;
import lombok.val;
import precomp.Const9x8;
import precomp.Mask;
import puzzle.Riddle.ClueSign;
import puzzle.Riddle.ExportedPuzzle;
import puzzle.Riddle.Placed;
import puzzle.Riddle.Rewards;
import puzzle.Riddle.Signa;
import puzzle.Riddle.WordOut;
import puzzle.SwedishGenerator.FillResult;
import puzzle.SwedishGenerator.Grid;
import java.util.ArrayList;
import puzzle.SwedishGenerator.Slotinfo;
import static puzzle.Masker.Slot;
import static puzzle.SwedishGenerator.X;
import java.util.stream.Stream;
import java.util.Arrays;
import java.util.HashMap;
import static puzzle.SwedishGenerator.R;
import static puzzle.SwedishGenerator.Lemma;
import static puzzle.SwedishGenerator.Slot;
import static puzzle.SwedishGenerator.C;
/**
* ExportFormat.java
*
* Direct port of export_format.js:
* - scans filled grid for clue digits '1'..'4'
* - extracts placed words in canonical direction (horizontal=right, vertical=down)
* - crops to bounding box (words + arrow cells) with 1-cell margin
* - outputs gridv2 + words[] (+ difficulty, rewards)
*/
@GenerateShapedCopies(
packageName = "puzzle",
className = "ExportX",
shapes = { "precomp.Const9x8", "precomp.Const3x4" }
)
public record Export() {
record Strings() {
public record ExportTemplates(byte[] table, byte[] dashTable, byte[] wordBytes) { }
@Shaped static final byte SPACE = Const9x8.SPACE;
@Shaped static final byte LINE_BREAK = Const9x8.LINE_BREAK;
@Shaped static final byte DASH = Const9x8.DASH;
@Shaped static final int SIZE = Const9x8.SIZE;
@Shaped static final byte[] INIT_GRID_OUTPUT_ARR = Const9x8.INIT_GRID_OUTPUT_ARR;
@Shaped static final byte[] INIT_GRID_OUTPUT_DASH_ARR = Const9x8.INIT_GRID_OUTPUT_DASH_ARR;
@Shaped static final long MASK_HI = Const9x8.MASK_HI;
@Shaped static final long MASK_LO = Const9x8.MASK_LO;
@Shaped static final Mask[] CELLS = Const9x8.CELLS;
public static final ThreadLocal<ExportTemplates> BYTES = ThreadLocal.withInitial(
() -> new ExportTemplates(INIT_GRID_OUTPUT_ARR, INIT_GRID_OUTPUT_DASH_ARR, new byte[8]));
static int HI(int in) { return in | 64; }
public static String gridToString(Clues clues) {
val chars = BYTES.get().table();
var signa = new Signa(clues).map(v -> CELLS[v.cellIndex()]).toArray(Mask[]::new);
Arrays.stream(signa).forEach(v -> chars[v.place()] = v.clueChar());
val result = new String(chars);
Arrays.stream(signa).forEach(v -> chars[v.place()] = SPACE);
return result;
}
record Puzzle(@Delegate Grid grid, Mask[] cells, Clues cl)
implements Stream<Mask> {
static String padRight(String s, int n) {
if (s.length() >= n) return s;
return s + " ".repeat(n - s.length());
public Puzzle {
for (var l = grid.lo & MASK_LO & ~cl.lo; l != X; l &= l - 1) set(Long.numberOfTrailingZeros(l), cells, grid.g);
for (var h = grid.hi & MASK_HI & ~cl.hi; h != X; h &= h - 1) set(HI(Long.numberOfTrailingZeros(h)), cells, grid.g);
new Signa(cl).forEach(v -> cells[v.index()] = CELLS[v.cellIndex()]);
}
static void set(int idx, Mask[] cells, byte[] read) { cells[idx] = CELLS[idx * 33 + read[idx]]; }
public Puzzle(Grid grid, Clues cl) { this(grid, new Mask[grid.g.length], cl); }
public Puzzle(Clues clues) { this(new Grid(new byte[SIZE], clues.lo, clues.hi), new Mask[SIZE], clues); }
public Puzzle(Signa clues) { this(clues.c()); }
public @Delegate Stream<Mask> stream() {
val stream = Stream.<Mask>builder();
for (var l = grid.lo & MASK_LO & ~cl.lo; l != X; l &= l - 1) stream.accept(cells[Long.numberOfTrailingZeros(l)]);
for (var h = grid.hi & MASK_HI & ~cl.hi; h != X; h &= h - 1) stream.accept(cells[HI(Long.numberOfTrailingZeros(h))]);
return stream.build();
}
public Puzzle sync() {
for (var l = grid.lo & MASK_LO & ~cl.lo; l != X; l &= l - 1) set(Long.numberOfTrailingZeros(l), cells, grid.g);
for (var h = grid.hi & MASK_HI & ~cl.hi; h != X; h &= h - 1) set(HI(Long.numberOfTrailingZeros(h)), cells, grid.g);
new Signa(cl).forEach(v -> cells[v.index()] = CELLS[v.cellIndex()]);
return this;
}
}
record Gridded(@Delegate Grid grid) {
public record PuzzleResult(Signa clues, Puzzle puzzle, Slotinfo[] slots, FillResult filled) {
String gridToString() {
var sb = new StringBuilder();
for (var r = 0; r < R; r++) {
if (r > 0) sb.append('\n');
for (var c = 0; c < C; c++) sb.append((char) grid.byteAt(Grid.offset(r, c)));
}
return sb.toString();
public String exportGrid(ClueSign clueChar, byte[] sb) {
Arrays.stream(slots).map(s -> puzzle.cells[Slot.clueIndex(s.key())]).forEach(c -> sb[c.place()] = clueChar.replace(c.clueChar()));
puzzle.forEach((l) -> sb[l.place()] = l.letter());
return new String(sb);
}
public String renderHuman() {
var sb = new StringBuilder();
for (var r = 0; r < R; r++) {
if (r > 0) sb.append('\n');
for (var c = 0; c < C; c++) {
sb.append(grid.isDigitAt(Grid.offset(r, c)) ? ' ' : (char) grid.byteAt(Grid.offset(r, c)));
}
}
return sb.toString();
}
}
static class Bit {
static long pack(int r, int c) { return (((long) r) << 32) ^ (c & 0xFFFFFFFFL); }
long l1, l2;
public boolean get(int bitIndex) {
if ((bitIndex & 64) == 0) return (l1 & (1L << bitIndex)) != 0L;
return (l2 & (1L << (bitIndex & 63))) != 0L;
}
public void set(int bitIndex) {
if ((bitIndex & 64) == 0) this.l1 |= 1L << bitIndex;
else this.l2 |= 1L << (bitIndex & 63);
}
public void clear() {
l1 = 0L;
l2 = 0L;
}
}
record Bit1029(long[] bits) {
public Bit1029() { this(new long[2048]); }
static int wordIndex(int bitIndex) { return bitIndex >> 6; }
public boolean get(int bitIndex) { return (this.bits[wordIndex(bitIndex)] & 1L << bitIndex) != 0L; }
public void set(int bitIndex) { bits[wordIndex(bitIndex)] |= 1L << bitIndex; }
public void clear(int bitIndex) { this.bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); }
public void clear() { Arrays.fill(bits, 0L); }
}
record Placed(long lemma, int startRow, int startCol, char direction, int arrowRow, int arrowCol, int[] cells, boolean isReversed) {
public static final char HORIZONTAL = 'h';
static final char VERTICAL = 'v';
}
public record Rewards(int coins, int stars, int hints) { }
public record WordOut(String word, int[] cell, int startRow, int startCol, char direction, int arrowRow, int arrowCol, boolean isReversed, int complex, String[] clue) {
public WordOut(long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed) {
this(Lemma.asWord(l), new int[]{ arrowRow, arrowCol, startRow, startCol }, startRow, startCol, d, arrowRow, arrowCol, isReversed, Lemma.simpel(l), Lemma.clue(l));
}
}
public record ExportedPuzzle(String[] gridv2, WordOut[] words, int difficulty, Rewards rewards) { }
public record PuzzleResult(SwedishGenerator swe, Dict dict, Gridded mask, FillResult filled) {
boolean inBounds(int r, int c) { return r >= 0 && r < SwedishGenerator.R && c >= 0 && c < SwedishGenerator.C; }
Placed extractPlacedFromSlot(Slot s, long lemma) {
var d = s.dir();
var cells = new int[s.len()];
for (int i = 0, len = s.len(); i < len; i++) cells[i] = s.pos(i);
char direction;
var isReversed = false;
var startRow = Grid.r(cells[0]);
var startCol = Grid.c(cells[0]);
if (d == 2) { // right -> horizontal
direction = Placed.HORIZONTAL;
} else if (d == 3 || d == 5) { // down or down-bent -> vertical
direction = Placed.VERTICAL;
} else if (d == 4) { // left -> horizontal (REVERSED)
direction = Placed.HORIZONTAL;
isReversed = true;
} else if (d == 1) { // up -> vertical (REVERSED)
direction = Placed.VERTICAL;
isReversed = true;
} else {
return null;
public String cluesGridToString() { return gridToString(clues.c()); }
public String gridRenderHuman() { return exportGrid(_ -> SPACE, INIT_GRID_OUTPUT_DASH_ARR.clone()); }
public String gridGridToString() { return exportGrid(d1 -> d1, INIT_GRID_OUTPUT_ARR.clone()); }
public ExportedPuzzle exportFormatFromFilled(Rewards rewards) {
if (slots.length == 0) {
return new ExportedPuzzle(new String(INIT_GRID_OUTPUT_DASH_ARR).split("\n"), new WordOut[0], 1, rewards);
}
return new Placed(
lemma,
startRow,
startCol,
direction,
s.clueR(),
s.clueC(),
cells,
isReversed
);
}
public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) {
var g = filled().grid();
var placed = new ArrayList<Placed>();
var clueMap = filled().clueMap();
g.grid().forEachSlot((int key, long packedPos, int len) -> {
var word = clueMap.get(key);
if (word != null) {
var p = extractPlacedFromSlot(Slot.from(key, packedPos, len), word);
if (p != null) placed.add(p);
}
});
// If nothing placed: return full grid mapped to letters/# only
if (placed.isEmpty()) {
var gridv2 = new String[R];
for (var r = 0; r < R; r++) {
var sb = new StringBuilder(C);
for (var c = 0; c < C; c++) {
int idx = Grid.offset(r, c);
sb.append(g.isLetterSet(idx) ? (char) g.byteAt(idx) : '#');
}
gridv2[r] = sb.toString();
}
return new ExportedPuzzle(gridv2, new WordOut[0], difficulty, rewards);
}
var placed = Arrays.stream(slots)
.map(slot -> new Placed(slot.assign().w, slot.key(), Riddle.cellWalk(slot.key(), slot.lo(), slot.hi())
.mapToObj(idx -> puzzle.cells[idx])
.toArray(Mask[]::new)))
.toArray(Placed[]::new);
// 2) bounding box around all word cells + arrow cells, with 1-cell margin
int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;
int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE;
for (var rc : placed) {
for (var c : rc.cells) {
minR = Math.min(minR, Grid.r(c));
minC = Math.min(minC, Grid.c(c));
maxR = Math.max(maxR, Grid.r(c));
maxC = Math.max(maxC, Grid.c(c));
}
minR = Math.min(minR, rc.arrowRow);
minC = Math.min(minC, rc.arrowCol);
maxR = Math.max(maxR, rc.arrowRow);
maxC = Math.max(maxC, rc.arrowCol);
}
// 3) map of only used letter cells (everything else becomes '#')
var letterAt = new HashMap<Long, Character>();
for (var p : placed) {
for (var c : p.cells) {
int rr = Grid.r(c), cc = Grid.c(c);
int idx = Grid.offset(rr, cc);
if (inBounds(rr, cc) && g.isLetterSet(idx)) {
letterAt.put(Bit.pack(rr, cc), (char) g.byteAt(idx));
}
for (var it : rc.cells()) {
minR = Math.min(minR, it.r());
minC = Math.min(minC, it.c());
maxR = Math.max(maxR, it.r());
maxC = Math.max(maxC, it.c());
}
}
// 4) render gridv2 over cropped bounds (out-of-bounds become '#')
var gridv2 = new String[Math.max(0, maxR - minR + 1)];
for (int r = minR, i = 0; r <= maxR; r++, i++) {
var row = new StringBuilder(Math.max(0, maxC - minC + 1));
for (var c = minC; c <= maxC; c++) row.append(letterAt.getOrDefault(Bit.pack(r, c), '#'));
gridv2[i] = row.toString();
}
// 3) render grid over cropped bounds (out-of-bounds become '#')
final int MINR = minR, MINC = minC;
int height = Math.max(0, maxR - minR + 1);
int width = Math.max(0, maxC - minC + 1);
byte[] template = new byte[height * (width + 1)];
Arrays.fill(template, DASH);
for (int i = width; i < template.length; i += width + 1) template[i] = LINE_BREAK;
puzzle.forEach(l -> l.letter(template, MINR, MINC, height, width));
var grid = new String(template).split("\n");
// 5) words output with cropped coordinates
int MIN_R = minR, MIN_C = minC;
var wordsOut = placed.stream().map(p -> new WordOut(
p.lemma,
p.startRow - MIN_R,
p.startCol - MIN_C,
p.direction,
p.arrowRow - MIN_R,
p.arrowCol - MIN_C,
p.isReversed
val bytes = BYTES.get().wordBytes();
var wordsOut = Arrays.stream(placed).map(p -> new WordOut(
p.lemma(),
p.startRow() - MINR,
p.startCol() - MINC,
p.direction(),
p.arrowRow() - MINR,
p.arrowCol() - MINC,
p.isReversed(), bytes
)).toArray(WordOut[]::new);
return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
var total = 0.0001d + Arrays.stream(wordsOut).mapToDouble(Riddle.WordOut::complex).sum();
return new ExportedPuzzle(grid, wordsOut, (int) (total / wordsOut.length), rewards);
}
}
record DictEntryDTO(ArrayList<Lemma> words, IntListDTO[][] pos) {
public DictEntryDTO(int L) {
this(new ArrayList<>(), new IntListDTO[L][26]);
for (var i = 0; i < L; i++) for (var j = 0; j < 26; j++) pos[i][j] = new IntListDTO();
}
}
static final class IntListDTO {
int[] a = new int[8];
int n = 0;
void add(int v) {
if (n >= a.length) a = Arrays.copyOf(a, a.length * 2);
a[n++] = v;
}
int size() { return n; }
int[] data() { return a; }
}
}
}

View File

@@ -1,61 +1,71 @@
package puzzle;
import module java.base;
import anno.ConstGen;
import anno.DictGen;
import anno.GenerateNeighbor;
import anno.GenerateNeighbors;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.val;
import puzzle.Riddle.ExportedPuzzle;
import puzzle.Riddle.Rewards;
import puzzle.Riddle.WordOut;
import puzzle.SwedishGenerator.Rng;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import static puzzle.CsvIndexService.SC;
import static puzzle.Export.*;
import static puzzle.SwedishGenerator.*;
import static puzzle.SwedishGenerator.Dict.loadDict;
import static puzzle.Export.Puzzle;
import static puzzle.Export.PuzzleResult;
import static puzzle.Riddle.Signa;
import static puzzle.SwedishGenerator.Dict;
import static puzzle.SwedishGenerator.Slotinfo;
import static puzzle.SwedishGenerator.fillMask;
@DictGen(
packageName = "puzzle.dict800",
className = "DictData800",
scv = "/home/mike/dev/puzzle-generator/nl_score_hints_v4.csv",
simpleMax = 800,
minLen = 2,
maxLen = 8
)
@ConstGen(C = 9, R = 8, packageName = "precomp", className = "Const9x8")
@GenerateNeighbors({
@GenerateNeighbor(C = 9, R = 8, packageName = "precomp", className = "Neighbors9x8", MIN_LEN = 2),
@GenerateNeighbor(C = 3, R = 4, packageName = "precomp", className = "Neighbors3x4", MIN_LEN = 2)
})
public class Main {
final static rci RCI = Masker.IT[0];
final static String OUT_DIR = envOrDefault("OUT_DIR", "/data/puzzle");
final static Path PUZZLE_DIR = Paths.get(OUT_DIR, "puzzles");
static final Path INDEX_FILE = PUZZLE_DIR.resolve("index.json");
static final OffsetDateTime now = OffsetDateTime.now(ZoneOffset.UTC);
static final String CREATED_AT = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
static final String FILE_ID = CREATED_AT.replace(":", "-") + "_" + System.currentTimeMillis() / 1000;
static final String FILE_NAME = FILE_ID + ".json";
static final Path OUTPUT_PATH = PUZZLE_DIR.resolve(FILE_NAME);
static final String DATE_STRING = now.toLocalDate().toString();
static final boolean VERBOSE = false;
static final AtomicLong TOTAL_NODES = new AtomicLong(0);
static final AtomicLong TOTAL_BACKTRACKS = new AtomicLong(0);
static final AtomicLong TOTAL_ATTEMPTS = new AtomicLong(0);
static final AtomicLong TOTAL_SUCCESS = new AtomicLong(0);
static final AtomicLong TOTAL_SIMPLICITY = new AtomicLong(0); // Scaled by 100 for precision
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Opts {
static int SSIZE = 23;
public int seed = (int) (System.nanoTime() ^ System.currentTimeMillis());
public int pop = 18;
public int gens = 1000;
public String wordsPath = "nl_score_hints_v3.csv";
public int clueSize = SSIZE;
public int pop = SSIZE * 2;
public int offspring = SSIZE * 3;
public int gens = 600;
public double minSimplicity = 0; // 0 means no limit
public int threads = Math.max(1, Runtime.getRuntime().availableProcessors());
public int tries = threads;
public int count = 2;
public boolean reindex = false;
public int fillTimeout = 20_000;
public boolean verbose = false;
public boolean verbose = true;
}
public void main(String[] args) {
var csv = Paths.get("nl_score_hints_v3.csv");
var idx = Paths.get("nl_score_hints_v3.idx");
ScopedValue.where(SC, new CsvIndexService(csv, idx)).run(() -> _main(args));
void main(String[] args) {
_main(args);
}
public void _main(String[] args) {
var opts = parseArgs(args);
@@ -70,64 +80,75 @@ public class Main {
section("Puzzle Generator");
info("OutputDir : " + OUT_DIR);
info("WordsFile : " + opts.wordsPath);
section("Settings");
printSettings(opts);
var res = generatePuzzle(opts);
if (res == null) {
err("Search status : UNSOLVED");
err("Reason : No solution found within tries.");
System.exit(1);
return;
}
section("Result");
res.filled().calcSimpel();
info(String.format(Locale.ROOT, "simplicity : %.2f", res.filled().stats().simplicity));
section("Mask");
System.out.print(indentLines(res.mask().gridToString(), " "));
section("Grid (raw)");
System.out.print(indentLines(res.filled().grid().gridToString(), " "));
section("Grid (human)");
System.out.print(indentLines(res.filled().grid().renderHuman(), " "));
var exported = res.exportFormatFromFilled(1, new Rewards(50, 2, 1));
section("Clues");
info("status : generating...");
info("generatedFor : " + exported.words().length);
info("status : done");
section("Words");
printWordsTable(exported.words());
section("Gridv2");
for (var row : exported.gridv2()) System.out.println(" " + row);
var theme = "algemeen";
section("Export");
info("file : " + OUTPUT_PATH);
try {
Files.createDirectories(PUZZLE_DIR);
var json = toJson(exported, DATE_STRING, theme);
Files.writeString(OUTPUT_PATH, json, StandardCharsets.UTF_8);
for (int count = 0; count < opts.count; count++) {
if (opts.count > 1) {
section("Generation " + (count + 1) + " / " + opts.count);
}
var res = generatePuzzle(opts);
opts.seed++; // Ensure different seed for next puzzle
// Update index.json
var pathInIndex = "/puzzles/" + FILE_NAME;
var indexRecord = toIndexRecordJson(FILE_ID, pathInIndex, DATE_STRING, theme, exported.difficulty(), CREATED_AT);
if (1 != 1) updateIndex(PUZZLE_DIR.toString(), indexRecord);
else rebuildIndex();
info("indexUpdated : " + INDEX_FILE);
} catch (IOException e) {
err("Failed to write: " + FILE_NAME);
err("Reason : " + e.getMessage());
System.exit(2);
if (res == null) {
err("Search status : UNSOLVED");
err("Reason : No solution found within tries.");
if (opts.count == 1) System.exit(1);
continue;
}
section("Mask");
System.out.print(indentLines(res.cluesGridToString()));
section("Grid (raw)");
System.out.print(indentLines(res.gridGridToString()));
section("Grid (human)");
System.out.print(indentLines(res.gridRenderHuman()));
var exported = res.exportFormatFromFilled(new Rewards(50, 2, 1));
section("Clues");
info("status : generating...");
info("generatedFor : " + exported.words().length);
info("status : done");
info("simpel : " + exported.difficulty());
section("Words");
printWordsTable(exported.words());
section("Grid");
for (var row : exported.grid()) System.out.println(" " + row);
var theme = "algemeen";
var now = OffsetDateTime.now(ZoneOffset.UTC);
var createdAt = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
var dateString = now.toLocalDate().toString();
var fileId = createdAt.replace(":", "-") + "_" + System.currentTimeMillis() / 1000 + "_" + count;
var fileName = fileId + ".json";
var outputPath = PUZZLE_DIR.resolve(fileName);
section("Export");
info("file : " + outputPath);
try {
Files.createDirectories(PUZZLE_DIR);
var json = toJson(exported, dateString, theme);
Files.writeString(outputPath, json, StandardCharsets.UTF_8);
// Update index.json
var pathInIndex = "/puzzles/" + fileName;
var indexRecord = toIndexRecordJson(fileId, pathInIndex, dateString, theme, exported.difficulty(), createdAt);
if (1 != 1) updateIndex(PUZZLE_DIR.toString(), indexRecord);
else rebuildIndex();
info("indexUpdated : " + INDEX_FILE);
} catch (IOException e) {
err("Failed to write: " + fileName);
err("Reason : " + e.getMessage());
if (opts.count == 1) System.exit(2);
}
}
}
@@ -149,11 +170,13 @@ public class Main {
private static void printSettings(Opts o) {
System.out.printf(Locale.ROOT, " %-14s: %d%n", "seed", o.seed);
System.out.printf(Locale.ROOT, " %-14s: %d%n", "clues", o.clueSize);
System.out.printf(Locale.ROOT, " %-14s: %d%n", "population", o.pop);
System.out.printf(Locale.ROOT, " %-14s: %d%n", "offspring", o.offspring);
System.out.printf(Locale.ROOT, " %-14s: %d%n", "generations", o.gens);
System.out.printf(Locale.ROOT, " %-14s: %s%n", "wordsPath", o.wordsPath);
System.out.printf(Locale.ROOT, " %-14s: %.2f%n", "minSimplicity", o.minSimplicity);
System.out.printf(Locale.ROOT, " %-14s: %d%n", "threads", o.threads);
System.out.printf(Locale.ROOT, " %-14s: %d%n", "count", o.count);
}
private static String fmtPoint(int r, int c) { return String.format(Locale.ROOT, "(%d,%d)", r, c); }
@@ -181,26 +204,29 @@ public class Main {
return s.substring(0, Math.max(0, max - 1)) + "";
}
static String indentLines(String s, String indent) {
static String indentLines(String s) {
if (s == null || s.isEmpty()) return "";
var lines = s.split("\\R", -1);
var sb = new StringBuilder();
for (var line : lines) sb.append(indent).append(line).append('\n');
for (var line : lines) sb.append(" ").append(line).append('\n');
return sb.toString();
}
static void usage() {
System.out.println("""
Usage:
java puzzle.Main [--seed N] [--pop N] [--gens N] [--tries N] [--words FILE] [--min-simplicity N.N] [--threads N] [--reindex]
Defaults:
--pop 18
--gens 500
--words nl_score_hints.csv
--min-simplicity 0 (no limit)
--threads %d
""".formatted(Math.max(1, Runtime.getRuntime().availableProcessors())));
System.out.printf("""
Usage:
java puzzle.Main [--seed N] [--clues N] [--pop N] [--offspring N] [--gens N] [--tries N] [--words FILE] [--min-simplicity N.N] [--threads N] [--count N] [--reindex]
Defaults:
--clues 18
--pop 40
--offspring 60
--gens 500
--words nl_score_hints.csv
--min-simplicity 0 (no limit)
--threads %d
--count 1
%n""", Math.max(1, Runtime.getRuntime().availableProcessors()));
}
static Opts parseArgs(String[] argv) {
@@ -217,24 +243,30 @@ public class Main {
if (a.equals("--seed")) {
out.seed = Integer.parseInt(v);
i++;
} else if (a.equals("--clues")) {
out.clueSize = Integer.parseInt(v);
i++;
} else if (a.equals("--pop")) {
out.pop = Integer.parseInt(v);
i++;
} else if (a.equals("--offspring")) {
out.offspring = Integer.parseInt(v);
i++;
} else if (a.equals("--gens")) {
out.gens = Integer.parseInt(v);
i++;
} else if (a.equals("--tries")) {
out.tries = Integer.parseInt(v);
i++;
} else if (a.equals("--words")) {
out.wordsPath = v;
i++;
} else if (a.equals("--min-simplicity")) {
out.minSimplicity = Double.parseDouble(v);
i++;
} else if (a.equals("--threads")) {
out.threads = Integer.parseInt(v);
i++;
} else if (a.equals("--count")) {
out.count = Integer.parseInt(v);
i++;
} else if (a.equals("--reindex")) {
out.reindex = true;
} else {
@@ -249,9 +281,9 @@ public class Main {
// Package-private method for testing
PuzzleResult generatePuzzle(Opts opts) {
var tLoad0 = System.nanoTime();
var dict = loadDict(opts.wordsPath);
var tLoad1 = System.nanoTime();
var tLoad0 = System.nanoTime();
Dict dict = puzzle.dict800.DictData800.DICT800;//loadDict(opts.wordsPath);
var tLoad1 = System.nanoTime();
section("Load");
info(String.format(Locale.ROOT, "words : %,d", dict.length()));
@@ -271,8 +303,8 @@ public class Main {
try {
// Keep at least some tasks in flight
for (int i = 0; i < opts.threads; i++) {
final int attempt = ++submitted;
completionService.submit(() -> attempt(new Rng(opts.seed + attempt), dict, opts));
final int attemptIdx = ++submitted;
completionService.submit(() -> attempt(new Rng(opts.seed + attemptIdx), dict, opts));
}
while (System.currentTimeMillis() < deadline) {
@@ -288,8 +320,8 @@ public class Main {
// Submit another task if we still have time
if (System.currentTimeMillis() < deadline) {
final int attempt = ++submitted;
completionService.submit(() -> attempt(new Rng(opts.seed + attempt), dict, opts));
final int attemptIdx = ++submitted;
completionService.submit(() -> attempt(new Rng(opts.seed + attemptIdx), dict, opts));
}
}
if (resFinal == null) warn("status : UNSOLVED (timeout)");
@@ -300,6 +332,11 @@ public class Main {
warn("status : ERROR (" + e.getMessage() + ")");
} finally {
executor.shutdownNow();
try {
executor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} else {
@@ -335,48 +372,75 @@ public class Main {
section("Material");
info(String.format(Locale.ROOT, "attempts : %,d", TOTAL_ATTEMPTS.get()));
info(String.format(Locale.ROOT, "successRate : %.1f%%", TOTAL_ATTEMPTS.get() == 0 ? 0 : TOTAL_SUCCESS.get() * 100.0 / TOTAL_ATTEMPTS.get()));
if (TOTAL_SUCCESS.get() > 0) {
info(String.format(Locale.ROOT, "avgSimplic : %.2f", TOTAL_SIMPLICITY.get() / 100.0 / TOTAL_SUCCESS.get()));
}
info(String.format(Locale.ROOT, "dictWords : %,d", dict.length()));
return resFinal;
}
static PuzzleResult attempt(Rng rng, Dict dict, Opts opts) {
try {
return _attempt(rng, dict, opts);
} catch (Exception e) {
System.err.println("Failed to operate" + e.getMessage());
return null;
}
}
static Clues generateNewClues(Rng rng, Opts opts) {
var masker = new Masker_Neighbors9x8(rng, new int[Masker_Neighbors9x8.STACK_SIZE], Clues.createEmpty());
return masker.generateMask(opts.clueSize, opts.pop, opts.gens, opts.offspring);
}
static PuzzleResult _attempt(Rng rng, Dict dict, Opts opts) {
val multiThreaded = Thread.currentThread().getName().contains("pool");
long t0 = System.currentTimeMillis();
TOTAL_ATTEMPTS.incrementAndGet();
var swe = new SwedishGenerator(rng);
var mask = swe.generateMask(opts.pop, opts.gens, Math.max(opts.pop, (int) Math.floor(opts.pop * 1.5)));
var filled = swe.fillMask(mask, dict.index(), opts.fillTimeout);
val mask = generateNewClues(rng, opts);
//val mask = generateClues();
if (mask == null) return null;
TOTAL_NODES.addAndGet(filled.stats().nodes);
TOTAL_BACKTRACKS.addAndGet(filled.stats().backtracks);
if (filled.ok()) {
filled.calcSimpel();
TOTAL_SUCCESS.incrementAndGet();
TOTAL_SIMPLICITY.addAndGet((long) (filled.stats().simplicity * 100));
val slotInfo = Masker_Neighbors9x8.slots(mask, dict.index(), dict.reversed());
var grid = Masker_Neighbors9x8.grid(slotInfo);// mask.toGrid();
var filled = fillMask(rng, slotInfo, grid.lo, grid.hi, grid.g);
if (Thread.currentThread().isInterrupted() && !filled.ok()) {
return null;
}
var name = Thread.currentThread().getName();
var status = filled.ok() ? "SUCCESS" : "FAILED";
var simplicity = String.format(Locale.ROOT, "%.2f", filled.stats().simplicity);
var nps = (int) (filled.stats().nodes / Math.max(0.001, filled.stats().seconds));
if (!multiThreaded) {
System.out.print("\r" + " ".repeat(120 - "".length()) + "\r");
System.out.flush();
}
// print a final progress line
if (Main.VERBOSE && !multiThreaded) {
System.out.printf(Locale.ROOT,
"[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs%n",
Slotinfo.wordCount(0, slotInfo), slotInfo.length, filled.nodes(), filled.backtracks(), filled.lastMRV(), filled.elapsed() * 0.001
);
}
TOTAL_NODES.addAndGet(filled.nodes());
TOTAL_BACKTRACKS.addAndGet(filled.backtracks());
if (filled.ok()) {
TOTAL_SUCCESS.incrementAndGet();
}
var name = Thread.currentThread().getName();
var status = filled.ok() ? "SUCCESS" : "FAILED";
var nps = (int) (filled.nodes() / Math.max(0.001, filled.elapsed() * 0.001));
var totalTime = (System.currentTimeMillis() - t0) / 1000.0;
System.out.printf(Locale.ROOT,
"[ATTEMPT] thread=%s | status=%s | nodes=%d | backtracks=%d | nps=%d | simplicity=%s | time=%.1fs%n",
name, status, filled.stats().nodes, filled.stats().backtracks, nps, simplicity, filled.stats().seconds
name, status, filled.nodes(), filled.backtracks(), nps, 1, totalTime
);
if (filled.ok() && (opts.minSimplicity <= 0 || filled.stats().simplicity >= opts.minSimplicity)) {
return new PuzzleResult(swe, dict, new Gridded(mask), filled);
if (!filled.ok()) {
//System.out.println(Arrays.stream(new Clued(mask).gridToString().split("\n")).map(s -> "\"" + s + "\\n\" +").collect(Collectors.joining("\n")));
}
if (filled.ok()) {
grid.lo = ~mask.lo;
grid.hi = 0xFFL & ~mask.hi;
return new PuzzleResult(new Signa(mask), new Puzzle(grid, mask), slotInfo, filled);
}
if (opts.verbose && filled.ok()) {
System.err.printf(Locale.ROOT,
"simplicity : %.2f (below min %.2f)%n",
filled.stats().simplicity, opts.minSimplicity
);
}
return null;
}
@@ -384,7 +448,7 @@ public class Main {
record JsonExportedPuzzle(String date, String theme, int difficulty, Rewards rewards, String[] grid, WordOut[] words) { }
private static String toJson(ExportedPuzzle puzzle, String date, String theme) {
return CsvIndexService.GSON.toJson(new JsonExportedPuzzle(date, theme, puzzle.difficulty(), puzzle.rewards(), puzzle.gridv2(), puzzle.words()));
return Meta.GSON.toJson(new JsonExportedPuzzle(date, theme, puzzle.difficulty(), puzzle.rewards(), puzzle.grid(), puzzle.words()));
}
private static String escapeJson(String s) {

View File

@@ -0,0 +1,732 @@
package puzzle;
import module java.base;
import anno.GenerateShapedCopies;
import anno.Shaped;
import lombok.val;
import precomp.Neighbors9x8;
import static java.lang.Long.bitCount;
import static java.lang.Long.lowestOneBit;
import static java.lang.Long.numberOfLeadingZeros;
import static java.lang.Long.numberOfTrailingZeros;
import static puzzle.SwedishGenerator.Dict;
import static puzzle.SwedishGenerator.DictEntry;
import static puzzle.SwedishGenerator.Grid;
import static puzzle.SwedishGenerator.MAX_TRIES_PER_SLOT;
import static puzzle.SwedishGenerator.Rng;
import static puzzle.SwedishGenerator.Slotinfo;
@GenerateShapedCopies(
packageName = "puzzle",
className = "Masker",
shapes = { "precomp.Neighbors9x8", "precomp.Neighbors3x4" }
)
public final class Masker {
public static final long X = 0L;
@Shaped public static final int SIZE = Neighbors9x8.SIZE;
@Shaped public static final rci[] IT = Neighbors9x8.IT;
@Shaped public static final long[] PATH_LO = Neighbors9x8.PATH_LO;
@Shaped public static final long[] PATH_HI = Neighbors9x8.PATH_HI;
@Shaped public static final long MASK_LO = Neighbors9x8.MASK_LO;
@Shaped public static final long RANGE_0_SIZE = Neighbors9x8.RANGE_0_SIZE;
@Shaped public static final long RANGE_0_624 = Neighbors9x8.RANGE_0_624;
@Shaped public static final long MASK_HI = Neighbors9x8.MASK_HI;//(1L << (SIZE - 64)) - 1;
@Shaped public static final int MIN_LEN = Neighbors9x8.MIN_LEN;//Config.MIN_LEN;
@Shaped public static final int C = Neighbors9x8.C;
@Shaped public static final int R = Neighbors9x8.R;
@Shaped public static final double SIZED = Neighbors9x8.SIZED;// ~18
@Shaped private static final long[] NBR_LO = Neighbors9x8.NBR_LO;
@Shaped private static final long[] NBR_HI = Neighbors9x8.NBR_HI;
public static final int[][] MUTATE_RI = new int[SIZE][625];
private static final boolean VERBOSE = false;
private final int[] activeCIdx = new int[SIZE];
private final long[] activeSLo = new long[SIZE];
private final long[] activeSHi = new long[SIZE];
private final long[] adjLo = new long[SIZE];
private final long[] adjHi = new long[SIZE];
private final int[] rCount = new int[R];
private final int[] cCount = new int[C];
private final Rng rng;
private final int[] stack;
private final Clues cache;
public static final int STACK_SIZE = 128;
public Masker(Rng rng, int[] stack, Clues cache) {
this.rng = rng;
this.stack = stack;
this.cache = cache;
}
public Clues cache(Clues clues) { return cache.from(clues); }
public static boolean isLo(int n) { return (n & 64) == 0; }
static {
for (var i = 0; i < SIZE; i++) {
var k = 0;
for (var dr1 = -2; dr1 <= 2; dr1++)
for (var dr2 = -2; dr2 <= 2; dr2++)
for (var dc1 = -2; dc1 <= 2; dc1++)
for (var dc2 = -2; dc2 <= 2; dc2++) {
val ti = IT[i];
MUTATE_RI[i][k++] = offset(clamp(ti.r() + dr1 + dr2, 0, R - 1),
clamp(ti.c() + dc1 + dc2, 0, C - 1));
}
}
}
public static double similarity(Clues a, Clues b) {
long diffLo = a.lo ^ b.lo;
long diffHi = a.hi ^ b.hi;
int diffPos = bitCount(diffLo) + bitCount(diffHi);
long commonLo = a.lo & b.lo;
long commonHi = a.hi & b.hi;
int diffDir = bitCount(commonLo & (a.vlo ^ b.vlo | a.rlo ^ b.rlo | a.xlo ^ b.xlo))
+ bitCount(commonHi & (a.vhi ^ b.vhi | a.rhi ^ b.rhi | a.xhi ^ b.xhi));
return (SIZED - (diffPos + diffDir)) / SIZED;
}
public static Grid grid(Slotinfo[] slots) {
long lo = X, hi = X;
for (var slot : slots) {
lo |= slot.lo();
hi |= slot.hi();
}
return new Grid(new byte[SIZE], ~lo & MASK_LO, ~hi & MASK_HI);
}
private long getWordPathLo(Clues c, int idx, int dir) {
int key = (idx << 3) | dir;
long sLo = PATH_LO[key], sHi = PATH_HI[key];
long hLo = sLo & c.lo, hHi = sHi & c.hi;
if ((dir & 2) == 0) { // Increasing
if (hLo != X) return sLo & ((hLo & -hLo) - 1);
if (hHi != X) return sLo;
return sLo;
} else {
if (hHi != X) return 0;
if (hLo != X) return sLo & -(Long.highestOneBit(hLo) << 1);
return sLo;
}
}
private long getWordPathHi(Clues c, int idx, int dir) {
int key = (idx << 3) | dir;
long sLo = PATH_LO[key], sHi = PATH_HI[key];
long hLo = sLo & c.lo, hHi = sHi & c.hi;
if ((dir & 2) == 0) { // Increasing
if (hLo != X) return 0;
if (hHi != X) return sHi & ((hHi & -hHi) - 1);
return sHi;
} else {
if (hHi != X) return sHi & -(Long.highestOneBit(hHi) << 1);
if (hLo != X) return sHi;
return sHi;
}
}
public boolean isValid(Clues c) {
return findOffendingClue(c, -1) == -1;
}
public boolean isValid(Clues c, int ri) {
return findOffendingClue(c, ri) == -1;
}
private boolean isDeadCell(Clues c, int idx) {
var rci = IT[idx];
return (4 - rci.nbrCount()) + bitCount(rci.n1() & c.lo) + bitCount(rci.n2() & c.hi) >= 3;
}
private int findAdjacentClue(Clues c, int ri) {
var rci = IT[ri];
long nLo = rci.n1() & c.lo;
if (nLo != 0) return numberOfTrailingZeros(nLo);
long nHi = rci.n2() & c.hi;
if (nHi != 0) return 64 | numberOfTrailingZeros(nHi);
return -1;
}
public int findOffendingClue(Clues c) {
return findOffendingClue(c, -1);
}
public int findOffendingClue(Clues c, int ri) {
int num = 0;
for (long bits = c.lo; bits != X; bits &= bits - 1) activeCIdx[num++] = numberOfTrailingZeros(bits);
for (long bits = c.hi; bits != X; bits &= bits - 1) activeCIdx[num++] = 64 | numberOfTrailingZeros(bits);
if (num == 0) return -1;
for (int i = 0; i < num; i++) {
int idx = activeCIdx[i];
int dir = c.getDir(idx);
activeSLo[i] = getWordPathLo(c, idx, dir);
activeSHi[i] = getWordPathHi(c, idx, dir);
}
long ri_lo = (ri != -1 && ri < 64) ? (1L << ri) : 0;
long ri_hi = (ri != -1 && ri >= 64) ? (1L << (ri - 64)) : 0;
for (int i = 0; i < num; i++) {
int idx = activeCIdx[i];
int dir = c.getDir(idx);
boolean affected = (ri == -1) || (idx == ri) || ((PATH_LO[(idx << 3) | dir] & ri_lo) != 0) || ((PATH_HI[(idx << 3) | dir] & ri_hi) != 0);
if (affected) {
long sLo = activeSLo[i], sHi = activeSHi[i];
if (bitCount(sLo) + bitCount(sHi) < MIN_LEN) return idx;
for (int j = 0; j < num; j++) {
if (i == j) continue;
long combined = (sLo & activeSLo[j]) | (sHi & activeSHi[j]);
if (combined != 0 && (combined & (combined - 1)) != 0) return idx;
}
}
}
if (ri == -1) {
for (long bits = ~c.lo & MASK_LO; bits != X; bits &= bits - 1) {
int clueIdx = numberOfTrailingZeros(bits);
if (isDeadCell(c, clueIdx)) return findAdjacentClue(c, clueIdx);
}
for (long bits = ~c.hi & MASK_HI; bits != X; bits &= bits - 1) {
int clueIdx = numberOfTrailingZeros(bits);
if (isDeadCell(c, 64 | clueIdx)) return findAdjacentClue(c, 64 | clueIdx);
}
} else {
if (c.notClue(ri) && isDeadCell(c, ri)) return findAdjacentClue(c, ri);
var rci = IT[ri];
for (long bits = rci.n1(); bits != X; bits &= bits - 1) {
int nIdx = numberOfTrailingZeros(bits);
if (c.notClue(nIdx) && isDeadCell(c, nIdx)) return findAdjacentClue(c, nIdx);
}
for (long bits = rci.n2(); bits != X; bits &= bits - 1) {
int nIdx = numberOfTrailingZeros(bits);
if (c.notClue(64 | nIdx) && isDeadCell(c, 64 | nIdx)) return findAdjacentClue(c, 64 | nIdx);
}
}
return -1;
}
public void cleanup(Clues c) {
var guard = 0;
while (guard++ < 50) {
var offending = findOffendingClue(c);
if (offending == -1) break;
if ((offending & 64) == 0) c.clearClueLo(~(1L << offending));
else c.clearClueHi(~(1L << (offending & 63)));
}
}
// slice ray to stop before first clue, depending on direction monotonicity
// right/down => increasing indices; up/left => decreasing indices
// first clue is highest index among hits (hi first, then lo)
static void processSlotRev(Clues c, SlotVisitor visitor, int key) {
var rayLo = PATH_LO[key];
var rayHi = PATH_HI[key];
// only consider clue cells
var hitsLo = rayLo & c.lo;
var hitsHi = rayHi & c.hi;
if (hitsHi != X) {
var msb = 63 - numberOfLeadingZeros(hitsHi);
var stop = 1L << msb;
rayHi &= -(stop << 1); // keep bits > stop
rayLo = 0; // lo indices are below stop
} else if (hitsLo != X) {
var msb = 63 - numberOfLeadingZeros(hitsLo);
var stop = 1L << msb;
rayLo &= -(stop << 1);
}
if (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN)
visitor.visit(key, rayLo, rayHi);
}
static boolean validSlotRev(long lo, long hi, int key) {
var rayLo = PATH_LO[key];
var rayHi = PATH_HI[key];
// only consider clue cells
var hitsLo = rayLo & lo;
var hitsHi = rayHi & hi;
if (hitsHi != X) return (Long.bitCount(rayHi & -(1L << 63 - numberOfLeadingZeros(hitsHi) << 1)) >= MIN_LEN);
else if (hitsLo != X) return (Long.bitCount(rayLo & -(1L << 63 - numberOfLeadingZeros(hitsLo) << 1)) + Long.bitCount(rayHi) >= MIN_LEN);
else return (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN);
}
static boolean validSlot(long lo, long hi, int key) {
var rayLo = PATH_LO[key];
var rayHi = PATH_HI[key];
var hitsLo = rayLo & lo;
var hitsHi = rayHi & hi;
if (hitsLo != X) return (Long.bitCount(rayLo & ((1L << numberOfTrailingZeros(hitsLo)) - 1)) >= MIN_LEN);
else if (hitsHi != X) return (Long.bitCount(rayLo) + Long.bitCount(rayHi & ((1L << numberOfTrailingZeros(hitsHi)) - 1)) >= MIN_LEN);
else return (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN);
}
static void processSlot(Clues c, SlotVisitor visitor, int key) {
var rayLo = PATH_LO[key];
var rayHi = PATH_HI[key];
var hitsLo = rayLo & c.lo;
var hitsHi = rayHi & c.hi;
if (hitsLo != X) {
var stop = 1L << numberOfTrailingZeros(hitsLo);
rayLo &= (stop - 1);
rayHi = 0;
} else if (hitsHi != X) {
var stop = 1L << numberOfTrailingZeros(hitsHi);
rayHi &= (stop - 1);
}
if (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN)
visitor.visit(key, rayLo, rayHi);
}
public static void forEachSlot(Clues c, SlotVisitor visitor) {
final long lo = c.lo, hi = c.hi, xlo = c.xlo, xhi = c.xhi, rlo = c.rlo, rhi = c.rhi, vlo = c.vlo, vhi = c.vhi;
for (var l = lo & ~xlo & ~rlo & vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 1));
for (var l = lo & ~xlo & ~rlo & ~vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 0));
for (var l = lo & ~xlo & rlo & ~vlo; l != X; l &= l - 1) processSlotRev(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 2));
for (var l = lo & ~xlo & rlo & vlo; l != X; l &= l - 1) processSlotRev(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 3));
for (var l = lo & xlo & ~rlo & ~vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 4));
for (var l = lo & xlo & ~rlo & vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 5));
for (var h = hi & ~xhi & ~rhi & vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 1));
for (var h = hi & ~xhi & ~rhi & ~vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 0));
for (var h = hi & ~xhi & rhi & ~vhi; h != X; h &= h - 1) processSlotRev(c, visitor, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 2));
for (var h = hi & ~xhi & rhi & vhi; h != X; h &= h - 1) processSlotRev(c, visitor, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 3));
for (var h = hi & xhi & ~rhi & ~vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 4));
for (var h = hi & xhi & ~rhi & vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 5));
}
public static Slot[] extractSlots(Clues c, DictEntry[] index, DictEntry[] rev) {
var slots = new ArrayList<Slot>(c.clueCount());
forEachSlot(c, (key, lo, hi) -> slots.add(Slot.from(key, lo, hi, Slotinfo.increasing(key) ? index[Slot.length(lo, hi)] : rev[Slot.length(lo, hi)])));
return slots.toArray(Slot[]::new);
}
public static Slotinfo[] slots(Clues mask, Dict d) { return slots(mask, d.index(), d.reversed()); }
public static Slotinfo[] slots(Clues mask, DictEntry[] index, DictEntry[] rev) {
var slots = extractSlots(mask, index, rev);
return scoreSlots(slots);
}
public static Slotinfo[] scoreSlots(Slot[] slots) {
val count = new byte[SIZE];
var slotInfo = new Slotinfo[slots.length];
for (var s : slots) {
for (var b = s.lo; b != X; b &= b - 1) count[numberOfTrailingZeros(b)]++;
for (var b = s.hi; b != X; b &= b - 1) count[64 | numberOfTrailingZeros(b)]++;
}
for (var i = 0; i < slots.length; i++) {
var slot = slots[i];
slotInfo[i] = new Slotinfo(slot.key, slot.lo, slot.hi, slotScore(count, slot.lo, slot.hi), new puzzle.SwedishGenerator.Assign(), slot.entry,
Math.min(slot.entry.words().length, MAX_TRIES_PER_SLOT));
}
return slotInfo;
}
public static int slotScore(byte[] count, long lo, long hi) {
var cross = 0;
for (var b = lo; b != X; b &= b - 1) cross += (count[numberOfTrailingZeros(b)] - 1);
for (var b = hi; b != X; b &= b - 1) cross += (count[64 | numberOfTrailingZeros(b)] - 1);
return cross * 10 + Slot.length(lo, hi);
}
public static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
public static int offset(int r, int c) { return r | (c << 3); }
public long maskFitness(final Clues grid, int clueSize) {
long cHLo = 0L, cHHi = 0L, cVLo = 0L, cVHi = 0L;
long cHLo2 = 0L, cHHi2 = 0L, cVLo2 = 0L, cVHi2 = 0L;
final long lo_cl = grid.lo, hi_cl = grid.hi;
final int numCluesTotal = bitCount(lo_cl) + bitCount(hi_cl);
if (numCluesTotal == 0) return 1_000_000_000L;
long penalty = Math.abs(numCluesTotal - clueSize) * 16000L;
boolean hasSlots = false;
int numClues = 0;
for (int part = 0; part < 2; part++) {
long bits = (part == 0) ? lo_cl : hi_cl;
long vlo = (part == 0) ? grid.vlo : grid.vhi;
long rlo = (part == 0) ? grid.rlo : grid.rhi;
long xlo = (part == 0) ? grid.xlo : grid.xhi;
int base = (part == 0) ? 0 : 64;
while (bits != X) {
long lsb = bits & -bits;
int clueIdx = base | numberOfTrailingZeros(lsb);
int dir = (vlo & lsb) != 0 ? 1 : 0;
if ((rlo & lsb) != 0) dir |= 2;
if ((xlo & lsb) != 0) dir |= 4;
int key = (clueIdx << 3) | dir;
long rLo = PATH_LO[key], rHi = PATH_HI[key];
long hLo = rLo & lo_cl, hHi = rHi & hi_cl;
if ((dir & 2) == 0) { // Increasing
if (hLo != X) {
rLo &= (hLo & -hLo) - 1;
rHi = 0;
} else if (hHi != X) rHi &= (hHi & -hHi) - 1;
} else if (hHi != X) {
rHi &= -(Long.highestOneBit(hHi) << 1);
rLo = 0;
} else if (hLo != X) { rLo &= -(Long.highestOneBit(hLo) << 1); }
activeCIdx[numClues] = clueIdx;
activeSLo[numClues] = rLo;
activeSHi[numClues] = rHi;
numClues++;
long combined = rLo | rHi;
if (combined != X) {
hasSlots = true;
if (Slot.horiz(dir)) {
cHLo2 |= (cHLo & rLo);
cHHi2 |= (cHHi & rHi);
cHLo |= rLo;
cHHi |= rHi;
} else {
cVLo2 |= (cVLo & rLo);
cVHi2 |= (cVHi & rHi);
cVLo |= rLo;
cVHi |= rHi;
}
int wordLen = bitCount(combined);
if (wordLen < MIN_LEN) penalty += 8000;
if (wordLen > 6) penalty += (wordLen - 6) * 1000L;
} else penalty += 25000;
bits &= ~lsb;
}
}
if (!hasSlots) return 1_000_000_000L;
penalty += (bitCount(cHLo2) + bitCount(cHHi2) + bitCount(cVLo2) + bitCount(cVHi2)) * 10000L;
Arrays.fill(rCount, 0);
Arrays.fill(cCount, 0);
for (int i = 0; i < numClues; i++) {
int idx = activeCIdx[i];
rCount[idx & 7]++;
cCount[idx >> 3]++;
}
for (int rc : rCount) {
if (rc < 2) penalty += (2 - rc) * 4000L;
if (rc > 4) penalty += (rc - 4) * 4000L;
}
for (int cc : cCount) {
if (cc < 2) penalty += (2 - cc) * 4000L;
if (cc > 4) penalty += (cc - 4) * 4000L;
}
// Connectivity check
for (int i = 0; i < numClues; i++) adjLo[i] = 0;
for (int i = 0; i < numClues; i++) {
for (int j = i + 1; j < numClues; j++) {
if (((activeSLo[i] & activeSLo[j]) | (activeSHi[i] & activeSHi[j])) != 0) {
adjLo[i] |= (1L << j);
adjLo[j] |= (1L << i);
}
}
}
if (numClues > 0) {
int maxReached = 0;
long totalReached = 0;
for (int i = 0; i < numClues; i++) {
if ((totalReached & (1L << i)) != 0) continue;
long currentReached = (1L << i);
stack[0] = i;
int sp = 1, count = 0;
while (sp > 0) {
int cur = stack[--sp];
count++;
long neighbors = adjLo[cur] & ~currentReached;
while (neighbors != 0) {
long lsb = neighbors & -neighbors;
currentReached |= lsb;
stack[sp++] = numberOfTrailingZeros(lsb);
neighbors &= ~lsb;
}
}
if (count > maxReached) maxReached = count;
totalReached |= currentReached;
}
if (maxReached < numClues) {
penalty += (numClues - maxReached) * 4000L;
penalty += 20000;
}
}
for (long bits = ~lo_cl & MASK_LO; bits != X; bits &= bits - 1) {
int clueIdx = numberOfTrailingZeros(bits);
var rci = IT[clueIdx];
if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 3000;
boolean h = (cHLo & (1L << clueIdx)) != X, v = (cVLo & (1L << clueIdx)) != X;
if (!h && !v) penalty += 4000;
else if (h && v) { /* ok */ } else penalty += 1500;
}
for (long bits = ~hi_cl & MASK_HI; bits != X; bits &= bits - 1) {
int clueIdx = numberOfTrailingZeros(bits);
var rci = IT[64 | clueIdx];
if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 3000;
boolean h = (cHHi & (1L << clueIdx)) != X, v = (cVHi & (1L << clueIdx)) != X;
if (!h && !v) penalty += 4000;
else if (h && v) { /* ok */ } else penalty += 1500;
}
long remLo = lo_cl, remHi = hi_cl;
while ((remLo | remHi) != X) {
int start = (remLo != X) ? numberOfTrailingZeros(remLo) : (64 | numberOfTrailingZeros(remHi));
stack[0] = start;
long currentCompLo = (start < 64) ? (1L << start) : 0;
long currentCompHi = (start >= 64) ? (1L << (start - 64)) : 0;
int sp = 1, s = 0;
while (sp > 0) {
int cur = stack[--sp];
s++;
long nLo = NBR_LO[cur] & lo_cl & ~currentCompLo;
long nHi = NBR_HI[cur] & hi_cl & ~currentCompHi;
while (nLo != 0) {
long lsb = nLo & -nLo;
currentCompLo |= lsb;
stack[sp++] = numberOfTrailingZeros(lsb);
nLo &= ~lsb;
}
while (nHi != 0) {
long lsb = nHi & -nHi;
currentCompHi |= lsb;
stack[sp++] = 64 | numberOfTrailingZeros(lsb);
nHi &= ~lsb;
}
}
if (s >= 2) penalty += (long) (s - 1) * 3000;
remLo &= ~currentCompLo;
remHi &= ~currentCompHi;
}
return penalty;
}
public static boolean hasRoomForClue(Clues c, int key) {
if (Slotinfo.increasing(key)) if (!validSlot(c.lo, c.hi, key)) return false;
return validSlotRev(c.lo, c.hi, key);
}
public Clues randomMask(final int clueSize) {
var g = Clues.createEmpty();
for (int placed = 0, guard = 0, ri; placed < clueSize && guard < 4000; guard++) {
ri = rng.randint0_SIZE(RANGE_0_SIZE);
if (isLo(ri)) {
if (g.isClueLo(ri)) continue;
var d_idx = rng.randomClueDir();
var key = Slot.packSlotKey(ri, d_idx);
if (hasRoomForClue(g, key)) {
g.setClueLo(1L << ri, d_idx);
if (isValid(g, ri)) placed++;
else g.clearClueLo(~(1L << ri));
}
} else {
if (g.isClueHi(ri)) continue;
var d_idx = rng.randomClueDir();
var key = Slot.packSlotKey(ri, d_idx);
if (hasRoomForClue(g, key)) {
g.setClueHi(1L << (ri & 63), d_idx);
if (isValid(g, ri)) placed++;
else g.clearClueHi(~(1L << (ri & 63)));
}
}
}
return g;
}
public Clues mutate(Clues c) {
var bytes = MUTATE_RI[rng.randint0_SIZE(RANGE_0_SIZE)];
for (int k = 0, ri; k < 6; k++) {
ri = bytes[rng.randint0_624(RANGE_0_624)];
if (c.notClue(ri)) { // ADD
var d = rng.randomClueDir();
var key = Slot.packSlotKey(ri, d);
if (hasRoomForClue(c, key)) {
if (isLo(ri)) {
c.setClueLo(1L << ri, d);
if (!isValid(c, ri)) c.clearClueLo(~(1L << ri));
else continue;
} else {
c.setClueHi(1L << (ri & 63), d);
if (!isValid(c, ri)) c.clearClueHi(~(1L << (ri & 63)));
else continue;
}
}
} else { // HAS CLUE
var op = rng.randomClueDir();
if (op < 2) { // REMOVE
var oldD = c.getDir(ri);
if (isLo(ri)) {
c.clearClueLo(~(1L << ri));
if (!isValid(c, ri)) c.setClueLo(1L << ri, oldD);
else continue;
} else {
c.clearClueHi(~(1L << (ri & 63)));
if (!isValid(c, ri)) c.setClueHi(1L << (ri & 63), oldD);
else continue;
}
}
if (op < 4) { // CHANGE DIRECTION
var d = rng.randomClueDir();
var key = Slot.packSlotKey(ri, d);
if (hasRoomForClue(c, key)) {
var oldD = c.getDir(ri);
if (isLo(ri)) {
c.setClueLo(1L << ri, d);
if (!isValid(c, ri)) c.setClueLo(1L << ri, oldD);
else continue;
} else {
c.setClueHi(1L << (ri & 63), d);
if (!isValid(c, ri)) c.setClueHi(1L << (ri & 63), oldD);
else continue;
}
}
} // MOVE
var nri = bytes[rng.randint0_624(RANGE_0_624)];
if (c.notClue(nri)) {
var d = c.getDir(ri);
var nkey = Slot.packSlotKey(nri, d);
if (hasRoomForClue(c, nkey)) {
if (isLo(ri)) c.clearClueLo(~(1L << ri));
else c.clearClueHi(~(1L << (ri & 63)));
if (isLo(nri)) c.setClueLo(1L << nri, d);
else c.setClueHi(1L << (nri & 63), d);
if (!isValid(c, -1)) { // For MOVE, it's easier to check full grid or we'd need to check both ri and nri
if (isLo(nri)) c.clearClueLo(~(1L << nri));
else c.clearClueHi(~(1L << (nri & 63)));
if (isLo(ri)) c.setClueLo(1L << ri, d);
else c.setClueHi(1L << (ri & 63), d);
} else continue;
}
}
}
}
return c;
}
public Clues crossover(Clues a, Clues other) {
var theta = rng.nextFloat() * Math.PI;
var nc = Math.cos(theta);
var nr = Math.sin(theta);
long maskLo = 0, maskHi = 0;
for (var rci : IT)
if ((rci.cross_r()) * nc + (rci.cross_c()) * nr < 0) {
var i = rci.i();
if ((i & 64) == 0) maskLo |= (1L << i);
else maskHi |= (1L << (i - 64));
}
var c = new Clues(
(a.lo & ~maskLo) | (other.lo & maskLo),
(a.hi & ~maskHi) | (other.hi & maskHi),
(a.vlo & ~maskLo) | (other.vlo & maskLo),
(a.vhi & ~maskHi) | (other.vhi & maskHi),
(a.rlo & ~maskLo) | (other.rlo & maskLo),
(a.rhi & ~maskHi) | (other.rhi & maskHi),
(a.xlo & ~maskLo) | (other.xlo & maskLo),
(a.xhi & ~maskHi) | (other.xhi & maskHi));
cleanup(c);
return c;
}
public Clues hillclimb(Clues start, int clue_size, int limit) {
var best = start;
var bestF = maskFitness(best, clue_size);
var fails = 0;
while (fails < limit) {
cache.from(best);
var cand = mutate(best);
var f = maskFitness(cand, clue_size);
if (f < bestF) {
best = cand;
bestF = f;
fails = 0;
} else {
best.from(cache);
fails++;
}
}
return best;
}
public Clues generateMask(int clueSize, int popSize, int gens, int offspring) {
class GridAndFit {
Clues grid;
long fite = -1;
GridAndFit(Clues grid) { this.grid = grid; }
long fit() {
if (fite == -1) this.fite = maskFitness(grid, clueSize);
return this.fite;
}
}
if (VERBOSE) System.out.println("generateMask init pop: " + popSize + " clueSize: " + clueSize);
var pop = new GridAndFit[popSize];
for (var i = 0; i < popSize; i++) {
if (Thread.currentThread().isInterrupted()) return null;
pop[i] = new GridAndFit(hillclimb(randomMask(clueSize), clueSize, 180));
}
for (var gen = 0; gen < gens; gen++) {
if (Thread.currentThread().isInterrupted()) return null;
var children = new GridAndFit[offspring];
var childCount = 0;
for (var k = 0; k < offspring; k++) {
if (Thread.currentThread().isInterrupted()) return null;
var p1 = rng.rand(pop);
var p2 = rng.rand(pop);
var child = crossover(p1.grid, p2.grid);
children[k] = new GridAndFit(hillclimb(child, clueSize, 70));
childCount++;
}
var combined = new GridAndFit[pop.length + childCount];
System.arraycopy(pop, 0, combined, 0, pop.length);
System.arraycopy(children, 0, combined, pop.length, childCount);
Arrays.sort(combined, Comparator.comparingLong(GridAndFit::fit));
var next = new GridAndFit[popSize];
var nextCount = 0;
for (var cand : combined) {
if (nextCount >= popSize) break;
var unique = true;
for (var i = 0; i < nextCount; i++) {
if (similarity(cand.grid, next[i].grid) > 0.92) {
unique = false;
break;
}
}
if (unique) next[nextCount++] = cand;
}
if (nextCount < popSize) {
for (var cand : combined) {
if (nextCount >= popSize) break;
var alreadyIn = false;
for (var i = 0; i < nextCount; i++) {
if (cand == next[i]) {
alreadyIn = true;
break;
}
}
if (!alreadyIn) next[nextCount++] = cand;
}
}
pop = nextCount == popSize ? next : Arrays.copyOf(next, nextCount);
if (VERBOSE && (gen & 15) == 15) System.out.println(" gen " + gen + "/" + gens + " bestFitness=" + pop[0].fit());
}
if (pop.length == 0) return null;
var best = pop[0];
for (var i = 1; i < pop.length; i++) {
var x = pop[i];
if (x.fit() < best.fit()) best = x;
}
return best.grid;
}
//@formatter:off
@FunctionalInterface public interface SlotVisitor { void visit(int key, long lo, long hi); }
public record Slot(int key, long lo, long hi, DictEntry entry) {
static final int BIT_FOR_DIR = 3;
public static Slot from(int key, long lo, long hi, DictEntry entry) { return new Slot(key, lo, hi, entry); }
public static int length(long lo, long hi) { return bitCount(lo) + bitCount(hi); }
public static int clueIndex(int key) { return key >>> BIT_FOR_DIR; }
public static int dir(int key) { return key & 7; }
public static boolean horiz(int d) { return (d == 1) || (d == 3); }
public static int packSlotKey(int idx, int d) { return (idx << BIT_FOR_DIR) | d; }
}
}

View File

@@ -0,0 +1,125 @@
package puzzle;
import com.google.gson.Gson;
import lombok.experimental.UtilityClass;
import puzzle.SwedishGenerator.Lemma;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
@UtilityClass
public class Meta {
static final Gson GSON = new Gson();
static final int VERSION = 1;
static final int SHARD_MAGIC = 0x49445831; // "IDX1"
static final int MAP_MAGIC = 0x4D415031; // "MAP1"
static final ByteOrder ORDER = ByteOrder.BIG_ENDIAN;
static final Path dir = detectShardDir();
static final Path shardData = dir.resolve("shard0.data");
static final Path shardMap = dir.resolve("shard0.map");
private Path detectShardDir() {
// 1) optioneel override
String p = System.getProperty("puzzle.shards.dir");
if (p != null && !p.isBlank()) return Path.of(p).toAbsolutePath().normalize();
// 2) default: naast classes output (CLASS_OUTPUT/shards)
try {
var url = Meta.class.getProtectionDomain().getCodeSource().getLocation(); // classes dir (niet jar)
Path classes = Path.of(url.toURI());
return classes.resolve("shards");
} catch (Exception e) {
// 3) fallback: oude dev-locatie
return Path.of("").toAbsolutePath().normalize().resolve("src/main/resources/shards");
}
}
// --- Lookup: w -> i using mmap ---
private int findIndexInMapMmap(long target) throws IOException {
try (var ch = FileChannel.open(Meta.shardMap, StandardOpenOption.READ)) {
var mbb = (MappedByteBuffer) ch.map(FileChannel.MapMode.READ_ONLY, 0, ch.size()).order(ORDER);
var magic = mbb.getInt(0);
var ver = mbb.getInt(4);
var n = mbb.getInt(8);
if (magic != MAP_MAGIC || ver != VERSION) throw new IOException("Bad map file");
int lo = 0, hi = n - 1;
while (lo <= hi) {
var mid = (lo + hi) >>> 1;
var off = 12 + mid * 8;
var key = mbb.getLong(off);
if (key < target) lo = mid + 1;
else if (key > target) hi = mid - 1;
else return mid;
}
return -1;
}
}
// --- Read record i from shard.data (your format) ---
private ShardLem readRecord(long w, int i) throws IOException {
try (var ch = FileChannel.open(Meta.shardData, StandardOpenOption.READ)) {
var hdr = ByteBuffer.allocate(12).order(ORDER);
ch.read(hdr);
hdr.flip();
var magic = hdr.getInt();
var ver = hdr.getInt();
var n = hdr.getInt();
if (magic != SHARD_MAGIC || ver != VERSION) throw new IOException("Bad shard file");
if (i < 0 || i >= n) throw new IndexOutOfBoundsException();
var tableStart = 12L;
var dataStart = 12L + (long) n * 4L;
var offI = readIntAt(ch, tableStart + (long) i * 4L);
var offIp = (i + 1 < n)
? readIntAt(ch, tableStart + (long) (i + 1) * 4L)
: (int) (ch.size() - dataStart);
var len = offIp - offI;
var buf = ByteBuffer.allocate(len);
ch.position(dataStart + offI);
ch.read(buf);
buf.flip();
var s = StandardCharsets.UTF_8.decode(buf).toString();
var parts = s.split("\t", 3);
var simpel = Integer.parseInt(parts[1]);
var clues = GSON.fromJson(parts[2], String[].class);
return new ShardLem(w, simpel, i, clues);
}
}
private int readIntAt(FileChannel ch, long pos) throws IOException {
var b = ByteBuffer.allocate(4).order(ORDER);
ch.position(pos);
ch.read(b);
b.flip();
return b.getInt();
}
public record ShardLem(long w, int simpel, int mmap, String[] clues) { }
public ShardLem lookupSilent(long w) {
try {
var i = findIndexInMapMmap(Lemma.packLetterAndLengthBits(w));
if (i >= 0) {
return readRecord(w, i);
} else {
throw new RuntimeException("NOT FOUND{w=" + w + ", text=" + Lemma.asWord(w, new byte[8]) + "}");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,183 @@
package puzzle;
import lombok.AllArgsConstructor;
import lombok.experimental.Delegate;
import lombok.val;
import precomp.Mask;
import puzzle.Masker.Slot;
import puzzle.Meta.ShardLem;
import puzzle.SwedishGenerator.Lemma;
import puzzle.SwedishGenerator.Slotinfo;
import java.util.Arrays;
import java.util.function.IntSupplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static precomp.Const9x8.CLUE_DOWN0;
import static precomp.Const9x8.CLUE_LEFT3;
import static precomp.Const9x8.CLUE_LEFT_TOP4;
import static precomp.Const9x8.CLUE_NONE;
import static precomp.Const9x8.CLUE_RIGHT1;
import static precomp.Const9x8.CLUE_RIGHT_TOP5;
import static precomp.Const9x8.CLUE_UP2;
import static puzzle.Clues.createEmpty;
import static puzzle.Masker.isLo;
import static puzzle.SwedishGenerator.X;
public class Riddle {
public static IntStream cellWalk(int base, long lo, long hi) {
if (Slotinfo.increasing(base)) {
return IntStream.concat(IntStream.of(Slot.clueIndex(base)), IntStream.concat(
IntStream.generate(new IntSupplier() {
long temp = lo;
@Override
public int getAsInt() {
int res = Long.numberOfTrailingZeros(temp);
temp &= temp - 1;
return res;
}
}).limit(Long.bitCount(lo)),
IntStream.generate(new IntSupplier() {
long temp = hi;
@Override
public int getAsInt() {
int res = 64 | Long.numberOfTrailingZeros(temp);
temp &= temp - 1;
return res;
}
}).limit(Long.bitCount(hi))));
} else {
return IntStream.concat(IntStream.of(Slot.clueIndex(base)), IntStream.concat(
IntStream.generate(new IntSupplier() {
long temp = hi;
@Override
public int getAsInt() {
int msb = 63 - Long.numberOfLeadingZeros(temp);
temp &= ~(1L << msb);
return 64 | msb;
}
}).limit(Long.bitCount(hi)),
IntStream.generate(new IntSupplier() {
long temp = lo;
@Override
public int getAsInt() {
int msb = 63 - Long.numberOfLeadingZeros(temp);
temp &= ~(1L << msb);
return msb;
}
}).limit(Long.bitCount(lo))));
}
}
@AllArgsConstructor
enum Clue {
DOWN0(CLUE_DOWN0, 'B', 'b'),
RIGHT1(CLUE_RIGHT1, 'A', 'a'),
UP2(CLUE_UP2, 'C', 'c'),
LEFT3(CLUE_LEFT3, 'D', 'd'),
LEFT_TOP4(CLUE_LEFT_TOP4, 'E', 'e'),
RIGHT_TOP5(CLUE_RIGHT_TOP5, 'F', 'f'),
NONE(CLUE_NONE, '?', '?');
final byte dir;
final char slotChar, clueChar;
private static final Clue[] CLUES = new Clue[]{ DOWN0, RIGHT1, UP2, LEFT3, LEFT_TOP4, RIGHT_TOP5, NONE, NONE, NONE };
public static Clue from(int dir) { return CLUES[dir]; }
}
public record Vestigium(int index, int clue) {
public int cellIndex() {
return index * 33 + 27 + Slot.dir(clue);
}
}
public record ExportedPuzzle(String[] grid, WordOut[] words, int difficulty, Rewards rewards) { }
public record Rewards(int coins, int stars, int hints) { }
public record WordOut(String word, int[] cell, int startRow, int startCol, char direction, int arrowRow, int arrowCol, boolean isReversed, int complex, String[] clue) {
record ShaLemma(String word, @Delegate ShardLem rec) { }
private static ShaLemma lookup(long w, byte[] bytes) {
try {
val rec = Meta.lookupSilent(w);
if (Main.VERBOSE) System.out.println("\nQuery: w=" + w + " -> i=" + rec.mmap());
var word1 = Lemma.asWord(w, bytes);
if (Main.VERBOSE) System.out.println(" word=" + word1 + "\n" + " simpel=" + rec.simpel() + "\n" + " clues=" + Arrays.toString(rec.clues()));
return new ShaLemma(word1, rec);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
static long reverse(long w) {
int L = Lemma.unpackSize(w) + 1;
long letters = w & Lemma.LETTER_MASK;
long rev = 0;
for (int i = 0; i < L; i++) {
long letter = (letters >>> (5 * i)) & 31;
rev |= (letter << (5 * (L - 1 - i)));
}
return (w & ~Lemma.LETTER_MASK) | rev;
}
public WordOut(long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed, byte[] bytes) {
val meta = lookup(isReversed ? reverse(l) : l, bytes);
this(meta.word, new int[]{ arrowRow, arrowCol, startRow, startCol }, startRow, startCol, d, arrowRow, arrowCol, isReversed,
meta.simpel(), meta.clues());
}
}
@FunctionalInterface
public interface ClueSign {
byte replace(byte data);
}
public record Signa(@Delegate Clues c) {
public static Signa of(Mask... cells) { return new Signa(createEmpty()).setClue(cells); }
public Signa setClue(Mask... cells) {
for (var cell : cells) {
if (isLo((cell.index()))) setClueLo(cell.lo(), cell.d());
else setClueHi(cell.hi(), cell.d());
}
return this;
}
public Signa deepCopyGrid() { return new Signa(new Clues(c.lo, c.hi, c.vlo, c.vhi, c.rlo, c.rhi, c.xlo, c.xhi)); }
@Delegate public Stream<Vestigium> stream() {
val stream = Stream.<Vestigium>builder();
for (var l = c.lo & ~c.xlo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_RIGHT1));
for (var l = c.lo & ~c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_DOWN0));
for (var l = c.lo & ~c.xlo & c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_UP2));
for (var l = c.lo & ~c.xlo & c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_LEFT3));
for (var l = c.lo & c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_LEFT_TOP4));
for (var l = c.lo & c.xlo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_RIGHT_TOP5));
for (var h = c.hi & ~c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(Export.HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT1));
for (var h = c.hi & ~c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(Export.HI(Long.numberOfTrailingZeros(h)), CLUE_DOWN0));
for (var h = c.hi & ~c.xhi & c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(Export.HI(Long.numberOfTrailingZeros(h)), CLUE_UP2));
for (var h = c.hi & ~c.xhi & c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(Export.HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT3));
for (var h = c.hi & c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(Export.HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT_TOP4));
for (var h = c.hi & c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(Export.HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT_TOP5));
return stream.build();
}
}
public record Placed(long lemma, int slotKey, Mask[] cells) {
public static final char HORIZONTAL = 'h';
static final char VERTICAL = 'v';
static final char[] DIRECTION = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.VERTICAL };
public int arrowCol() { return cells[0].c(); }
public int arrowRow() { return cells[0].r(); }
public int startRow() { return cells[1].r(); }
public int startCol() { return cells[1].c(); }
public boolean isReversed() { return !Slotinfo.increasing(slotKey); }
public char direction() { return DIRECTION[Slot.dir(slotKey)]; }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
package puzzle;
import precomp.Neighbors9x8;
public class TestGen {
static void main() {
var cell = Neighbors9x8.IT[0];
long n1 = cell.n1();
long n2 = cell.n2();
int count = cell.nbrCount();
// of direct je offsets:
var up = Neighbors9x8.OFFSETS[1];
}
}

View File

@@ -1,876 +0,0 @@
package puzzle;
import org.w3c.dom.*;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.text.Normalizer;
import java.time.LocalDate;
import java.util.*;
public class ThemePoolBuilderLength {
private static final List<String> DEFAULT_FEEDS = List.of(
"https://feeds.nos.nl/nosnieuwsalgemeen",
"https://feeds.nos.nl/nosnieuwstech");
static final String url = "jdbc:postgresql://192.168.1.159:5432/postgres";
static final String user = "puzzle";
static final String pass = "heel-goed-wachtwoord";
// NOTE: normalizeDutchToken strips non A-Z. Keep entries 2-8 after normalization.
private static final List<String> DEFAULT_SHORTS = List.of(
"EU", "VS", "UK", "NAVO", "NOS", "NS", "ANP", "VN", "NPO", "RTL",
"UUR", "MIN", "TV", "GPS", "AI", "IT", "CPU", "GPU",
"ING", "KPN", "KVK", "RIVM", "GGD", "AIVD", "MIVD", "CEO", "CFO", "HR",
"NL", "BE", "BRU", "EUR", "EURO", "WET", "ART", "BTW", "DI", "MA",
"PVV", "VVD", "CDA", "FNV",
"EN", "IN", "OP", "OM", "TE", "ER", "DE", "HET", "EEN", "VAN", "MET", "NOG", "OOK", "MAAR", "WEL", "NIET",
"HOE", "ALS",
"ZO", "DO", "WO", "VR", "MO", "WA", "WE", "TAAL",
"LAND", "GEMEENTE", "STAAT", "BUREAU", "HUIS", "SCHOOL", "STR", "BAAN",
"WERK", "KLUS",
"FONDS", "RAAD", "CONGRESS", "GROEP", "STRAAT", "BRUG", "PARK",
"BUURT",
"BOUW", "HOTEL", "CAFE", "BAR",
"BIJBAAN", "STUDENT", "DOCENT",
"WINKEL", "MARKT", "KIOSK", "AUTO", "MOBILE", "FIETS", "SCOOTER",
// afkortingen
"DHR", "MEVR", "DR", "ST", "CA", "IVM", "MBT", "TAV", "TOV", "DWZ", "MAW", "OA", "TM",
"ANWB", "BRP", "CBS",
"AL", "NU", "TO", "NA", "BIJ", "TOT", "DAN", "WAT", "DAT",
"IK", "JE", "WE", "WIJ", "JIJ", "ZIJ", "HIJ", "HEN", "ONS", "JOU",
// romeinse cijfers (2-8)
"II", "III", "IV", "VI", "VII", "VIII", "IX",
"XI", "XII", "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XIX", "XX"
);
private static final String BROWSER_UA =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36";
static int MIN_SIMPLICITY = 520,
MAX_WORD_LENGTH = 7;
static final class Opts {
String endpoint = "https://jarvis-lan.appmodel.nl/api/ollama/";
List<String> feeds = new ArrayList<>(DEFAULT_FEEDS);
String outDir = System.getenv("OUT_DIR") != null ? System.getenv("OUT_DIR") : "/data/puzzle";
int bridgeN = 30000;
int themeN = 800;
int relatedN = 2200;
int rssItemsPerFeed = 10;
String model = "/models/Hadiseh-Mhd/Mixtral-8x7B-Instruct-v0.1-Q4_K_M-GGUF/mixtral-8x7b-instruct-v0.1.Q4_K_M.gguf";
int timeoutSeconds = 180;
int retries = 2;
int minLen2 = 1000;
int minLen3 = 1000;
int minLen4 = 1000;
int minLen5 = 1000; // set if you also want to force 5-letter words, etc.
int minLen6 = 1000;
int minLen7 = 1000;
int minLen8 = MAX_WORD_LENGTH >= 8 ? 1000 : 0;
}
public static void main(String[] args) throws Exception {
var o = parseArgs(args);
var outDir = Path.of(o.outDir);
Files.createDirectories(outDir);
System.out.println("Loading lexicon...");
Lexicon lex;
Class.forName("org.postgresql.Driver");
try (var c = DriverManager.getConnection(url, user, pass);) {
lex = loadLexicon(c);
}
System.out.println("Master words (2-" + MAX_WORD_LENGTH + ", A-Z): " + lex.words.size());
// RSS via curl (browser-like)
var all = new ArrayList<RssItem>();
for (var feed : o.feeds) {
var f = feed.trim();
if (f.isEmpty()) continue;
System.out.println("Fetching RSS: " + f);
all.addAll(fetchRssViaCurlBrowser(f, o.rssItemsPerFeed, o.timeoutSeconds));
}
var rssText = new StringBuilder();
var k = 0;
for (var it : all) {
k++;
rssText.append(k).append(". ").append(it.title).append("\n");
if (!it.desc.isBlank()) rssText.append(" ").append(it.desc).append("\n");
}
Files.writeString(outDir.resolve("rss.txt"), rssText.toString(), StandardCharsets.UTF_8);
// LM Studio via curl
var modelId = o.model;
if (modelId == null) {
var modelsUrl = apiUrl(o.endpoint, "/models");
System.out.println("Ollama GET: " + modelsUrl);
var modelsJson = curlGetJson(o, modelsUrl);
modelId = pickModelId(modelsJson);
if (modelId == null) {
throw new IOException("Could not auto-pick model id from /v1/models. Use --model <id>.\n--- /models ---\n" + modelsJson);
}
}
System.out.println("Using model: " + modelId);
System.out.println("Generating theme words via LM Studio...");
var llmWords = List.<String>of();//llmThemeWords(o, modelId, rssText.toString());
var themeKept = new LinkedHashSet<String>();
for (var wRaw : llmWords) {
var w = normalizeDutchToken(wRaw);
if (w == null) continue;
if (lex.idOf.containsKey(w)) themeKept.add(w);
}
Files.write(outDir.resolve("theme.txt"), themeKept, StandardCharsets.UTF_8);
// BitSets
var themeBs = bitmapFromWords(lex, themeKept);
var bridgeBs = buildBridgeBitmap(lex, o.bridgeN);
var shortBs = bitmapFromWords(lex, DEFAULT_SHORTS);
var pool = new BitSet(lex.words.size());
pool.or(themeBs);
pool.or(bridgeBs);
pool.or(shortBs);
// ---- NEW: enforce minimum counts per length ----
enforceMinima(o, lex, pool);
// Report
var themeCounts = countsPerLen(lex, themeBs);
var poolCounts = countsPerLen(lex, pool);
var report = """
Date: %s
Feeds: %s
Model: %s
Master size: %d
Theme kept (in master): %d
Bridge size: %d
Shorts kept: %d
Pool total: %d
Enforced minima:
2: %d
3: %d
4: %d
5: %d
6: %d
7: %d
8: %d
Counts per length (theme):
%s
Counts per length (pool):
%s
""".formatted(
LocalDate.now(),
String.join(", ", o.feeds),
modelId,
lex.words.size(),
themeBs.cardinality(),
bridgeBs.cardinality(),
shortBs.cardinality(),
pool.cardinality(),
o.minLen2, o.minLen3, o.minLen4, o.minLen5, o.minLen6, o.minLen7, o.minLen8,
mapToLines(themeCounts),
mapToLines(poolCounts)
);
Files.writeString(outDir.resolve("report.txt"), report, StandardCharsets.UTF_8);
System.out.println(report);
// Output pool list
var poolFile = outDir.resolve("pool.txt");
writeWordList(poolFile, lex, pool);
System.out.println("Wrote: " + poolFile.toAbsolutePath());
}
static Opts parseArgs(String[] args) {
var o = new Opts();
for (var i = 0; i < args.length; i++) {
var a = args[i];
var v = (i + 1 < args.length) ? args[i + 1] : null;
switch (a) {
case "--endpoint" -> {
o.endpoint = v;
i++;
}
case "--feeds" -> {
o.feeds = Arrays.asList(v.split(","));
i++;
}
case "--out" -> {
o.outDir = v;
i++;
}
case "--bridge" -> {
o.bridgeN = Integer.parseInt(v);
i++;
}
case "--theme" -> {
o.themeN = Integer.parseInt(v);
i++;
}
case "--related" -> {
o.relatedN = Integer.parseInt(v);
i++;
}
case "--items" -> {
o.rssItemsPerFeed = Integer.parseInt(v);
i++;
}
case "--model" -> {
o.model = v;
i++;
}
case "--timeout" -> {
o.timeoutSeconds = Integer.parseInt(v);
i++;
}
case "--retries" -> {
o.retries = Integer.parseInt(v);
i++;
}
// ---- NEW: minima per length ----
case "--min2" -> {
o.minLen2 = Integer.parseInt(v);
i++;
}
case "--min3" -> {
o.minLen3 = Integer.parseInt(v);
i++;
}
case "--min4" -> {
o.minLen4 = Integer.parseInt(v);
i++;
}
case "--min5" -> {
o.minLen5 = Integer.parseInt(v);
i++;
}
case "--min6" -> {
o.minLen6 = Integer.parseInt(v);
i++;
}
case "--min7" -> {
o.minLen7 = Integer.parseInt(v);
i++;
}
case "--min8" -> {
o.minLen8 = Integer.parseInt(v);
i++;
}
case "-h", "--help" -> {
System.out.println("""
Usage:
java puzzle.ThemePoolBuilder --words WORDS.txt [options]
Options:
--endpoint http://HOST:1234/v1 (LM Studio)
--feeds url1,url2
--out ./out
--bridge 5000
--theme 300
--related 1200
--items 20 (per feed)
--model <id> (recommended; skips /v1/models)
--timeout 60 (seconds)
--retries 4
# enforce minima per length in final pool
--min2 4000
--min3 7000
--min4 9000
--min5 0
--min6 0
--min7 0
--min8 0
""");
System.exit(0);
}
default -> throw new IllegalArgumentException("Unknown arg: " + a);
}
}
return o;
}
static boolean isAZ(String s) {
for (var i = 0; i < s.length(); i++) {
var ch = s.charAt(i);
if (ch < 'A' || ch > 'Z') return false;
}
return true;
}
static String normalizeDutchToken(String raw) {
if (raw == null) return null;
var s = raw.trim();
if (s.isEmpty()) return null;
s = Normalizer.normalize(s, Normalizer.Form.NFD).replaceAll("\\p{M}+", "");
s = s.toUpperCase(Locale.ROOT);
s = s.replaceAll("[^A-Z]", "");
if (s.length() < 2 || s.length() > 8) return null;
if (!isAZ(s)) return null;
return s;
}
static String stripHtml(String s) {
if (s == null) return "";
var x = s.replaceAll("<[^>]+>", " ");
x = x.replace("&amp;", "&").replace("&lt;", "<").replace("&gt;", ">");
x = x.replaceAll("\\s+", " ").trim();
return x;
}
/**
* @param words id -> word
* @param idOf word -> id
* @param score id -> crossability
* @param byLen byLen[L] for L 0..8
*/
record Lexicon(List<String> words, Map<String, Integer> idOf, int[] score, BitSet[] byLen) { }
/**
* Loads lexicon from PostgreSQL view/table: export_words_with_hints_2_8
* Columns: WOORD, level_1_to_10, hint
*
* Notes:
* - Normalizes words via normalizeDutchToken(...)
* - Dedupes on normalized word
* - Uses level_1_to_10 as the "LLM score" (fallback 5)
* - Ignores hint for scoring (but you can store it elsewhere if needed)
*/
static Lexicon loadLexicon(Connection conn) throws SQLException {
var out = new ArrayList<String>(200_000);
var idOf = new HashMap<String, Integer>(400_000);
// Store level per normalized word while loading so we can compute scores later
var levelOf = new HashMap<String, Integer>(400_000);
final var sql = """
SELECT woord, 10-level_1_to_10, hint
FROM export_real_words_with_hints
where length(woord)<=7
order by level_1_to_10 asc
""" ;
try (var ps = conn.prepareStatement(sql);
var rs = ps.executeQuery()) {
while (rs.next()) {
var rawWord = rs.getString(1);
var lvlObj = (Integer) rs.getObject(2); // nullable
// String hint = rs.getString(3); // available if you want it later
var w = normalizeDutchToken(rawWord);
if (w == null) continue;
if (idOf.containsKey(w)) continue;
idOf.put(w, out.size());
out.add(w);
var lvl = (lvlObj == null ? 5 : lvlObj.intValue());
levelOf.put(w, lvl);
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
var n = out.size();
var score = new int[n];
var byLen = new BitSet[9];
for (var L = 0; L <= 8; L++) byLen[L] = new BitSet(n);
for (var i = 0; i < n; i++) {
var w = out.get(i);
var crossScore = HintScores.crossabilityScore(w);
var lScore = levelOf.getOrDefault(w, 5);
// Prioritize simple words (high lScore) and long words.
// lScore (1-10) adds up to 1000 points (weight 100).
// Length (2-8) adds up to 160 points (weight 20).
score[i] = crossScore + (lScore * 100) + (w.length() * 40);
byLen[w.length()].set(i);
}
return new Lexicon(out, idOf, score, byLen);
}
// ---------------- RSS via curl (browser-like) ----------------
record RssItem(String title, String desc) { }
static String textOfFirst(Element parent, String tag) {
var nl = parent.getElementsByTagName(tag);
if (nl.getLength() == 0) return null;
var n = nl.item(0);
return n.getTextContent();
}
static List<RssItem> fetchRssViaCurlBrowser(String url, int limit, int timeoutSeconds) throws Exception {
var cmd = new ArrayList<String>();
cmd.add("curl");
cmd.add("-fsSL");
cmd.add("-L");
cmd.add("--compressed");
cmd.add("--connect-timeout");
cmd.add("10");
cmd.add("--max-time");
cmd.add(String.valueOf(timeoutSeconds));
cmd.add("--retry");
cmd.add("5");
cmd.add("--retry-all-errors");
cmd.add("--retry-delay");
cmd.add("1");
cmd.add("-H");
cmd.add("User-Agent: " + BROWSER_UA);
cmd.add("-H");
cmd.add("Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
cmd.add("-H");
cmd.add("Accept-Language: nl-NL,nl;q=0.9,en;q=0.7");
cmd.add("-H");
cmd.add("Cache-Control: no-cache");
cmd.add("-H");
cmd.add("Pragma: no-cache");
cmd.add("-H");
cmd.add("Sec-Fetch-Dest: document");
cmd.add("-H");
cmd.add("Sec-Fetch-Mode: navigate");
cmd.add("-H");
cmd.add("Sec-Fetch-Site: none");
cmd.add("-H");
cmd.add("Sec-Fetch-User: ?1");
cmd.add(url);
var p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
var bytes = p.getInputStream().readAllBytes();
var code = p.waitFor();
if (code != 0) {
throw new IOException("curl RSS failed (" + code + ") url=" + url + " output=" +
new String(bytes, StandardCharsets.UTF_8));
}
try (InputStream is = new ByteArrayInputStream(bytes)) {
var dbf = DocumentBuilderFactory.newInstance();
var doc = dbf.newDocumentBuilder().parse(is);
var items = doc.getElementsByTagName("item");
var out = new ArrayList<RssItem>();
for (var i = 0; i < items.getLength() && out.size() < limit; i++) {
var item = (Element) items.item(i);
var title = textOfFirst(item, "title");
var desc = textOfFirst(item, "description");
if (title == null) title = "";
if (desc == null) desc = "";
out.add(new RssItem(stripHtml(title), stripHtml(desc)));
}
return out;
}
}
// ---------------- LM Studio (OpenAI-compatible) ----------------
static String apiUrl(String endpointArg, String path) {
var base = endpointArg.trim();
if (base.endsWith("/")) base = base.substring(0, base.length() - 1);
if (base.endsWith("/v1")) base = base.substring(0, base.length() - 3);
if (!path.startsWith("/")) path = "/" + path;
if (!path.startsWith("/v1/")) path = "/" + path;
return base + path;
}
static void sleepBackoff(int attempt) {
try {
var ms = (long) (300L * Math.pow(2, attempt - 1)); // 300, 600, 1200, ...
Thread.sleep(Math.min(ms, 3000));
} catch (InterruptedException ignored) { }
}
static String curlGetJson(Opts o, String url) throws Exception {
Exception last = null;
for (var attempt = 1; attempt <= o.retries; attempt++) {
try {
var cmd = new ArrayList<String>();
cmd.add("curl");
cmd.add("-fsSL");
cmd.add("--connect-timeout");
cmd.add("10");
cmd.add("--max-time");
cmd.add(String.valueOf(o.timeoutSeconds));
cmd.add("--retry");
cmd.add("3");
cmd.add("--retry-all-errors");
cmd.add("--retry-delay");
cmd.add("1");
cmd.add("-H");
cmd.add("Accept: application/json");
cmd.add("-H");
cmd.add("User-Agent: " + BROWSER_UA);
cmd.add(url);
var p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
var bytes = p.getInputStream().readAllBytes();
var code = p.waitFor();
if (code != 0) {
throw new IOException("curl GET failed (" + code + ") url=" + url + "\nOutput:\n" +
new String(bytes, StandardCharsets.UTF_8));
}
return new String(bytes, StandardCharsets.UTF_8);
} catch (Exception e) {
last = e;
if (attempt < o.retries) sleepBackoff(attempt);
}
}
throw last;
}
static String curlPostJson(Opts o, String url, String jsonBody) throws Exception {
Exception last = null;
for (var attempt = 1; attempt <= o.retries; attempt++) {
try {
System.out.println(" Attempt " + attempt + "/" + o.retries + " via curl...");
var tempFile = Files.createTempFile("lm-request-", ".json");
try {
Files.writeString(tempFile, jsonBody, StandardCharsets.UTF_8);
List<String> cmd = new ArrayList<>();
cmd.add("curl");
cmd.add("-fsSL");
cmd.add("--connect-timeout");
cmd.add("10");
cmd.add("--max-time");
cmd.add(String.valueOf(o.timeoutSeconds));
cmd.add("--retry");
cmd.add("3");
cmd.add("--retry-all-errors");
cmd.add("--retry-delay");
cmd.add("1");
cmd.add("-H");
cmd.add("Content-Type: application/json");
cmd.add("-H");
cmd.add("Accept: application/json");
cmd.add("-H");
cmd.add("User-Agent: " + BROWSER_UA);
cmd.add("-d");
cmd.add("@" + tempFile.toString());
cmd.add(url);
var p = new ProcessBuilder(cmd)
.redirectErrorStream(true)
.start();
var bytes = p.getInputStream().readAllBytes();
var code = p.waitFor();
if (code != 0) {
throw new IOException("curl POST failed (" + code + ") url=" + url + "\nOutput:\n" +
new String(bytes, StandardCharsets.UTF_8));
}
return new String(bytes, StandardCharsets.UTF_8);
} finally {
Files.deleteIfExists(tempFile);
}
} catch (Exception e) {
System.err.println(" Error: " + e.getClass().getName() + ": " + e.getMessage());
last = e;
if (attempt < o.retries) sleepBackoff(attempt);
}
}
throw last;
}
static String pickModelId(String modelsJson) {
if (modelsJson == null) return null;
var data = modelsJson.indexOf("\"data\"");
if (data < 0) return null;
var id = modelsJson.indexOf("\"id\"", data);
if (id < 0) return null;
var q1 = modelsJson.indexOf('"', modelsJson.indexOf(':', id) + 1);
if (q1 < 0) return null;
var q2 = modelsJson.indexOf('"', q1 + 1);
if (q2 < 0) return null;
return modelsJson.substring(q1 + 1, q2);
}
static String extractChatContent(String json) {
if (json == null) return null;
var choices = json.indexOf("\"choices\"");
var p = (choices >= 0) ? choices : 0;
var i = json.indexOf("\"content\"", p);
if (i < 0) return null;
var colon = json.indexOf(':', i);
if (colon < 0) return null;
var q = json.indexOf('"', colon + 1);
if (q < 0) return null;
var sb = new StringBuilder();
var esc = false;
for (var k = q + 1; k < json.length(); k++) {
var ch = json.charAt(k);
if (esc) {
if (ch == 'n') sb.append('\n');
else if (ch == 't') sb.append('\t');
else if (ch == 'r') sb.append('\r');
else sb.append(ch);
esc = false;
} else {
if (ch == '\\') esc = true;
else if (ch == '"') break;
else sb.append(ch);
}
}
return sb.toString();
}
static List<String> parseStringArray(String s) {
if (s == null) return List.of();
var a = s.indexOf('[');
var b = s.lastIndexOf(']');
if (a < 0 || b < 0 || b <= a) return List.of();
var body = s.substring(a + 1, b);
var out = new ArrayList<String>();
// If it's a simple comma-separated list without quotes (or with mixed quotes),
// let's try a more robust approach.
if (!body.contains("\"")) {
for (var part : body.split(",")) {
var trimmed = part.trim();
if (!trimmed.isEmpty()) out.add(trimmed);
}
if (!out.isEmpty()) return out;
}
var cur = new StringBuilder();
boolean in = false, esc = false;
for (var i = 0; i < body.length(); i++) {
var ch = body.charAt(i);
if (!in) {
if (ch == '"') {
in = true;
cur.setLength(0);
esc = false;
}
} else {
if (esc) {
cur.append(ch);
esc = false;
} else if (ch == '\\') {
esc = true;
} else if (ch == '"') {
out.add(cur.toString());
in = false;
} else {
cur.append(ch);
}
}
}
return out;
}
static String jsonQuote(String s) {
if (s == null) return "null";
var sb = new StringBuilder();
sb.append('"');
for (var i = 0; i < s.length(); i++) {
var ch = s.charAt(i);
if (ch == '\\' || ch == '"') sb.append('\\').append(ch);
else if (ch == '\n') sb.append("\\n");
else if (ch == '\r') sb.append("\\r");
else if (ch == '\t') sb.append("\\t");
else sb.append(ch);
}
sb.append('"');
return sb.toString();
}
static List<String> llmThemeWords(Opts o, String modelId, String rssText) throws Exception {
var prompt = """
Je genereert woorden voor een Nederlandse kruiswoordpuzzel.
Regels:
- Output MOET exact één JSON array zijn: ["WOORD", ...]
- Alleen A-Z, 2-8 letters woorden
- Geen spaties, streepjes, cijfers, accenten, apostrofs, punten
- Geen duplicaten
- Focus op zelfstandige naamwoorden/termen uit het nieuws en relevante Zweedse kruiswoordpuzzel koppelwoorden in het thema.
- Lever %d THEMA-woorden en daarna %d GERELATEERDE woorden (totaal %d).
- Voeg ook wat korte woorden/afkortingen toe (2-4 letters), maar houd het totaal gelijk.
Nieuws (koppen/samenvattingen):
%s
""".formatted(o.themeN, o.relatedN, (o.themeN + o.relatedN), rssText.substring(0, Math.min(rssText.length(), 8000)));
var body = """
{
"model": %s,
"messages": [
{"role":"system","content":"Je bent een strikte JSON generator. Antwoord ALLEEN met een JSON array van strings."},
{"role":"user","content": %s}
],
"temperature": 0.35,
"max_tokens": 20000
}
""".formatted(jsonQuote(modelId), jsonQuote(prompt));
var url = apiUrl(o.endpoint, "/chat/completions");
System.out.println("LM Studio POST: " + url);
System.out.println("Request body length: " + body.length() + " bytes");
var resp = curlPostJson(o, url, body);
var content = extractChatContent(resp);
if (content == null) {
throw new IOException("Could not extract chat content from LM Studio response.\n--- response ---\n" + resp);
}
return parseStringArray(content);
}
// ---------------- Pool building ----------------
static BitSet buildBridgeBitmap(Lexicon lex, int bridgeN) {
var n = lex.words.size();
var ids = new ArrayList<Integer>(n);
for (var i = 0; i < n; i++) {
// Optionally filter out VERY complex words from the bridge (e.g. lScore < 3)
// But since we sort by score (which is now dominated by lScore),
// they will be at the very bottom anyway.
// if (lex.score[i] < 800) continue;
ids.add(i);
}
ids.sort((a, b) -> Integer.compare(lex.score[b], lex.score[a]));
var bs = new BitSet(n);
var take = Math.min(bridgeN, ids.size());
for (var i = 0; i < take; i++) bs.set(ids.get(i));
return bs;
}
static BitSet bitmapFromWords(Lexicon lex, Collection<String> words) {
var bs = new BitSet(lex.words.size());
for (var raw : words) {
var w = normalizeDutchToken(raw);
if (w == null) continue;
var id = lex.idOf.get(w);
if (id != null) bs.set(id);
}
return bs;
}
static Map<Integer, Integer> countsPerLen(Lexicon lex, BitSet bs) {
var out = new HashMap<Integer, Integer>();
for (var L = 2; L <= 8; L++) {
var tmp = (BitSet) bs.clone();
tmp.and(lex.byLen[L]);
out.put(L, tmp.cardinality());
}
return out;
}
static void writeWordList(Path path, Lexicon lex, BitSet bs) throws IOException {
var ids = new ArrayList<Integer>(bs.cardinality());
for (var i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i + 1)) {
ids.add(i);
}
// Sort by score descending (higher score is easier/better)
ids.sort((a, b) -> Integer.compare(lex.score[b], lex.score[a]));
var out = new ArrayList<String>(ids.size());
for (var id : ids) {
if (lex.score[id] < MIN_SIMPLICITY)
continue;
out.add(lex.words.get(id));
}
Files.write(path, out, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
static String mapToLines(Map<Integer, Integer> m) {
var sb = new StringBuilder();
for (var L = 2; L <= 8; L++) {
sb.append(" ").append(L).append(": ").append(m.getOrDefault(L, 0)).append("\n");
}
return sb.toString();
}
// ---------------- NEW: enforce minima per length ----------------
static int countLen(Lexicon lex, BitSet bs, int L) {
var tmp = (BitSet) bs.clone();
tmp.and(lex.byLen[L]);
return tmp.cardinality();
}
static void ensureMinLen(Lexicon lex, BitSet pool, int L, int minWanted) {
if (minWanted <= 0) return;
var current = countLen(lex, pool, L);
if (current >= minWanted) return;
var need = minWanted - current;
// Collect candidate ids of exactly length L that are not already in pool.
var candidates = new ArrayList<Integer>(Math.max(need * 2, 1024));
for (var id = lex.byLen[L].nextSetBit(0); id >= 0; id = lex.byLen[L].nextSetBit(id + 1)) {
if (!pool.get(id)) candidates.add(id);
}
if (candidates.isEmpty()) return;
// Sort by crossability score (desc)
candidates.sort((a, b) -> Integer.compare(lex.score[b], lex.score[a]));
var added = 0;
for (var id : candidates) {
pool.set(id);
added++;
if (added >= need) break;
}
}
static void enforceMinima(Opts o, Lexicon lex, BitSet pool) {
ensureMinLen(lex, pool, 2, o.minLen2);
ensureMinLen(lex, pool, 3, o.minLen3);
ensureMinLen(lex, pool, 4, o.minLen4);
ensureMinLen(lex, pool, 5, o.minLen5);
ensureMinLen(lex, pool, 6, o.minLen6);
ensureMinLen(lex, pool, 7, o.minLen7);
ensureMinLen(lex, pool, 8, o.minLen8);
}
}

View File

@@ -1,6 +0,0 @@
// file: app/Trigger.java
package puzzle;
import gen.GenerateNeighbors;
@GenerateNeighbors(C = 9, R = 8, packageName = "precomp", className = "Neighbors9x8")
public final class Trigger { }

View File

@@ -0,0 +1,3 @@
package puzzle;
public record rci(int r, int c, int i, long n1, long n2, int nbrCount, long n8_1, long n8_2, double cross_r, double cross_c) {}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,106 @@
package puzzle;
import module java.base;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.val;
import org.junit.jupiter.api.Test;
import precomp.Neighbors9x8;
import puzzle.SwedishGenerator.Dict;
import puzzle.SwedishGenerator.DictEntry;
import puzzle.SwedishGenerator.Lemma;
import puzzle.dict950.DictData950;
public final class DictJavaGeneratorMulti {
@Test
void testReversed() {
val wOrig = DictData950.DICT950.index()[4].words()[4];
val wRev = DictData950.DICT950.reversed()[4].words()[4];
System.out.println(Lemma.asWord(wOrig, new byte[8]) + " " + Lemma.asWord(wRev, new byte[8]));
}
interface Dicts {
static Dict makeDict(long[] wordz) {
var index = new DictEntryDTO[Neighbors9x8.MAX_WORD_LENGTH_PLUS_ONE];
Arrays.setAll(index, DictEntryDTO::new);
for (var lemma : wordz) {
var L = Lemma.unpackSize(lemma) + 1;//Lemma.unpackSize(lemma) + 2;
val entry = index[L];
val idx = entry.words().size();
val pos = entry.pos();
entry.words().add(lemma);
var i = 0;
for (var w = lemma & Lemma.LETTER_MASK; w != 0; w >>>= 5, i++) {
pos[i][(int) ((w & 31) - 1)].add(idx);
}
}
for (var i = 2; i < index.length; i++) if (index[i].words().size() <= 0) throw new RuntimeException("No words for length " + i);
return new Dict(Arrays.stream(index).map(i -> {
var words = i.words().toArray();
var numWords = words.length;
var numLongs = (numWords + 63) >>> 6;
var bitsets = new long[i.pos().length * 26][numLongs];
for (var p = 0; p < i.pos().length; p++) {
for (var l = 0; l < 26; l++) {
var list = i.pos()[p][l];
var bs = bitsets[p * 26 + l];
for (var k = 0; k < list.size(); k++) {
var wordIdx = list.data()[k];
bs[wordIdx >>> 6] |= (1L << (wordIdx & 63));
}
}
}
return new DictEntry(words, bitsets, words.length, (words.length + 63) >>> 6);
}).toArray(DictEntry[]::new),
Arrays.stream(index).mapToInt(i -> i.words().size()).sum());
}
}
record DictEntryDTO(LongArrayList words, IntListDTO[][] pos) {
public DictEntryDTO(int L) {
this(new LongArrayList(1024), new IntListDTO[L][26]);
for (var i = 0; i < L; i++) for (var j = 0; j < 26; j++) pos[i][j] = new IntListDTO();
}
@Getter
@Accessors(fluent = true)
@NoArgsConstructor
static final class IntListDTO {
int[] data = new int[8];
int size = 0;
void add(int v) {
if (size >= data.length) data = Arrays.copyOf(data, data.length * 2);
data[size++] = v;
}
}
}
static final class LongArrayList {
long[] a;
int size;
LongArrayList(int initialCapacity) {
if (initialCapacity < 0) throw new IllegalArgumentException();
a = new long[initialCapacity];
}
int size() { return size; }
void add(long v) {
if (size == a.length) grow();
a[size++] = v;
}
void grow() {
var newCap = a.length == 0 ? 1 : a.length * 2;
var n = new long[newCap];
System.arraycopy(a, 0, n, 0, size);
a = n;
}
long[] toArray() { return Arrays.copyOf(a, this.size); }
}
}

View File

@@ -1,111 +0,0 @@
package puzzle;
import org.junit.jupiter.api.Test;
import puzzle.Export.Gridded;
import puzzle.Export.Placed;
import puzzle.Export.Rewards;
import puzzle.Export.PuzzleResult;
import puzzle.SwedishGenerator.FillResult;
import puzzle.SwedishGenerator.Grid;
import puzzle.SwedishGenerator.Lemma;
import puzzle.SwedishGenerator.Rng;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.*;
import static puzzle.SwedishGenerator.*;
public class ExportFormatTest {
@Test
void testExportFormatFromFilled() {
var swe = new SwedishGenerator(new Rng(0));
var grid = Grid.createEmpty();
// Place a '2' (right) at (0,0)
grid.setClue(0, (byte) '2');
// This creates a slot starting at (0,1)
var clueMap = new HashMap<Integer, Long>();
// key = (cellIndex << 4) | direction
var key = (0 << 4) | 2;
var lemma = new Lemma("TEST");
clueMap.put(key, lemma.word());
// Manually fill the grid letters for "TEST" at (0,1), (0,2), (0,3), (0,4)
grid.setByteAt(Grid.offset(0, 1), (byte) 'T');
grid.setByteAt(Grid.offset(0, 2), (byte) 'E');
grid.setByteAt(Grid.offset(0, 3), (byte) 'S');
grid.setByteAt(Grid.offset(0, 4), (byte) 'T');
// Terminate thGrid.offset(e slot at) (0,5) with another digit to avoid it extending to MAX_WORD_LENGTH
grid.setClue(Grid.offset(0, 5), (byte) '1');
var fillResult = new FillResult(true, new Gridded(grid), clueMap, new FillStats());
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
var rewards = new Rewards(10, 5, 1);
var exported = puzzleResult.exportFormatFromFilled(2, rewards);
assertNotNull(exported);
assertEquals(2, exported.difficulty());
assertEquals(rewards, exported.rewards());
// Check words
assertEquals(1, exported.words().length);
var w = exported.words()[0];
assertEquals("TEST", w.word());
assertEquals(Placed.HORIZONTAL, w.direction());
// The bounding box should include (0,0) for the arrow and (0,1)-(0,4) for the word.
// minR=0, maxR=0, minC=0, maxC=4
// startRow = 0 - minR = 0
// startCol = 1 - minC = 1
assertEquals(0, w.startRow());
assertEquals(1, w.startCol());
assertEquals(0, w.arrowRow());
assertEquals(0, w.arrowCol());
// Check gridv2
// It should be 1 row, containing "2TEST" -> but letters are mapped, digits are not explicitly in letterAt.
// Wait, look at exportFormatFromFilled logic:
// row.append(letterAt.getOrDefault(pack(r, c), '#'));
// letterAt only contains letters from placed words.
// arrow cells are NOT in letterAt unless they are also part of a word (unlikely).
// So (0,0) should be '#'
assertEquals(1, exported.gridv2().length);
assertEquals("#TEST", exported.gridv2()[0]);
}
@Test
void testExportFormatEmpty() {
var swe = new SwedishGenerator(new Rng(0));
var grid = Grid.createEmpty();
var fillResult = new FillResult(true, new Gridded(grid), new HashMap<>(), new FillStats());
var puzzleResult = new PuzzleResult(swe, null, null, fillResult);
var exported = puzzleResult.exportFormatFromFilled(1, new Rewards(0, 0, 0));
assertNotNull(exported);
assertEquals(0, exported.words().length);
// Should return full grid with '#'
assertEquals(R, exported.gridv2().length);
for (var row : exported.gridv2()) {
assertEquals(C, row.length());
assertTrue(row.matches("#+"));
}
}
@Test
void testIndex() {
var csv = Paths.get("nl_score_hints_v3.csv");
var idx = Paths.get("nl_score_hints_v3.idx");
try (var svc = new CsvIndexService(csv, idx)) {
System.out.println(svc.getLine(1319));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,63 @@
package puzzle;
import puzzle.SwedishGenerator.Grid;
import puzzle.SwedishGenerator.Lemma;
import puzzle.SwedishGenerator.Slotinfo;
import static java.lang.Long.bitCount;
import static java.lang.Long.numberOfTrailingZeros;
public class GridBuilder {
public static boolean placeWord(final Grid grid, final byte[] g, final int key, final long lo, final long hi, final long w) {
final long glo = grid.lo, ghi = grid.hi;
if (Slotinfo.increasing(key)) {
for (long b = lo & glo; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
if (g[idx] != Lemma.byteAt(w, bitCount(lo & ((1L << idx) - 1)))) return false;
}
int bcLo = bitCount(lo);
for (long b = hi & ghi; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
if (g[64 | idx] != Lemma.byteAt(w, bcLo + bitCount(hi & ((1L << idx) - 1)))) return false;
}
long maskLo = lo & ~glo, maskHi = hi & ~ghi;
if ((maskLo | maskHi) != SwedishGenerator.X) {
for (long b = maskLo; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
g[idx] = Lemma.byteAt(w, bitCount(lo & ((1L << idx) - 1)));
}
for (long b = maskHi; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
g[64 | idx] = Lemma.byteAt(w, bcLo + bitCount(hi & ((1L << idx) - 1)));
}
grid.lo |= maskLo;
grid.hi |= maskHi;
}
} else {
int bcHi = bitCount(hi);
for (long b = hi & ghi; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
if (g[64 | idx] != Lemma.byteAt(w, bitCount(hi & ~((1L << idx) | ((1L << idx) - 1))))) return false;
}
for (long b = lo & glo; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
if (g[idx] != Lemma.byteAt(w, bcHi + bitCount(lo & ~((1L << idx) | ((1L << idx) - 1))))) return false;
}
long maskLo = lo & ~glo, maskHi = hi & ~ghi;
if ((maskLo | maskHi) != SwedishGenerator.X) {
for (long b = maskHi; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
g[64 | idx] = Lemma.byteAt(w, bitCount(hi & ~((1L << idx) | ((1L << idx) - 1))));
}
for (long b = maskLo; b != SwedishGenerator.X; b &= b - 1) {
int idx = numberOfTrailingZeros(b);
g[idx] = Lemma.byteAt(w, bcHi + bitCount(lo & ~((1L << idx) | ((1L << idx) - 1))));
}
grid.lo |= maskLo;
grid.hi |= maskHi;
}
}
return true;
}
}

View File

@@ -1,184 +1,271 @@
package puzzle;
import module java.base;
import anno.DictGen;
import lombok.val;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import precomp.Mask;
import puzzle.Export.Puzzle;
import puzzle.Export.PuzzleResult;
import puzzle.Main.Opts;
import puzzle.Riddle.Rewards;
import puzzle.SwedishGenerator.Rng;
import puzzle.SwedishGenerator.Slot;
import java.util.concurrent.atomic.AtomicInteger;
import puzzle.dict950.DictData950;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static puzzle.SwedishGenerator.*;
import static puzzle.SwedishGenerator.DASH;
import static precomp.Const9x8.Cell.r0c0d0;
import static precomp.Const9x8.Cell.r0c0d1;
import static precomp.Const9x8.Cell.r0c1;
import static precomp.Const9x8.Cell.r0c1d2;
import static precomp.Const9x8.Cell.r0c3d0;
import static precomp.Const9x8.Cell.r0c3d3;
import static precomp.Const9x8.Cell.r0c4d0;
import static precomp.Const9x8.Cell.r0c5d0;
import static precomp.Const9x8.Cell.r0c6d0;
import static precomp.Const9x8.Cell.r0c6d3;
import static precomp.Const9x8.Cell.r0c7d0;
import static precomp.Const9x8.Cell.r0c8d0;
import static precomp.Const9x8.Cell.r1c0d1;
import static precomp.Const9x8.Cell.r1c0d3;
import static precomp.Const9x8.Cell.r1c1;
import static precomp.Const9x8.Cell.r1c1d0;
import static precomp.Const9x8.Cell.r1c1d1;
import static precomp.Const9x8.Cell.r1c1d3;
import static precomp.Const9x8.Cell.r2c0d1;
import static precomp.Const9x8.Cell.r2c1d1;
import static precomp.Const9x8.Cell.r2c1d2;
import static precomp.Const9x8.Cell.r3c0d3;
import static precomp.Const9x8.Cell.r3c3d3;
import static precomp.Const9x8.Cell.r3c6d0;
import static precomp.Const9x8.Cell.r4c0d3;
import static precomp.Const9x8.Cell.r4c2d3;
import static precomp.Const9x8.Cell.r4c3d0;
import static precomp.Const9x8.Cell.r4c3d1;
import static precomp.Const9x8.Cell.r4c6d3;
import static precomp.Const9x8.Cell.r5c0d3;
import static precomp.Const9x8.Cell.r5c1d1;
import static precomp.Const9x8.Cell.r6c0d3;
import static precomp.Const9x8.Cell.r6c1d1;
import static precomp.Const9x8.Cell.r6c8d2;
import static precomp.Const9x8.Cell.r7c0d2;
import static precomp.Const9x8.Cell.r7c1d1;
import static precomp.Const9x8.Cell.r7c1d2;
import static precomp.Const9x8.Cell.r7c2d2;
import static precomp.Const9x8.Cell.r7c4d2;
import static precomp.Const9x8.Cell.r7c5d2;
import static precomp.Const9x8.Cell.r7c8d3;
import static precomp.Const9x8.LETTER_A;
import static precomp.Const9x8.LETTER_Z;
import static precomp.Const9x8.OFF_0_0;
import static precomp.Const9x8.OFF_0_1;
import static precomp.Const9x8.OFF_0_2;
import static precomp.Const9x8.OFF_1_1;
import static precomp.Const9x8.OFF_1_2;
import static precomp.Const9x8.OFF_2_1;
import static precomp.Const9x8.OFF_2_3;
import static puzzle.LemmaData.AB;
import static puzzle.LemmaData.AZ;
import static puzzle.Riddle.Clue.DOWN0;
import static puzzle.Riddle.Clue.RIGHT1;
import static puzzle.Riddle.Clue.UP2;
import static puzzle.SwedishGenerator.Lemma;
import static puzzle.SwedishGenerator.Slotinfo;
import static puzzle.SwedishGenerator.fillMask;
@DictGen(
packageName = "puzzle.dict950",
className = "DictData950",
scv = "/home/mike/dev/puzzle-generator/nl_score_hints_v4.csv",
simpleMax = 950,
minLen = 2,
maxLen = 8
)
public class MainTest {
static final Opts opts = new Main.Opts() {{
this.seed = 12348;
this.clueSize = 4;
this.pop = 4; // Tiny population
this.offspring = 18;
this.gens = 20; // Very few generations
this.minSimplicity = 0;
this.threads = 1;
this.tries = 1;
this.verbose = false;
}};
@Test
void testExtractSlots() {
var grid = Grid.createEmpty();
var clues = Riddle.Signa.of(r0c0d1);
var grid = new Puzzle(clues);
val g = grid.grid().g;
GridBuilder.placeWord(grid.grid(), g, r0c0d1.slotKey, (1L << OFF_0_1) | (1L << OFF_0_2), 0, AB);
// Set up digits on the grid to create slots.
// '2' (right) at (0,0) -> slot at (0,1), (0,2)
grid.setClue(0, (byte) '2');
grid.setByteAt(Grid.offset(0, 1), (byte) 'A');
grid.setByteAt(Grid.offset(0, 2), (byte) 'B');
var slots = extractSlots(grid);
assertEquals(1, slots.size());
var s = slots.getFirst();
assertEquals(8, s.len());
assertEquals(0, Grid.r(s.pos(0)));
assertEquals(1, Grid.c(s.pos(0)));
assertEquals(0, Grid.r(s.pos(1)));
assertEquals(2, Grid.c(s.pos(1)));
var slots = Masker.slots(clues.c(), DictData950.DICT950.index(), DictData950.DICT950.reversed());
assertEquals(1, slots.length);
var s = slots[0];
assertEquals(8, Masker.Slot.length(s.lo(), s.hi()));
var cells = Riddle.cellWalk(s.key(), s.lo(), s.hi()).mapToObj(c -> Masker.IT[c]).toArray(rci[]::new);
assertEquals(0, cells[1].r());
assertEquals(1, cells[1].c());
assertEquals(0, cells[2].r());
assertEquals(2, cells[2].c());
}
@Test
void testStaticSlotMethods() {
// Test static offset extraction
// packedPos: offset(1, 1) at index 0, offset(2, 2) at index 1
var packedPos = ((long) Grid.offset(1, 1)) | (((long) Grid.offset(2, 2)) << 7);
assertEquals(Grid.offset(1, 1), Slot.offset(packedPos, 0));
assertEquals(Grid.offset(2, 2), Slot.offset(packedPos, 1));
// Test static horiz
// dir 2 (right) is horizontal
assertTrue(Slot.horiz(2));
// dir 3 (down) is vertical
assertFalse(Slot.horiz(3));
// dir 1 (right) is horizontal
assertTrue(Masker.Slot.horiz(1));
// dir 0 (down) is vertical
assertFalse(Masker.Slot.horiz(0));
}
@Test
void testForEachSlot() {
var grid = Grid.createEmpty();
grid.setClue(0, (byte) '2'); // right
var count = new AtomicInteger(0);
grid.forEachSlot((key, packedPos, len) -> {
Masker.forEachSlot(Riddle.Signa.of(r0c0d1).c(), (key, lo, hi) -> {
count.incrementAndGet();
assertEquals(8, len);
assertEquals(0, Grid.r(Slot.offset(packedPos, 0)));
assertEquals(1, Grid.c(Slot.offset(packedPos, 0)));
assertEquals(8, Long.bitCount(lo) + Long.bitCount(hi));
assertEquals(0, Masker.IT[Long.numberOfTrailingZeros(lo)].r());
assertEquals(1, Masker.IT[Long.numberOfTrailingZeros(lo)].c());
});
assertEquals(1, count.get());
}
@Test
public void testHoriz() {
assertTrue(Slot.from(2, 0L, 1).horiz());
assertTrue(Slot.from(4, 0L, 1).horiz());
assertFalse(Slot.from(1, 0L, 1).horiz());
assertFalse(Slot.from(3, 0L, 1).horiz());
assertFalse(Slot.from(5, 0L, 1).horiz());
assertTrue(Masker.Slot.horiz(1)); // Right
assertTrue(Masker.Slot.horiz(3)); // Left
assertFalse(Masker.Slot.horiz(0)); // Down
assertFalse(Masker.Slot.horiz(2)); // Up
assertFalse(Masker.Slot.horiz(4)); //
assertFalse(Masker.Slot.horiz(5)); //
}
@Test
public void testGridBasics() {
var grid = Grid.createEmpty();
var clues = Riddle.Signa.of(r2c1d2);
var grid = new Puzzle(clues);
r1c1.or(r0c1).lo();
// Test set/get
grid.setByteAt(Grid.offset(0, 0), (byte) 'A');
grid.setClue(Grid.offset(1, 2), (byte) '5');
grid.setByteAt(Grid.offset(2, 3), (byte) 'Z');
Assertions.assertEquals((byte) 'A', grid.byteAt(Grid.offset(0, 0)));
Assertions.assertEquals((byte) '5', grid.byteAt(Grid.offset(1, 2)));
Assertions.assertEquals((byte) 'Z', grid.byteAt(Grid.offset(2, 3)));
Assertions.assertEquals(DASH, grid.byteAt(Grid.offset(1, 1)));
GridBuilder.placeWord(grid.grid(), grid.grid().g, r2c1d2.slotKey, (1L << OFF_1_1) | (1L << OFF_0_1), 0, AZ);
val map = new Puzzle(grid.grid(), clues.c()).collect(Collectors.toMap(Mask::index, Mask::d));
Assertions.assertEquals(LETTER_A, map.get(OFF_1_1));
Assertions.assertEquals(LETTER_Z, map.get(OFF_0_1));
var clueMap = clues.stream().collect(Collectors.toMap(Riddle.Vestigium::index, Riddle.Vestigium::clue));
Assertions.assertEquals(1, clueMap.size());
Assertions.assertEquals(UP2.dir, clueMap.get(OFF_2_1));
// Test isLetterAt
Assertions.assertTrue(grid.isLetterSet(Grid.offset(0, 0)));
Assertions.assertFalse(grid.isLetterSet(Grid.offset(1, 2)));
Assertions.assertTrue(grid.isLetterSet(Grid.offset(2, 3)));
Assertions.assertFalse(grid.isLetterSet(Grid.offset(1, 1)));
Assertions.assertFalse(clueMap.containsKey(OFF_0_0));
Assertions.assertFalse(clueMap.containsKey(OFF_1_2));
Assertions.assertFalse(clueMap.containsKey(OFF_2_3));
Assertions.assertFalse(clueMap.containsKey(OFF_1_1));
// Test isDigitAt
Assertions.assertFalse(grid.isDigitAt(0));
Assertions.assertTrue(grid.isDigitAt(Grid.offset(1, 2)));
Assertions.assertEquals(5, grid.digitAt(Grid.offset(1, 2)));
Assertions.assertFalse(grid.isDigitAt(Grid.offset(2, 3)));
Assertions.assertFalse(grid.isDigitAt(Grid.offset(1, 1)));
Assertions.assertFalse(clues.isClueLo(OFF_0_0));
Assertions.assertTrue(clues.isClueLo(OFF_2_1));
clueMap = clues.stream().collect(Collectors.toMap(Riddle.Vestigium::index, Riddle.Vestigium::clue));
Assertions.assertEquals(UP2.dir, clueMap.get(OFF_2_1));
Assertions.assertFalse(clues.isClueLo(OFF_2_3));
Assertions.assertFalse(clues.isClueLo(OFF_1_1));
// Test isLettercell
Assertions.assertTrue(grid.isLetterAt(Grid.offset(0, 0))); // 'A' is letter
Assertions.assertFalse(grid.isLetterAt(Grid.offset(1, 2))); // '5' is digit
Assertions.assertTrue(grid.isLetterAt(Grid.offset(1, 1))); // '#' is lettercell
Assertions.assertTrue(clues.notClue(OFF_0_0)); // 'A' is letter
Assertions.assertTrue(clues.isClueLo(OFF_2_1)); // digit
Assertions.assertTrue(clues.notClue(OFF_1_1)); // '#' is lettercell
}
@Test
public void testGridDeepCopy() {
var grid = Grid.createEmpty();
grid.setByteAt(Grid.offset(0, 0), (byte) 'A');
grid.setByteAt(Grid.offset(0, 1), (byte) 'B');
grid.setByteAt(Grid.offset(1, 0), (byte) 'C');
grid.setByteAt(Grid.offset(1, 1), (byte) 'D');
public void testCluesDeepCopy() {
var clues = Riddle.Signa.of(r0c0d1, r0c1d2, r1c0d3, r1c1d0);
var copy = grid.deepCopyGrid();
Assertions.assertEquals((byte) 'A', copy.byteAt(0));
var copy = clues.deepCopyGrid();
var clueMap = clues.stream().collect(Collectors.toMap(Riddle.Vestigium::index, Riddle.Vestigium::clue));
Assertions.assertEquals(RIGHT1.dir, clueMap.get(OFF_0_0));
copy.setByteAt(0, (byte) 'X');
Assertions.assertEquals((byte) 'X', copy.byteAt(0));
Assertions.assertEquals((byte) 'A', grid.byteAt(0)); // Original should be unchanged
copy.setClue(r0c0d0);
var copied = copy.stream().collect(Collectors.toMap(Riddle.Vestigium::index, Riddle.Vestigium::clue));
Assertions.assertEquals(DOWN0.dir, copied.get(OFF_0_0));
Assertions.assertEquals(RIGHT1.dir, clueMap.get(OFF_0_0));
}
@Test
public void testMini() {
var grid = Grid.createEmpty();
val idx = Grid.offset(1, 1);
grid.setClue(idx, (byte) '1');
Assertions.assertTrue(grid.isDigitAt(idx));
Assertions.assertTrue(Riddle.Signa.of(r1c1d3).isClueLo(OFF_1_1));
}
@Test
void testFiller2() {
var mask = Riddle.Signa.of(
r0c0d1,
r0c3d0, r0c4d0, r0c5d0, r0c6d0, r0c7d0, r0c8d0,
r1c0d1,
r2c0d1,
r3c0d3, r3c3d3,
r4c0d3, r4c3d0, r4c6d3,
r5c0d3,
r6c0d3,
r7c0d2, r7c1d2, r7c2d2, r7c8d3
);
Assertions.assertEquals(20, mask.clueCount());
val map = mask.stream().collect(Collectors.toMap(Riddle.Vestigium::index, Riddle.Vestigium::clue));
Assertions.assertEquals(20, map.size());
var slots = Masker.slots(mask.c(), DictData950.DICT950.index(), DictData950.DICT950.reversed());
// var filled = fillMask(rng, slotInfo, grid, false);
// val res = new PuzzleResult(new Clued(mask), new Gridded(grid), slotInfo, filled).exportFormatFromFilled(0, new Rewards(0, 0, 0));
}
@Test
void testFiller() {
System.out.println(DictData950.DICT950.index().length);
val rng = new Rng(-343913721);
var mask = Riddle.Signa.of(
r0c3d3, r0c6d3, r0c7d0, r0c8d0,
r1c1d1,
r2c1d1,
r3c3d3, r3c6d0,
r4c2d3, r4c3d1,
r5c1d1,
r6c1d1, r6c8d2,
r7c0d2, r7c1d1, r7c4d2, r7c5d2, r7c8d3
);
var slotInfo = Masker.slots(mask.c(), DictData950.DICT950.index(), DictData950.DICT950.reversed());
var grid = Masker.grid(slotInfo);
var filled = fillMask(rng, slotInfo, grid.lo, grid.hi, grid.g);
Assertions.assertTrue(filled.ok(), "Puzzle generation failed (not ok)");
Assertions.assertEquals(17, Slotinfo.wordCount(0, slotInfo), "Number of assigned words changed");
Assertions.assertEquals("BEADEMT", Lemma.asWord(slotInfo[0].assign().w, Export.BYTES.get().wordBytes()));
Assertions.assertEquals(74732156493031040L, grid.lo);
Assertions.assertEquals(193L, grid.hi);
grid.lo = ~mask.c().lo;
grid.hi = 0xFFL & ~mask.c().hi;
var g = new Puzzle(grid, mask.c());
var result = new PuzzleResult(mask, g, slotInfo, filled);
var aa = result.exportFormatFromFilled(new Rewards(1, 1, 1));
result.gridGridToString();
System.out.println(String.join("\n", aa.grid()));
}
@Test
public void testAttempt() {
// Arrange
var opts = new Main.Opts();
opts.seed = 12348;
opts.pop = 4; // Tiny population
opts.gens = 20; // Very few generations
opts.minSimplicity = 0;
opts.fillTimeout = 10_000;
opts.threads = 1;
opts.tries = 1;
opts.verbose = false;
var dict = Dict.loadDict(opts.wordsPath);
// Act
PuzzleResult res = null;
int foundSeed = -1;
for (int i = 0; i < 50; i++) {
int seed = opts.seed + i;
res = Main.attempt(new Rng(seed), dict, opts);
res = Main.attempt(new Rng(seed), DictData950.DICT950, opts);
if (res != null && res.filled().ok()) {
foundSeed = seed;
System.out.println("[DEBUG_LOG] Seed found: " + seed);
System.out.println("[DEBUG_LOG] Simplicity: " + res.filled().stats().simplicity);
System.out.println("[DEBUG_LOG] ClueMap Size: " + res.filled().clueMap().size());
System.out.println("[DEBUG_LOG] ClueMap Size: " + Slotinfo.wordCount(0, res.slots()));
System.out.println("[DEBUG_LOG] Grid:");
System.out.println(res.filled().grid().renderHuman());
System.out.println(res.gridRenderHuman());
System.out.println(res.gridGridToString());
break;
}
}
// Assert
Assertions.assertNotNull(res, "Puzzle generation failed (null result)");
Assertions.assertTrue(res.filled().ok(), "Puzzle generation failed (not ok)");
// Regression baseline for seed search starting at 12347, pop 4, gens 20
Assertions.assertEquals(12348, foundSeed, "Found seed changed");
Assertions.assertEquals(18, res.filled().clueMap().size(), "Number of assigned words changed");
Assertions.assertEquals("RIJTUIG", Lemma.asWord( res.filled().clueMap().get(74)));
Assertions.assertEquals(301794542151533187L, res.filled().grid().grid().lo);
Assertions.assertEquals(193L, res.filled().grid().grid().hi);
}
@Test
public void testIsLetterA() {
assertTrue(isLetter((byte) 'A'));
}
@Test
public void testIsLetterZ() {
assertTrue(isLetter((byte) 'Z'));
}
boolean isLetter(byte b) { return (b & 64) != 0; }
@Test public void testIsLetterA() { assertTrue(isLetter((byte) 'A')); }
@Test public void testIsLetterZ() { assertTrue(isLetter((byte) 'Z')); }
}

View File

@@ -0,0 +1,477 @@
package puzzle;
import lombok.val;
import org.junit.jupiter.api.Test;
import puzzle.Export.Puzzle;
import puzzle.Export.PuzzleResult;
import puzzle.Riddle.Signa;
import puzzle.Riddle.Rewards;
import puzzle.SwedishGenerator.Assign;
import puzzle.SwedishGenerator.FillResult;
import puzzle.SwedishGenerator.Lemma;
import puzzle.SwedishGenerator.Rng;
import puzzle.SwedishGenerator.Slotinfo;
import puzzle.dict950.DictData950;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static precomp.Const9x8.Cell.r0c0d0;
import static precomp.Const9x8.Cell.r0c0d1;
import static precomp.Const9x8.Cell.r0c0d4;
import static precomp.Const9x8.Cell.r0c1d5;
import static precomp.Const9x8.Cell.r0c2d1;
import static precomp.Const9x8.Cell.r0c2d5;
import static precomp.Const9x8.Cell.r0c3d1;
import static precomp.Const9x8.Cell.r0c5d3;
import static precomp.Const9x8.Cell.r0c7d1;
import static precomp.Const9x8.Cell.r0c8d1;
import static precomp.Const9x8.Cell.r1c1d1;
import static precomp.Const9x8.Cell.r1c2d4;
import static precomp.Const9x8.Cell.r2c0d1;
import static precomp.Const9x8.Cell.r2c1d0;
import static precomp.Const9x8.Cell.r2c1d1;
import static precomp.Const9x8.Cell.r2c2d2;
import static precomp.Const9x8.Cell.r3c1d1;
import static precomp.Const9x8.Cell.r7c7d1;
import static precomp.Const9x8.OFF_0_0;
import static precomp.Const9x8.OFF_0_1;
import static precomp.Const9x8.OFF_0_2;
import static precomp.Const9x8.OFF_0_3;
import static precomp.Const9x8.OFF_0_4;
import static precomp.Const9x8.OFF_1_0;
import static precomp.Const9x8.OFF_1_1;
import static puzzle.GridBuilder.placeWord;
import static puzzle.LemmaData.TEST;
import static puzzle.Masker.C;
import static puzzle.Masker.R;
import static puzzle.Masker.STACK_SIZE;
public class MarkerTest {
private static Masker emptyMasker() {
return new Masker(new Rng(42), new int[STACK_SIZE], Clues.createEmpty());
}
@Test
void testValidRandomMask() {
var masker = emptyMasker();
for (var i = 0; i < 200; i++) {
for (var j = 19; j < 24; j++) {
var clues = masker.randomMask(j);
assertTrue(masker.isValid(clues), "Mask should be valid for length \n" + Export.gridToString(clues));
}
}
}
@Test
void testValidMutate() {
var masker = emptyMasker();
var sim = 0.0;
var simCount = 0.0;
for (var i = 0; i < 200; i++) {
for (var j = 19; j < 24; j++) {
var clues = masker.randomMask(j);
val orig = masker.cache(clues);
simCount++;
masker.mutate(clues);
sim += Masker.similarity(orig, clues);
assertTrue(masker.isValid(clues), "Mask should be valid for length \n" + Export.gridToString(clues));
}
}
System.out.println("Average similarity: " + sim / simCount);
}
@Test
void testCross() {
var masker = emptyMasker();
var sim = 0.0;
var simCount = 0.0;
for (var i = 0; i < 200; i++) {
for (var j = 19; j < 24; j++) {
var clues = masker.randomMask(j);
var clues2 = masker.randomMask(j);
simCount++;
var cross = masker.crossover(clues, clues2);
sim += Math.max(Masker.similarity(cross, clues), Masker.similarity(cross, clues2));
assertTrue(masker.isValid(cross), "Mask should be valid for length \n" + Export.gridToString(cross));
}
}
System.out.println("Average similarity: " + sim / simCount);
}
@Test
void testSimilarity() {
var a = Riddle.Signa.of(r0c0d1, r2c1d0).c();
var b = Riddle.Signa.of(r0c0d1, r2c1d0).c();
// Identity
assertEquals(1.0, Masker.similarity(a, b), 0.001);
// Different direction
var c = Riddle.Signa.of(r0c0d0, r2c1d0);
assertTrue(Masker.similarity(a, c.c()) < 1.0);
// Completely different
var d = Clues.createEmpty();
// Matching empty cells count towards similarity.
// a has 2 clues, d has 0. They match on 70 empty cells.
assertEquals(70.0 / 72.0, Masker.similarity(a, d), 0.001);
}
@Test
void testIsValid() {
var masker = emptyMasker();
assertTrue(masker.isValid(Clues.createEmpty()));
// Valid clue: Right from (0,0) in 9x8 grid. Length is 8.
assertTrue(masker.isValid(Riddle.Signa.of(r0c0d1).c()));
// Invalid clue: Right from (0,7) in 9x8 grid. Length is 1 (too short if MIN_LEN >= 2).
assertFalse(masker.isValid(Riddle.Signa.of(r0c7d1).c()));
}
@Test
void testHasRoomForClue() {
var g = Clues.createEmpty();
// Room for Right clue at (0,0) (length 8)
assertTrue(Masker.hasRoomForClue(g, r0c0d1.slotKey));
// No room for Right clue at (0,8) (length 0 < MIN_LEN)
assertFalse(Masker.hasRoomForClue(g, r0c8d1.slotKey));
// Blocked room
// Let's place a clue that leaves only 1 cell for another clue.
g.setClue(r0c2d1);
// Now Right at (0,0) only has (0,1) available -> length 1 < MIN_LEN (which is 2)
assertFalse(Masker.hasRoomForClue(g, r0c0d1.slotKey));
// But enough room
g.clearClueLo(0L);
g.setClue(r0c3d1);
// Now Right at (0,0) has (0,1), (0,2) -> length 2 == MIN_LEN
assertTrue(Masker.hasRoomForClue(g, r0c0d1.slotKey));
}
@Test
void testIntersectionConstraint() {
var masker = emptyMasker();
// Clue 1: (0,0) Right. Slot cells: (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8)
// Clue 2: (1,2) Up. Slot cells: (0,2)
// Intersection is exactly 1 cell (0,2). Valid.
assertTrue(masker.isValid(Riddle.Signa.of(r0c0d1, r2c2d2).c()));
// Clue 3: (1,3) Right. Slot cells: (1,4), (1,5), ...
// No intersection with Clue 1 or 2. Valid.
assertTrue(masker.isValid(Riddle.Signa.of(r0c0d1, r2c2d2, precomp.Const9x8.Cell.r1c3d1).c()));
// Now create a violation: two slots sharing 2 cells.
// We can do this with Corner Down and another clue.
// Clue A: (0,0) Corner Down. Starts at (0,1) goes down: (0,1), (1,1), (2,1), (3,1), ...
// Clue B: (0,2) Corner Down Left. Starts at (0,1) goes down: (0,1), (1,1), (2,1), ...
// They share MANY cells starting from (0,1).
assertFalse(masker.isValid(Riddle.Signa.of(r0c0d4, r0c2d5).c()));
}
@Test
void testInvalidDirectionBits() {
var masker = emptyMasker();
var g = Clues.createEmpty();
// Dir 6 (x=1, r=1, v=0) is invalid
g.setClueLo(1L << 0, (byte) 6);
assertFalse(masker.isValid(g));
// Dir 7 (x=1, r=1, v=1) is invalid
var g2 = Clues.createEmpty();
g2.setClueLo(1L << 0, (byte) 7);
assertFalse(masker.isValid(g2));
}
@Test
void testConnectivityPenalty() {
var masker = emptyMasker();
// 1. Maak een masker met één component van clues (bijv. 2 clues die elkaar kruisen)
var singleComp = Clues.createEmpty().setClue(r0c0d1).setClue(r2c2d2);
var fitnessSingle = masker.maskFitness(singleComp, 2);
// 2. Maak een masker met twee eilandjes van clues
var twoIslands = Clues.createEmpty().setClue(r0c0d1).setClue(r7c7d1);
var fitnessIslands = masker.maskFitness(twoIslands, 2);
System.out.println("[DEBUG_LOG] Fitness single component: " + fitnessSingle);
System.out.println("[DEBUG_LOG] Fitness two islands: " + fitnessIslands);
assertTrue(fitnessIslands > fitnessSingle + 10000, "Islands should have much higher penalty");
}
@Test
void testDeadCellPenalty() {
var masker = emptyMasker();
// A cell surrounded by 4 clues is a "dead cell"
// Let's take cell (4,4)
// Clues at (4,3), (4,5), (3,4), (5,4)
var deadClues = Clues.createEmpty();
deadClues.setClue(precomp.Const9x8.CELLS[precomp.Const9x8.OFF_4_3 * 33 + 1]); // (4,3) Down
deadClues.setClue(precomp.Const9x8.CELLS[precomp.Const9x8.OFF_4_5 * 33 + 1]); // (4,5) Down
deadClues.setClue(precomp.Const9x8.CELLS[precomp.Const9x8.OFF_3_4 * 33]); // (3,4) Right
deadClues.setClue(precomp.Const9x8.CELLS[precomp.Const9x8.OFF_5_4 * 33]); // (5,4) Right
var fitness = masker.maskFitness(deadClues, 4);
System.out.println("[DEBUG_LOG] Fitness dead cell: " + fitness);
// The cell (4,4) should have 4 clue neighbors -> penalty 2000.
// It is also not covered by any word -> penalty 4000.
// Total penalty should be high.
assertTrue(fitness > 5000, "Dead cell should have high penalty");
}
@Test
void testDeadCellIsValid() {
var masker = emptyMasker();
var deadClues = Clues.createEmpty();
deadClues.setClue(precomp.Const9x8.Cell.r0c1d1);
deadClues.setClue(precomp.Const9x8.Cell.r2c1d1);
deadClues.setClue(precomp.Const9x8.Cell.r1c0d0);
deadClues.setClue(precomp.Const9x8.Cell.r1c2d0);
int offending = masker.findOffendingClue(deadClues);
if (offending != -1) {
System.out.println("[DEBUG_LOG] Offending clue index: " + offending);
}
assertFalse(masker.isValid(deadClues), "Mask with dead cell should be invalid");
}
@Test
void testCleanupDeadCell() {
var masker = emptyMasker();
var deadClues = Clues.createEmpty();
deadClues.setClue(precomp.Const9x8.Cell.r0c1d1);
deadClues.setClue(precomp.Const9x8.Cell.r2c1d1);
deadClues.setClue(precomp.Const9x8.Cell.r1c0d0);
deadClues.setClue(precomp.Const9x8.Cell.r1c2d0);
assertFalse(masker.isValid(deadClues));
masker.cleanup(deadClues);
assertTrue(masker.isValid(deadClues), "After cleanup, mask should be valid");
assertTrue(deadClues.clueCount() < 4, "Cleanup should have removed at least one clue");
}
@Test
void testOnlyOneWordCoveragePenalty() {
var masker = emptyMasker();
// A white cell covered by only one word.
// Clue (4,0) Right. Cell (4,1) is covered only by this horizontal word.
var oneWord = Clues.createEmpty().setClue(precomp.Const9x8.CELLS[precomp.Const9x8.OFF_4_0 * 33]);
var fitness = masker.maskFitness(oneWord, 1);
System.out.println("[DEBUG_LOG] Fitness only one word: " + fitness);
// Each white cell of (4,0) Right (length 8) will have 1 word coverage.
// Penalty per cell is 1500. 8 cells * 1500 = 12000.
// Plus some 1-clue-per-row/col penalties etc.
assertTrue(fitness > 11000, "Should have penalty for single word coverage");
}
@Test
void testCornerClueConnectivity() {
var masker = emptyMasker();
// Clue A: (2,0) Right. Slot: (2,1), (2,2), (2,3), ...
// Clue B: (1,2) Corner Down. Word starts at (1,3) en gaat omlaag: (1,3), (2,3), (3,3)...
// Ze kruisen op (2,3).
var clues = Clues.createEmpty().setClue(r2c0d1).setClue(r1c2d4);
var fitness = masker.maskFitness(clues, 2);
System.out.println("[DEBUG_LOG] Fitness corner clue connected: " + fitness);
// Als ze verbonden zijn, is de penalty voor eilandjes 0.
// We vergelijken met een island scenario (2 clues die elkaar NIET raken)
var island = Clues.createEmpty().setClue(r2c0d1).setClue(r7c7d1);
var fitnessIsland = masker.maskFitness(island, 2);
System.out.println("[DEBUG_LOG] Fitness island: " + fitnessIsland);
assertTrue(fitnessIsland > fitness + 20000, "Island should add significant penalty compared to connected corner clue");
}
@Test
void testCornerDownSlot() {
var clues = Riddle.Signa.of(r0c0d4);
// Clue op (0,0), type 4 (Corner Down)
assertEquals(r0c0d4.d, clues.getDir(r0c0d4.index));
// Controleer of forEachSlot het slot vindt
final var found = new boolean[]{ false };
Masker.forEachSlot(clues.c(), (key, lo, hi) -> {
if (key == r0c0d4.slotKey) {
found[0] = true;
// Woord zou moeten starten op (0,1)
assertTrue((lo & (1L << OFF_0_1)) != 0, "Slot should start at (0,1)");
// En omlaag gaan
assertTrue((lo & (1L << OFF_1_1)) != 0, "Slot should continue to (1,1)");
// Lengte van het slot zou 8 moeten zijn (van rij 0 t/m 7 in kolom 1)
assertEquals(8, Masker.Slot.length(lo, hi));
}
});
assertTrue(found[0], "Corner Down slot should be found");
}
@Test
void testCornerDownExtraction() {
var slots = Masker.slots(Riddle.Signa.of(r0c0d4).c(), DictData950.DICT950);
assertEquals(1, slots.length);
assertEquals(r0c0d4.d, Masker.Slot.dir(slots[0].key()));
}
@Test
void testCornerDownLeftSlot() {
var clues = Riddle.Signa.of(r0c1d5);
assertEquals(r0c1d5.d, clues.getDir(r0c1d5.index));
// Controleer of forEachSlot het slot vindt
final var found = new boolean[]{ false };
Masker.forEachSlot(clues.c(), (key, lo, hi) -> {
if (key == r0c1d5.slotKey) {
found[0] = true;
// Woord zou moeten starten op (0,0)
assertTrue((lo & (1L << OFF_0_0)) != 0, "Slot should start at (0,0)");
// En omlaag gaan
assertTrue((lo & (1L << OFF_1_0)) != 0, "Slot should continue to (1,0)");
// Lengte van het slot zou 8 moeten zijn (van rij 0 t/m 7 in kolom 0)
assertEquals(8, Masker.Slot.length(lo, hi));
}
});
assertTrue(found[0], "Corner Down Left slot should be found");
}
@Test
void testCornerDownLeftExtraction() {
var slots = Masker.slots(Riddle.Signa.of(r0c1d5).c(), DictData950.DICT950.index(), DictData950.DICT950.reversed());
assertEquals(1, slots.length);
assertEquals(r0c1d5.d, Masker.Slot.dir(slots[0].key()));
}
@Test
void testExportFormatFromFilled() {
val clues = Riddle.Signa.of(r0c0d1, r0c5d3);
var puzzle = new Puzzle(clues);
// key = (cellIndex << 2) | (direction)
var key = r0c0d1.slotKey;
var lo = (1L << OFF_0_1) | (1L << OFF_0_2) | (1L << OFF_0_3) | (1L << OFF_0_4);
assertTrue(placeWord(puzzle.grid(), puzzle.grid().g, key, lo, 0L, TEST));
var fillResult = new FillResult(true, 0, 0, 0, 0);
var puzzleResult = new PuzzleResult(clues, new Puzzle(puzzle.grid(), puzzle.cl()), new Slotinfo[]{
new Slotinfo(key, lo, 0L, 0, new Assign(TEST), null, 0)
}, fillResult);
var rewards = new Rewards(10, 5, 1);
var exported = puzzleResult.exportFormatFromFilled(rewards);
assertNotNull(exported);
assertEquals(709, exported.difficulty());
assertEquals(rewards, exported.rewards());
// Check words
assertEquals(1, exported.words().length);
var w = exported.words()[0];
assertEquals("TEST", w.word());
assertEquals(Riddle.Placed.HORIZONTAL, w.direction());
// The bounding box should include (0,0) for the arrow and (0,1)-(0,4) for the word.
// minR=0, maxR=0, minC=0, maxC=4
// startRow = 0 - minR = 0
// startCol = 1 - minC = 1
assertEquals(0, w.startRow());
assertEquals(1, w.startCol());
assertEquals(0, w.arrowRow());
assertEquals(0, w.arrowCol());
// Check gridv2
// It should be 1 row, containing "2TEST" -> but letters are mapped, digits are not explicitly in letterAt.
// Wait, look at exportFormatFromFilled logic:
// row.append(letterAt.getOrDefault(pack(r, c), '#'));
// letterAt only contains letters from placed words.
// arrow cells are NOT in letterAt unless they are also part of a word (unlikely).
// So (0,0) should be '#'
assertEquals(1, exported.grid().length);
assertEquals("#TEST", exported.grid()[0]);
}
@Test
void testExportFormatEmpty() {
var grid = SwedishGeneratorTest.createEmpty9x8();
val clues = Clues.createEmpty();
var fillResult = new FillResult(true, 0, 0, 0, 0);
var puzzleResult = new PuzzleResult(new Signa(clues), new Puzzle(grid, clues), new Slotinfo[0], fillResult);
var exported = puzzleResult.exportFormatFromFilled(new Rewards(0, 0, 0));
assertNotNull(exported);
assertEquals(0, exported.words().length);
// Should return full grid with '#'
assertEquals(R, exported.grid().length);
for (var row : exported.grid()) {
assertEquals(C, row.length());
assertTrue(row.matches("#+"));
}
}
@Test
void testShardToClue() {
var bytes = Export.BYTES.get().wordBytes();
for (var length = 2; length <= 8; length++) {
val entry = DictData950.DICT950.index()[length];
if (entry == null) continue;
val words = entry.words();
for (var i = 0; i < Math.min(words.length, 5); i++) {
val wordVal = words[i];
val rec = Meta.lookupSilent(wordVal);
assertNotNull(rec);
assertEquals(Lemma.asWord(wordVal, bytes), Lemma.asWord(rec.w(), bytes));
assertTrue(rec.simpel() >= 0);
assertTrue(rec.clues().length > 0);
}
}
}
@Test
void testSpecificWords() {
// These words are known to be in the CSV and likely in the dictionary
var bytes = Export.BYTES.get().wordBytes();
var testWords = new String[]{ "EEN", "NAAR", "IEDEREEN" };
for (var wStr : testWords) {
var w = Lemma.from(wStr);
val clueRec = Meta.lookupSilent(w);
assertNotNull(clueRec);
assertEquals(wStr, Lemma.asWord(clueRec.w(), bytes));
// Check some expected complexity values (from CSV head output, column 3)
if (wStr.equals("EEN")) {
assertEquals(451, clueRec.simpel());
assertEquals("een geheel vormend", clueRec.clues()[0]);
}
if (wStr.equals("NAAR")) {
assertEquals(497, clueRec.simpel());
assertEquals("onaangenaam, vervelend, rot, niet leuk", clueRec.clues()[0]);
}
if (wStr.equals("IEDEREEN")) {
assertEquals(501, clueRec.simpel());
assertEquals("elke persoon", clueRec.clues()[0]);
}
assertTrue(clueRec.clues().length > 0);
}
}
}

View File

@@ -0,0 +1,223 @@
package puzzle;
import module java.base;
import anno.DictGen;
import anno.Dictionaries;
import lombok.val;
import org.junit.jupiter.api.Test;
import puzzle.SwedishGenerator.Rng;
import puzzle.SwedishGenerator.Slotinfo;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static precomp.Const9x8.Cell.r0c0d1;
import static precomp.Const9x8.Cell.r0c5d0;
import static precomp.Const9x8.Cell.r0c6d0;
import static precomp.Const9x8.Cell.r0c7d0;
import static precomp.Const9x8.Cell.r0c8d0;
import static precomp.Const9x8.Cell.r1c0d1;
import static precomp.Const9x8.Cell.r2c0d0;
import static precomp.Const9x8.Cell.r2c1d0;
import static precomp.Const9x8.Cell.r2c3d0;
import static precomp.Const9x8.Cell.r2c4d1;
import static precomp.Const9x8.Cell.r3c4d1;
import static precomp.Const9x8.Cell.r4c4d1;
import static precomp.Const9x8.Cell.r5c2d2;
import static precomp.Const9x8.Cell.r5c4d1;
import static precomp.Const9x8.Cell.r6c2d1;
import static precomp.Const9x8.Cell.r7c0d2;
import static precomp.Const9x8.Cell.r7c1d2;
import static precomp.Const9x8.Cell.r7c2d1;
import static precomp.Const9x8.Cell.r7c7d2;
import static precomp.Const9x8.Cell.r7c8d2;
import static puzzle.SwedishGenerator.fillMask;
import static puzzle.dict800.DictData800.DICT800;
import static puzzle.dict900.DictData900.DICT900;
@Dictionaries({ @DictGen(
packageName = "puzzle.dict900",
className = "DictData900",
scv = "/home/mike/dev/puzzle-generator/nl_score_hints_v4.csv",
simpleMax = 900,
minLen = 2,
maxLen = 8
), @DictGen(
packageName = "puzzle.dict800",
className = "DictData800",
scv = "/home/mike/dev/puzzle-generator/nl_score_hints_v4.csv",
simpleMax = 800,
minLen = 2,
maxLen = 8
), @DictGen(
packageName = "puzzle.dict800_4",
className = "DictData800_4",
scv = "/home/mike/dev/puzzle-generator/nl_score_hints_v4.csv",
simpleMax = 800,
minLen = 2,
maxLen = 4
) })
public class PerformanceTest {
void main() {
testPerformance();
}
@Test
void testPerformance() {
val rng = new Rng(42);
// 1. Stress test Clue Generation (Mask Generation)
System.out.println("[DEBUG_LOG] --- Mask Generation Performance ---");
var clueSizes = new int[]{ 20,22,24, 25, 30 };
var arr = new Clues[clueSizes.length];
var c = 0;
val masker = new Masker(rng, new int[Masker.STACK_SIZE], Clues.createEmpty());
for (var size : clueSizes) {
var t0 = System.currentTimeMillis();
// Increased population and generations for stress
arr[c++] = masker.generateMask(size, 200, 700, 50);
var t1 = System.currentTimeMillis();
var duration = (t1 - t0) / 1000.0;
System.out.printf(Locale.ROOT, "[DEBUG_LOG] Size %d (pop=200, gen=100): %.3fs%n", size, duration);
// Basic sanity check: should not take forever
assertTrue(duration < 10.0, "Mask generation took too long for size " + size);
}
// 2. Stress test Word Filler
System.out.println("[DEBUG_LOG] \n--- Word Filler Performance ---");
c = 0;
for (var size : clueSizes) {
var t0 = System.currentTimeMillis();
// Try to fill multiple times to get a better average
var iterations = 10;
long totalNodes = 0;
long totalBacktracks = 0;
var successCount = 0;
for (var i = 0; i < iterations; i++) {
val slotInfo = Masker.slots(arr[c], DICT800);
var grid = Masker.grid(slotInfo);
val result = fillMask(rng, slotInfo, grid.lo, grid.hi, grid.g);
if (result.ok()) successCount++;
totalNodes += result.nodes();
totalBacktracks += result.backtracks();
}
c++;
var t1 = System.currentTimeMillis();
var totalDuration = (t1 - t0) / 1000.0;
System.out.printf(Locale.ROOT, "[DEBUG_LOG] Size %d: %d/%d SUCCESS | avg nodes=%d | avg backtracks=%d | total time=%.3fs%n",
size, successCount, iterations, totalNodes / iterations, totalBacktracks / iterations, totalDuration);
}
}
@Test
void testIncrementalComplexity() {
// Use the complex mask from Main.java
var mask = Riddle.Signa.of(
r0c0d1, r0c5d0, r0c6d0, r0c7d0, r0c8d0,
r1c0d1,
r2c0d0, r2c1d0, r2c3d0, r2c4d1,
r3c4d1,
r4c4d1,
r5c2d2, r5c4d1,
r6c2d1,
r7c0d2, r7c1d2, r7c2d1, r7c7d2, r7c8d2
);
val allSlots = Masker.slots(mask.c(), DICT900);
//mask.toGrid()
System.out.println("[DEBUG_LOG] \n--- Incremental Complexity Test ---");
System.out.println("[DEBUG_LOG] Full Slot Layout:");
visualizeSlots(allSlots);
for (var i = 10; i <= allSlots.length; i++) {
val subset = Arrays.copyOf(allSlots, i);
// Arrays.sort(subset, Comparator.comparingInt(Slotinfo::score));
System.out.printf("[DEBUG_LOG] Testing with first %d slots%n of %s", i, allSlots.length);
visualizeSlots(subset);
measureFill(new Rng(123 + i), subset, "Subset size " + i);
}
}
@Test
void testSingleSlotResolution() {
val rng = new Rng(42);
// A single horizontal slot at (0,0)
val mask = Riddle.Signa.of(r0c0d1);
val slots = Masker.slots(mask.c(), DICT800);
System.out.println("[DEBUG_LOG] \n--- Single Slot Resolution ---");
if (slots.length > 0) {
measureFill(rng, slots, "Single Slot");
} else {
System.out.println("[DEBUG_LOG] Error: No slots found in mask.");
}
}
private void measureFill(Rng rng, Slotinfo[] slots, String label) {
var t0 = System.currentTimeMillis();
var iterations = 1;
long totalNodes = 0;
long totalBacktracks = 0;
var successCount = 0;
for (var i = 0; i < iterations; i++) {
// Reset assignments for each iteration
for (var s : slots) s.assign().w = 0;
var grid = Masker.grid(slots);
val result = fillMask(rng, slots, grid.lo, grid.hi, grid.g);
if (result.ok()) {
successCount++;
}
totalNodes += result.nodes();
totalBacktracks += result.backtracks();
}
var t1 = System.currentTimeMillis();
var totalDuration = (t1 - t0) / 1000.0;
System.out.printf(Locale.ROOT, "[DEBUG_LOG] %s: %d/%d SUCCESS | avg nodes=%d | avg backtracks=%d | total time=%.3fs%n",
label, successCount, iterations, totalNodes / iterations, totalBacktracks / iterations, totalDuration);
visualizeSlots(slots);
}
private void visualizeSlots(Slotinfo[] slots) {
var R = Masker.R;
var C = Masker.C;
var display = new char[R][C];
for (var r = 0; r < R; r++) Arrays.fill(display[r], ' ');
for (var slot : slots) {
var key = slot.key();
var dir = Riddle.Clue.from(Masker.Slot.dir(key));
var clueIdx = Masker.Slot.clueIndex(key);
var cr = Masker.IT[clueIdx].r();
var cc = Masker.IT[clueIdx].c();
// User requested: aAAAA for a four letter to RIGHT clue slot.
// SwedishGenerator: 1=RIGHT, 0=DOWN, 2=UP, 3=LEFT
var clueChar = dir.clueChar;
var slotChar = dir.slotChar;
display[cr][cc] = clueChar;
Riddle.cellWalk(slot.key(), slot.lo(), slot.hi())
.skip(1)
.forEach(idx -> {
var r = Masker.IT[idx].r();
var c = Masker.IT[idx].c();
if (display[r][c] == ' ' || (display[r][c] >= 'A' && display[r][c] <= 'D')) {
if (display[r][c] != ' ' && display[r][c] != slotChar) {
display[r][c] = '+'; // Intersection
} else {
display[r][c] = slotChar;
}
}
});
}
for (var r = 0; r < R; r++) {
System.out.println("[DEBUG_LOG] " + new String(display[r]));
}
}
}

View File

@@ -1,15 +1,14 @@
package puzzle;
import java.sql.*;
import java.util.Map;
import java.util.function.ToIntFunction;
import module java.base;
import module java.sql;
public final class HintScores {
public final class ScoreHintsTask {
public static void main(String[] args) throws Exception {
static void main(String[] args) throws Exception {
Class.forName("org.sqlite.JDBC");
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:/home/mike/dev/puzzle-generator/tools/hint/hint.sqlite")) {
updateCrossScores(conn, HintScores::crossabilityScore, 1000);
try (Connection conn = DriverManager.getConnection("jdbc:sqlite:tools/hint/hint.sqlite")) {
updateCrossScores(conn, ScoreHintsTask::crossabilityScore, 1000);
}
}
static final Map<Character, Integer> LETTER_WEIGHT = Map.ofEntries(
@@ -82,6 +81,7 @@ public final class HintScores {
try {
score = scoreFn.applyAsInt(word);
} catch (RuntimeException ex) {
ex.printStackTrace();
// If scoring fails, decide your policy: skip or set 0.
// Here: skip row.
continue;

View File

@@ -1,89 +1,170 @@
package puzzle;
import module java.base;
import anno.GenerateNeighbor;
import anno.GenerateNeighbors;
import anno.LemmaGen;
import lombok.val;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import puzzle.Export.IntListDTO;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import static org.junit.jupiter.api.Assertions.*;
import static puzzle.SwedishGenerator.*;
import precomp.Neighbors9x8;
import puzzle.DictJavaGeneratorMulti.DictEntryDTO.IntListDTO;
import precomp.Mask;
import puzzle.Export.Puzzle;
import puzzle.Riddle.Signa;
import puzzle.Masker.Slot;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static precomp.Const9x8.CLUE_DOWN0;
import static precomp.Const9x8.CLUE_LEFT3;
import static precomp.Const9x8.CLUE_RIGHT1;
import static precomp.Const9x8.CLUE_UP2;
import static precomp.Const9x8.Cell.r0c0;
import static precomp.Const9x8.Cell.r0c0d0;
import static precomp.Const9x8.Cell.r0c0d1;
import static precomp.Const9x8.Cell.r0c0d3;
import static precomp.Const9x8.Cell.r0c1;
import static precomp.Const9x8.Cell.r0c2;
import static precomp.Const9x8.Cell.r0c3;
import static precomp.Const9x8.Cell.r2c0;
import static precomp.Const9x8.Cell.r2c3d0;
import static precomp.Const9x8.Cell.r2c5;
import static precomp.Const9x8.Cell.r3c5;
import static precomp.Const9x8.Cell.r4c5;
import static precomp.Const9x8.LETTER_A;
import static precomp.Const9x8.LETTER_B;
import static precomp.Const9x8.LETTER_C;
import static precomp.Const9x8.LETTER_E;
import static precomp.Const9x8.LETTER_I;
import static precomp.Const9x8.LETTER_N;
import static precomp.Const9x8.LETTER_R;
import static precomp.Const9x8.LETTER_X;
import static precomp.Const9x8.LETTER_Z;
import static precomp.Const9x8.OFF_0_0;
import static precomp.Const9x8.OFF_0_1;
import static precomp.Const9x8.OFF_0_2;
import static precomp.Const9x8.OFF_0_3;
import static precomp.Const9x8.OFF_2_3;
import static puzzle.LemmaData.ABC;
import static puzzle.LemmaData.ABD;
import static puzzle.LemmaData.APPLE;
import static puzzle.LemmaData.APPLY;
import static puzzle.LemmaData.AT;
import static puzzle.LemmaData.AZ;
import static puzzle.LemmaData.BANAN;
import static puzzle.LemmaData.BANANA;
import static puzzle.LemmaData.BANANAS;
import static puzzle.LemmaData.BANANASS;
import static puzzle.LemmaData.CAT;
import static puzzle.LemmaData.DOGS;
import static puzzle.LemmaData.EXE;
import static puzzle.LemmaData.IN;
import static puzzle.LemmaData.INE;
import static puzzle.LemmaData.INER;
import static puzzle.LemmaData.INEREN;
import static puzzle.LemmaData.INERENA;
import static puzzle.LemmaData.INERENAE;
import static puzzle.LemmaData.WORD_A;
import static puzzle.LemmaData.WORD_C;
import static puzzle.LemmaData.WORD_X;
import static puzzle.SwedishGenerator.Grid;
import static puzzle.SwedishGenerator.Lemma;
import static puzzle.SwedishGenerator.Rng;
import static puzzle.SwedishGenerator.Slotinfo;
import static puzzle.SwedishGenerator.X;
import static puzzle.SwedishGenerator.candidateCountForPattern;
import static puzzle.SwedishGenerator.candidateInfoForPattern;
import static puzzle.SwedishGenerator.patternForSlot;
@GenerateNeighbors(@GenerateNeighbor(C = 3, R = 4, packageName = "precomp", className = "Neighbors3x4", MIN_LEN = 2))
@LemmaGen(
packageName = "puzzle",
className = "LemmaData",
words = {
"EEN", "NAAR", "IEDEREEN", "A", "C", "X", "TEST", "IN", "INE", "INER", "INEREN", "INERENA", "INERENAE",
"APPLE", "AXE", "ABC", "ABD", "AZ", "AB",
"AT", "CAT", "DOGS", "APPLY", "BANAN", "BANANA", "BANANAS", "BANANASS"
},
minLen = 2,
maxLen = 8
)
public class SwedishGeneratorTest {
static Grid createEmpty9x8() { return new Grid(new byte[Neighbors9x8.SIZE], X, X); }
record Context(long[] bitset) {
public Context() { this(new long[2500]); }
private static final ThreadLocal<Context> CTX = ThreadLocal.withInitial(Context::new);
public static Context get() { return CTX.get(); }
}
static final long[] WORDS = new long[]{
AT,
CAT,
DOGS,
APPLE,
APPLY,
BANAN,
BANANA,
BANANAS,
BANANASS
};
static final long[] WORDS2 = new long[]{ IN,
APPLE,
APPLY,
BANAN,
INE,
INER,
INEREN,
INERENA,
INERENAE };
@Test
void testPatternForSlotAllLetters() {
var grid = new Grid(new byte[]{ 65, 66, 67 }); // A B C
var slot = Slot.from(18, ((long) 0) | ((long) 1 << 7) | ((long) 2 << 14), 3);
long pattern = patternForSlot(grid, slot);
assertEquals(1 | (2 << 5) | (3 << 10), pattern);
var grid = new Puzzle(Riddle.Signa.of(r0c0d1));
GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c1.or(r0c2).or(r0c3).lo(), X, ABC);
val map = grid.sync().collect(Collectors.toMap(Mask::index, Mask::d));
assertEquals(LETTER_A, map.get(OFF_0_1));
assertEquals(LETTER_B, map.get(OFF_0_2));
assertEquals(LETTER_C, map.get(OFF_0_3));
}
@Test
void testPatternForSlotMixed() {
var grid = new Grid(new byte[]{ 65, DASH, 67 }); // A - C
var slot = Slot.from(1 << 4 | 2, ((long) 0) | ((long) 1 << 7) | ((long) 2 << 14), 3);
long pattern = patternForSlot(grid, slot);
assertEquals(1 | (0 << 5) | (3 << 10), pattern);
var grid = createEmpty9x8();
GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r0c0.mask, X, WORD_A);
GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r2c0.mask, X, WORD_C);
var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X);
assertEquals(14081L, pattern);
}
@Test
void testPatternForSlotAllDashes() {
var grid = new Grid(new byte[]{ DASH, DASH, DASH }); // - - -
var slot = Slot.from(1 << 4 | 2, ((long) 0) | ((long) 1 << 7) | ((long) 2 << 14), 3);
long pattern = patternForSlot(grid, slot);
assertEquals(0, pattern);
var grid = createEmpty9x8();
var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X);
assertEquals(X, pattern);
}
@Test
void testPatternForSlotSingleLetter() {
var grid = new Grid(new byte[]{ 65, DASH, DASH }); // A - -
var slot = Slot.from(1 << 4 | 2, ((long) 0) | ((long) 1 << 7) | ((long) 2 << 14), 3);
long pattern = patternForSlot(grid, slot);
assertEquals(1, pattern);
}
@Test
void testRng() {
var rng = new Rng(123);
var val1 = rng.nextU32();
var val2 = rng.nextU32();
assertNotEquals(val1, val2);
var rng2 = new Rng(123);
assertEquals(val1, rng2.nextU32());
for (var i = 0; i < 100; i++) {
var r = rng.randint(5, 10);
assertTrue(r >= 5 && r <= 10);
var f = rng.nextFloat();
assertTrue(f >= 0.0 && f <= 1.0);
}
var grid = createEmpty9x8();
GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r0c0.mask, X, WORD_A);
var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X);
assertEquals(1L, pattern);
}
@Test
void testGrid() {
var grid = Grid.createEmpty();
grid.setByteAt(0, (byte) 'A');
grid.setClue(Grid.offset(0, 1), (byte) '1');
assertEquals('A', grid.byteAt(0));
assertEquals(1, grid.digitAt(Grid.offset(0, 1)));
assertTrue(grid.isLetterAt(0));
assertFalse(grid.isDigitAt(0));
assertTrue(grid.isDigitAt(Grid.offset(0, 1)));
assertFalse(grid.isLetterAt(Grid.offset(0, 1)));
assertTrue(grid.isLetterAt(0));
assertFalse(grid.isLetterAt(Grid.offset(0, 1)));
var copy = grid.deepCopyGrid();
assertEquals('A', copy.byteAt(0));
copy.setByteAt(0, (byte) 'B');
assertEquals('B', copy.byteAt(0));
assertEquals('A', grid.byteAt(0));
var p = new Puzzle(Clues.createEmpty());
GridBuilder.placeWord(p.grid(), p.grid().g, r0c0d1.slotKey, r0c0d0.mask, X, WORD_A);
val arr = p.sync().collect(Collectors.toMap(Mask::index, Mask::d));
assertEquals(1, arr.size());
assertEquals(LETTER_A, arr.get(OFF_0_0));
}
@Test
@@ -100,231 +181,226 @@ public class SwedishGeneratorTest {
@Test
void testLemmaAndDict() {
var l2a = new Lemma("IN");
var l4a = new Lemma("INER");
var l6a = new Lemma("INEREN");
var l7a = new Lemma("INERENA");
var l8a = new Lemma("INERENAE");
Assertions.assertEquals(Lemma.packShiftIn("APPLE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(APPLE));
assertEquals(4, Lemma.unpackSize(APPLE));
assertEquals(LETTER_I, Lemma.byteAt(INERENAE, 0));
assertEquals(LETTER_N, Lemma.byteAt(INERENAE, 1));
assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 2));
assertEquals(LETTER_R, Lemma.byteAt(INERENAE, 3));
assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 4));
assertEquals(LETTER_N, Lemma.byteAt(INERENAE, 5));
assertEquals(LETTER_A, Lemma.byteAt(INERENAE, 6));
assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 7));
var l1 = new Lemma("APPLE");
Assertions.assertEquals(Lemma.pack("APPLE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(l1.word()));
assertEquals(5, Lemma.length(l1.word()));
assertEquals((byte) 'A', l1.byteAt(0));
assertEquals(1, l1.intAt(0));
var l2 = new Lemma("AXE");
var dict = new Dict(new Lemma[]{ l1, l2, l2a, l4a, l6a, l7a, l8a });
var dict = DictJavaGeneratorMulti.Dicts.makeDict(new long[]{ APPLE, EXE, IN, INER, INEREN, INERENA, INERENAE });
assertEquals(1, dict.index()[3].words().length);
assertEquals(1, dict.index()[5].words().length);
var entry3 = dict.index()[3];
assertEquals(1, entry3.words().length);
assertEquals(Lemma.pack("AXE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(entry3.words()[0]));
// Check pos indexing
// AXE: A at 0, X at 1, E at 2
/* assertTrue(entry3.pos()[0][0].size() > 0);
assertTrue(entry3.pos()[1]['X' - 'A'].size() > 0);
assertTrue(entry3.pos()[2]['E' - 'A'].size() > 0);*/
assertEquals(Lemma.packShiftIn("AXE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(entry3.words()[0]));
}
@Test
void testSlot() {
System.out.println("[DEBUG_LOG] Slot.BIT_FOR_DIR = " + Slot.BIT_FOR_DIR);
// key = (r << 8) | (c << 4) | d
var offset = Grid.offset(2, 3);
System.out.println("[DEBUG_LOG] Grid.offset(2, 3) = " + offset);
var key = (offset << Slot.BIT_FOR_DIR) | 5;
System.out.println("[DEBUG_LOG] key = " + key);
long packedPos = 0;
// pos 0: (2, 5)
packedPos |= Grid.offset(2, 5);
// pos 1: (3, 5)
packedPos |= (long) Grid.offset(3, 5) << 7;
// pos 2: (4, 5)
packedPos |= (long) Grid.offset(4, 5) << 14;
assertEquals(OFF_2_3, Slot.clueIndex(r2c3d0.slotKey));
assertEquals(CLUE_DOWN0, Slot.dir(r2c3d0.slotKey));
assertFalse(Slot.horiz(r2c3d0.slotKey));
var cells = Riddle.cellWalk(r2c3d0.slotKey, r2c5.or(r3c5).or(r4c5).lo(), 0L).mapToObj(i -> Masker.IT[i]).toArray(rci[]::new);
assertEquals(2, cells[1].r());
assertEquals(5, cells[1].c());
assertEquals(3, cells[2].r());
assertEquals(5, cells[2].c());
assertEquals(4, cells[3].r());
assertEquals(5, cells[3].c());
var s = Slot.from(key, packedPos, 3);
System.out.println("[DEBUG_LOG] s.dir() = " + s.dir());
assertEquals(2, s.clueR());
assertEquals(3, s.clueC());
assertEquals(5, s.dir());
assertFalse(s.horiz());
assertEquals(2, Grid.r(s.pos(0)));
assertEquals(3, Grid.r(s.pos(1)));
assertEquals(4, Grid.r(s.pos(2)));
assertEquals(5, Grid.c(s.pos(0)));
assertEquals(5, Grid.c(s.pos(1)));
assertEquals(5, Grid.c(s.pos(2)));
assertTrue(Slot.horiz(2)); // right
assertFalse(Slot.horiz(3)); // down
assertTrue(Slot.horiz(CLUE_RIGHT1)); // right
assertFalse(Slot.horiz(CLUE_DOWN0)); // down
}
@Test
void testIntersectSorted() {
var buff = new int[10];
var a = new int[]{ 1, 3, 5, 7, 9 };
var b = new int[]{ 2, 3, 6, 7, 10 };
var count = intersectSorted(a, a.length, b, b.length, buff);
assertEquals(2, count);
assertEquals(3, buff[0]);
assertEquals(7, buff[1]);
var c = new int[]{ 1, 2, 3 };
var d = new int[]{ 4, 5, 6 };
count = intersectSorted(c, c.length, d, d.length, buff);
assertEquals(0, count);
static long packPattern(String s) {
long p = 0;
var b = s.getBytes(StandardCharsets.US_ASCII);
for (var i = 0; i < b.length; i++) {
var val = b[i] & 31;
if (val != 0) {
p |= (i * 26L + val) << (i << 3);
}
}
return p;
}
@Test
void testCandidateInfoForPattern() {
var l0 = new Lemma("IN");
var l3a = new Lemma("INE");
var l4a = new Lemma("INER");
var l6a = new Lemma("INEREN");
var l7a = new Lemma("INERENA");
var l8a = new Lemma("INERENAE");
var l1 = new Lemma("APPLE");
var l2 = new Lemma("APPLY");
var l3 = new Lemma("BANAN");
var dict = new Dict(new Lemma[]{ l0, l1, l2, l3, l3a, l4a, l6a, l7a, l8a });
var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS2);
// Pattern "APP--" for length 5
var context = new Context();
context.setPattern(Lemma.pack(new byte[]{ 'A', 'P', 'P', DASH, DASH }));
var info = candidateInfoForPattern(context, dict.index()[5], 5);
var info = candidateInfoForPattern(Context.get().bitset(), packPattern("APP"), dict.index()[5].posBitsets(), dict.index()[5].numlong());
assertEquals(2, info.count());
assertNotNull(info.indices());
assertEquals(2, info.length);
assertNotNull(info);
}
@Test
void testForEachSlotAndExtractSlots() {
var gen = new SwedishGenerator(new Rng(0));
var grid = Grid.createEmpty();
// 3x3 grid (Config.PUZZLE_ROWS/COLS are 3 in test env)
// Set '2' (right) at 0,0
grid.setClue(0, (byte) '2');
// This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2)
var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS2);
var slots = Masker.extractSlots(Riddle.Signa.of(r0c0d1).c(), dict.index(), dict.reversed());
assertEquals(1, slots.length);
var s = slots[0];
var slots = extractSlots(grid);
// Depending on MAX_WORD_LENGTH and grid size.
// In 3x3, if we have '2' at 0,0, rr=0, cc=1.
// while loop:
// 1. rr=0, cc=1, n=0 -> packedRs |= 0, packedCs |= 1, n=1, rr=0, cc=2
// 2. rr=0, cc=2, n=1 -> packedRs |= 0, packedCs |= 2<<4, n=2, rr=0, cc=3 (out)
// result: Slot with len 2.
assertEquals(1, slots.size());
var s = slots.getFirst();
// MAX_WORD_LENGTH = Math.min(W, H). In tests with -DPUZZLE_ROWS=3 -DPUZZLE_COLS=3, it should be 3.
// However, the test run might be using default Config values if not properly overridden in the test environment.
// If Actual was 8, it means MAX_WORD_LENGTH was at least 8.
assertTrue(s.len() >= 2);
assertEquals(0, s.clueR());
assertEquals(0, s.clueC());
assertEquals(2, s.dir());
assertTrue(Slot.length(s.lo(), s.hi()) >= 2);
assertEquals(OFF_0_0, Slot.clueIndex(s.key()));
assertEquals(CLUE_RIGHT1, Slot.dir(s.key()));
}
@Test
void testMaskFitnessBasic() {
var gen = new SwedishGenerator(new Rng(0));
var grid = Grid.createEmpty();
var lenCounts = new int[12];
lenCounts[2] = 10;
lenCounts[8] = 10; // In case MAX_WORD_LENGTH is 8
var gen = new Masker(new Rng(0), new int[Masker.STACK_SIZE], Clues.createEmpty());
var grid = Clues.createEmpty();
// Empty grid should have high penalty (no slots)
var f1 = gen.maskFitness(grid);
var f1 = gen.maskFitness(grid, 18);
assertTrue(f1 >= 1_000_000_000L);
// Add a slot
grid.setClue(0, OFFSETS[2].dbyte());
var f2 = gen.maskFitness(grid);
grid.setClue(r0c0d1);
var f2 = gen.maskFitness(grid, 18);
assertTrue(f2 < f1);
}
@Test
void testGeneticAlgorithmComponents() {
var rng = new Rng(42);
var gen = new SwedishGenerator(rng);
var g1 = gen.randomMask();
assertNotNull(g1);
var g2 = gen.mutate(g1);
var gen = new Masker(new Rng(42), new int[Masker.STACK_SIZE], Clues.createEmpty());
var c1 = new Signa(gen.randomMask(18));
assertNotNull(c1);
var g2 = new Signa(gen.mutate(c1.deepCopyGrid().c()));
assertNotNull(g2);
assertNotSame(g1, g2);
var g3 = gen.crossover(g1, g2);
assertNotNull(g3);
var lenCounts = new int[12];
Arrays.fill(lenCounts, 10);
var g4 = gen.hillclimb(g1, 10);
assertNotNull(g4);
assertNotSame(c1.c(), g2.c());
assertNotNull(gen.crossover(c1.c(), g2.c()));
assertNotNull(gen.hillclimb(c1.c(), 18, 10));
}
@Test
void testPlaceWord() {
var grid = Grid.createEmpty();
// Slot at (0,0) length 3, horizontal (right)
// key = (r << 8) | (c << 4) | d. Here we just need a valid slot for placeWord.
// r(i) and c(i) are used by placeWord.
var packedPos = ((long) Grid.offset(0, 0)) | (((long) Grid.offset(0, 1)) << 7) | (((long) Grid.offset(0, 2)) << 14);
var s = Slot.from(0, packedPos, 3);
var w1 = new Lemma("ABC").word();
var undoBuffer = new int[10];
var grid = new Puzzle(Clues.createEmpty());
assertTrue(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC));
// 1. Successful placement in empty grid
assertTrue(placeWord(grid, s, w1, undoBuffer, 0));
assertEquals('A', grid.byteAt(0));
assertEquals('B', grid.byteAt(Grid.offset(0, 1)));
assertEquals('C', grid.byteAt(Grid.offset(0, 2)));
assertEquals(0b111L, undoBuffer[0]);
var map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d));
assertEquals(3, map.size());
assertEquals(LETTER_A, map.get(OFF_0_0));
assertEquals(LETTER_B, map.get(OFF_0_1));
assertEquals(LETTER_C, map.get(OFF_0_2));
// 2. Successful placement with partial overlap (same characters)
assertTrue(placeWord(grid, s, w1, undoBuffer, 1));
assertEquals(0L, undoBuffer[1]); // 0 new characters placed
assertTrue(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC));
// 3. Conflict: place "ABD" where "ABC" is
var w2 = new Lemma("ABD").word();
assertFalse(placeWord(grid, s, w2, undoBuffer, 2));
assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABD));
// Verify grid is unchanged (still "ABC")
assertEquals('A', grid.byteAt(Grid.offset(0, 0)));
assertEquals('B', grid.byteAt(Grid.offset(0, 1)));
assertEquals('C', grid.byteAt(Grid.offset(0, 2)));
map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d));
assertEquals(3, map.size());
assertEquals(LETTER_A, map.get(OFF_0_0));
assertEquals(LETTER_B, map.get(OFF_0_1));
assertEquals(LETTER_C, map.get(OFF_0_2));
// 4. Partial placement then conflict (rollback)
grid = Grid.createEmpty();
grid.setByteAt(Grid.offset(0, 2), (byte) 'X'); // Conflict at the end
assertFalse(placeWord(grid, s, w1, undoBuffer, 3));
// Verify grid is still empty (except for 'X')
assertEquals(DASH, grid.byteAt(Grid.offset(0, 0)));
assertEquals(DASH, grid.byteAt(Grid.offset(0, 1)));
assertEquals('X', grid.byteAt(Grid.offset(0, 2)));
grid = new Puzzle(Clues.createEmpty());
GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, 1L << OFF_0_2, 0, WORD_X); // Conflict at the end
assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC));
map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d));
assertEquals(1, map.size());
assertEquals(LETTER_X, map.get(OFF_0_2));
}
@Test
void testBacktrackingHelpers() {
var grid = Grid.createEmpty();
var grid = new Puzzle(Clues.createEmpty());
// Slot at 0,1 length 2
var packedPos = ((long) Grid.offset(0, 1)) | (((long) Grid.offset(0, 2)) << 7);
var s = Slot.from((0 << 8) | (1 << 4) | 2, packedPos, 2);
var w = new Lemma("AZ").word();
var undoBuffer = new int[10];
var placed = placeWord(grid, s, w, undoBuffer, 0);
val low = grid.grid().lo;
val top = grid.grid().hi;
var placed = GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c1.or(r0c2).lo(), X, AZ);
assertTrue(placed);
assertEquals('A', grid.byteAt(Grid.offset(0, 1)));
assertEquals('Z', grid.byteAt(Grid.offset(0, 2)));
assertEquals(0b11L, undoBuffer[0]);
s.undoPlace(grid, undoBuffer[0]);
assertEquals(DASH, grid.byteAt(Grid.offset(0, 1)));
assertEquals(DASH, grid.byteAt(Grid.offset(0, 2)));
var map = new Puzzle(grid.grid(), grid.cl()).collect(Collectors.toMap(Mask::index, Mask::d));
assertEquals(2, map.size());
assertEquals(LETTER_A, map.get(OFF_0_1));
assertEquals(LETTER_Z, map.get(OFF_0_2));
grid.grid().hi = top;
grid.grid().lo = low;
map = grid.collect(Collectors.toMap(Mask::index, Mask::d));
assertEquals(0, map.size());
assertFalse(map.containsKey(OFF_0_1));
assertFalse(map.containsKey(OFF_0_2));
}
@Test
void testInnerWorkings() {
// 1. Test Slot.increasing
assertFalse(Slotinfo.increasing(CLUE_LEFT3)); // Left
assertTrue(Slotinfo.increasing(CLUE_RIGHT1)); // Right
assertTrue(Slotinfo.increasing(CLUE_DOWN0)); // Down
assertFalse(Slotinfo.increasing(CLUE_UP2)); // Up
assertTrue(Slotinfo.increasing(r0c0d1.slotKey));
assertFalse(Slotinfo.increasing(r0c0d3.slotKey));
// 2. Test slotScore
val counts = new byte[Neighbors9x8.SIZE];
counts[1] = 2;
counts[2] = 3;
var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS);
var entry5 = dict.index()[5];
// cross = (counts[1]-1) + (counts[2]-1) = 1 + 2 = 3
// score = 3 * 10 + len(2) = 32
assertEquals(32, Masker.slotScore(counts, (1L << 1) | (1L << 2), 0L));
// 3. Test candidateCountForPattern
var ctx = Context.get();
var pattern = packPattern("APP");
assertEquals(2, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong()));
pattern = packPattern("BAN");
assertEquals(1, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong()));
pattern = packPattern("CAT");
assertEquals(0, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong()));
}
@Test
void testMaskFitnessDetailed() {
var gen = new Masker(new Rng(42), new int[Masker.STACK_SIZE], Clues.createEmpty());
var grid = Clues.createEmpty();
// Empty grid: huge penalty
var fitEmpty = gen.maskFitness(grid, 18);
assertTrue(fitEmpty >= 1_000_000_000L);
grid.setClue(r0c0d1); // Right from 0,0. Len 2 if 3x3.
var fitOne = gen.maskFitness(grid, 18);
assertTrue(fitOne < fitEmpty);
}
@Test
void testRng() {
var rng = new Rng(123);
var val1 = rng.nextU32();
var val2 = rng.nextU32();
assertNotEquals(val1, val2);
var rng2 = new Rng(123);
assertEquals(val1, rng2.nextU32());
for (var i = 0; i < 100; i++) {
var r = rng.randomClueDir();
assertTrue(r >= 0 && r <= 5);
var f = rng.nextFloat();
assertTrue(f >= 0.0 && f <= 1.0);
}
assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
}
}

View File

@@ -0,0 +1,47 @@
package puzzle;
import anno.ConstGen;
import lombok.val;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import puzzle.Riddle.Rewards;
import puzzle.Riddle.Signa;
import puzzle.SwedishGenerator.Rng;
import puzzle.dict800_4.DictData800_4;
import java.util.stream.Collectors;
import static precomp.Const3x4.Cell.r0c0d4;
import static precomp.Const3x4.Cell.r0c2d0;
import static precomp.Const3x4.Cell.r1c0d1;
import static precomp.Const3x4.Cell.r2c0d1;
import static precomp.Const3x4.Cell.r3c0d1;
@ConstGen(C = 3, R = 4, packageName = "precomp", className = "Const3x4")
public class TestDuplication {
static void main() {
TestDuplication test = new TestDuplication();
test.testFiller2();
}
@Test
void testFiller2() {
var mask = Riddle.Signa.of(
r0c0d4, r0c2d0,
r1c0d1,
r2c0d1,
r3c0d1);
Assertions.assertEquals(5, mask.clueCount());
val map = mask.stream().collect(Collectors.toMap(Riddle.Vestigium::index, Riddle.Vestigium::clue));
Assertions.assertEquals(5, map.size());
var slots = Masker_Neighbors3x4.slots(mask.c(), DictData800_4.DICT800);
var grid = Masker_Neighbors3x4.grid(slots);
var filled = SwedishGenerator.fillMask(new Rng(1), slots, grid.lo, grid.hi, grid.g);
grid.lo = Masker_Neighbors3x4.MASK_LO & ~mask.c().lo;
grid.hi = Masker_Neighbors3x4.MASK_HI & ~mask.c().hi;
var grid1 = new ExportX_Const3x4.Puzzle(grid, mask.c());
var result = new ExportX_Const3x4.PuzzleResult(new Signa(mask.c()), grid1, slots, filled);
if (filled.ok()) {
System.out.println(filled);
val res = result.exportFormatFromFilled(new Rewards(0, 0, 0));
System.out.println(String.join("\n", res.grid()));
}
}
}

View File

@@ -1,26 +0,0 @@
package puzzle;
import puzzle.ThemePoolBuilderLength.Lexicon;
import java.nio.file.*;
import java.util.*;
public class TestSort {
public static void main(String[] args) throws Exception {
Lexicon lex = new Lexicon(
Arrays.asList("A", "B", "C"),
new HashMap<>(),
new int[]{10, 30, 20},
new BitSet[9]
);
BitSet bs = new BitSet();
bs.set(0); bs.set(1); bs.set(2);
Path p = Paths.get("test_pool.txt");
ThemePoolBuilderLength.writeWordList(p, lex, bs);
List<String> lines = Files.readAllLines(p);
System.out.println("Sorted words: " + lines);
if (lines.get(0).equals("B") && lines.get(1).equals("C") && lines.get(2).equals("A")) {
System.out.println("SUCCESS");
} else {
System.out.println("FAILURE");
System.exit(1);
}
}
}

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/generated-sources" isTestSource="false" generated="true" />
</content>
</component>
</module>