Compare commits
140 Commits
7e323040cb
...
20994d46d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20994d46d8 | ||
|
|
26cd6fb284 | ||
|
|
f61d04bb61 | ||
|
|
b8fc6581e1 | ||
|
|
3cc6570cdc | ||
|
|
2a5b70896e | ||
|
|
282ec56f19 | ||
|
|
d9b565301f | ||
|
|
061065c9a7 | ||
|
|
e9d787bbb9 | ||
|
|
7e34276726 | ||
|
|
73192f5905 | ||
|
|
fa3e8ef1ed | ||
|
|
c21ad114dc | ||
|
|
d0b64fac5f | ||
|
|
e5db5b8d96 | ||
|
|
7db439a9dc | ||
|
|
2aa08fedb0 | ||
|
|
2c39e82b00 | ||
|
|
4109c51cbe | ||
|
|
ed7cade1c7 | ||
|
|
4b61205bbb | ||
|
|
dc45ad45c9 | ||
|
|
2295a7d97c | ||
|
|
a659bd5162 | ||
|
|
1e13d39153 | ||
|
|
dd53009e69 | ||
|
|
ebcbc9b33c | ||
|
|
386777e576 | ||
|
|
f203f2106e | ||
|
|
92a736aa0a | ||
|
|
78f72a024e | ||
|
|
46b2bb04dc | ||
|
|
7f15ab8ff1 | ||
|
|
85fa317eec | ||
|
|
b66437bb70 | ||
|
|
ddce9addb5 | ||
|
|
dadde53f76 | ||
|
|
58b8b57688 | ||
|
|
8780a26451 | ||
|
|
5d0da1cf6b | ||
|
|
3cd17d170a | ||
|
|
e8e0344212 | ||
|
|
fde6f15b24 | ||
|
|
30ca46ac03 | ||
|
|
8b7827cfc2 | ||
|
|
a764f45041 | ||
|
|
47ead135d3 | ||
|
|
a1061f5eb9 | ||
|
|
ba90818b66 | ||
|
|
0dcfbebcb0 | ||
|
|
28f448d178 | ||
|
|
d1c448e1cb | ||
|
|
7e5e363a3e | ||
|
|
5678af332e | ||
|
|
5d186ae0ba | ||
|
|
1fa112ab65 | ||
|
|
37581d15b4 | ||
|
|
948730d7be | ||
|
|
b026ebfbd2 | ||
|
|
6daab5ef4e | ||
|
|
112c16c525 | ||
|
|
20617cda57 | ||
|
|
19812d81e5 | ||
|
|
938d2ac66b | ||
|
|
8ff9d661e3 | ||
|
|
9367833407 | ||
|
|
c76d463c8c | ||
|
|
bfa19ec585 | ||
|
|
9bd85c81a3 | ||
|
|
bd25f65194 | ||
|
|
9102dcb922 | ||
|
|
0c56fafeaa | ||
|
|
3bd7a0f958 | ||
|
|
47b33af09d | ||
|
|
81ea708345 | ||
|
|
d3c7128cf9 | ||
|
|
44f53801a3 | ||
|
|
57be64c37e | ||
|
|
a2134f0dce | ||
|
|
4585c1f2eb | ||
|
|
60b7509bf6 | ||
|
|
aceaa0fc18 | ||
|
|
5c7d1120db | ||
|
|
91722ecc60 | ||
|
|
ecbc408cce | ||
|
|
e8711b30a1 | ||
|
|
04e3844732 | ||
|
|
5849f543c5 | ||
|
|
70b2009723 | ||
|
|
75f599318a | ||
|
|
29aceb2180 | ||
|
|
b29831859b | ||
|
|
b1f54c2cae | ||
|
|
0b7a59b769 | ||
|
|
ecf4ae913e | ||
|
|
dfb4679da8 | ||
|
|
6afe675a9d | ||
|
|
8e049b3fa5 | ||
|
|
c1706e1bf7 | ||
|
|
66bd8193ef | ||
|
|
cb2935e0f5 | ||
|
|
69af69a8b8 | ||
|
|
1a56297986 | ||
|
|
afd4a7f53c | ||
|
|
bd52bd0ef0 | ||
|
|
6e2ecae082 | ||
|
|
aafa5d4d43 | ||
|
|
352e8c79ca | ||
|
|
bcad930bfc | ||
|
|
c529ce90c7 | ||
|
|
5d34893ef1 | ||
|
|
0405d11753 | ||
|
|
eeae90a886 | ||
|
|
19f235ae59 | ||
|
|
b0b10d356a | ||
|
|
107dfab0c7 | ||
|
|
1d731334d9 | ||
|
|
2430dbdfb9 | ||
|
|
6ab6791d9c | ||
|
|
81ae0aa84c | ||
|
|
6119722867 | ||
|
|
61d246e551 | ||
|
|
a9b4dfb422 | ||
|
|
88a61e6f4d | ||
|
|
8e7b29a2d3 | ||
|
|
4784fa7180 | ||
|
|
b3b1921414 | ||
|
|
368473c80f | ||
|
|
c7d67bf778 | ||
|
|
c2e94fb02e | ||
|
|
16676d633a | ||
|
|
a0862fcc43 | ||
|
|
b6351a6fb2 | ||
|
|
6ef007522c | ||
|
|
67eedb773b | ||
|
|
84e832df40 | ||
|
|
fdd1c76bae | ||
|
|
9c1be77d76 | ||
|
|
f8f1a67a61 |
@@ -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
|
||||
|
||||
@@ -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
97245
nl_score_hints_v4.csv
Normal file
File diff suppressed because it is too large
Load Diff
428
package-lock.json
generated
428
package-lock.json
generated
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
13
package.json
13
package.json
@@ -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
18
pom.xml
@@ -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>--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
|
||||
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
|
||||
<arg>--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
|
||||
<arg>--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>
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
28
src/main/java/precomp/Mask.java
Normal file
28
src/main/java/precomp/Mask.java
Normal 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); }
|
||||
}
|
||||
85
src/main/java/puzzle/Clues.java
Normal file
85
src/main/java/puzzle/Clues.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
732
src/main/java/puzzle/Masker.java
Normal file
732
src/main/java/puzzle/Masker.java
Normal 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; }
|
||||
}
|
||||
}
|
||||
125
src/main/java/puzzle/Meta.java
Normal file
125
src/main/java/puzzle/Meta.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
183
src/main/java/puzzle/Riddle.java
Normal file
183
src/main/java/puzzle/Riddle.java
Normal 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
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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("&", "&").replace("<", "<").replace(">", ">");
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 { }
|
||||
Binary file not shown.
3
src/main/java/puzzle/rci.java
Normal file
3
src/main/java/puzzle/rci.java
Normal 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) {}
|
||||
BIN
src/main/resources/shards/shard0.data
Normal file
BIN
src/main/resources/shards/shard0.data
Normal file
Binary file not shown.
BIN
src/main/resources/shards/shard0.map
Normal file
BIN
src/main/resources/shards/shard0.map
Normal file
Binary file not shown.
106
src/test/java/puzzle/DictJavaGeneratorMulti.java
Normal file
106
src/test/java/puzzle/DictJavaGeneratorMulti.java
Normal 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); }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
63
src/test/java/puzzle/GridBuilder.java
Normal file
63
src/test/java/puzzle/GridBuilder.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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')); }
|
||||
|
||||
}
|
||||
|
||||
477
src/test/java/puzzle/MarkerTest.java
Normal file
477
src/test/java/puzzle/MarkerTest.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
223
src/test/java/puzzle/PerformanceTest.java
Normal file
223
src/test/java/puzzle/PerformanceTest.java
Normal 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]));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
47
src/test/java/puzzle/TestDuplication.java
Normal file
47
src/test/java/puzzle/TestDuplication.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user