├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── sample_data ├── example.gif ├── problem_easy.txt ├── problem_hard.txt ├── readme_example.png └── solution_easy.txt ├── src ├── lib.rs └── main.rs └── www ├── .babelrc ├── .bin └── create-wasm-app.js ├── .gitignore ├── .travis.yml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── package-lock.json ├── package.json ├── public ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── bootstrap.js ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.tsx ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── serviceWorker.js ├── setupTests.js └── sudoku │ ├── SudokuTable.css │ ├── SudokuTable.tsx │ └── SudokuTableCore.tsx ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | 7 | INSTALL_NODE_VIA_NVM: &INSTALL_NODE_VIA_NVM 8 | | 9 | rustup target add wasm32-unknown-unknown 10 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.38.0/install.sh | bash 11 | source ~/.nvm/nvm.sh 12 | nvm install --lts 13 | 14 | install: 15 | - *INSTALL_NODE_VIA_NVM 16 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f 17 | 18 | matrix: 19 | allow_failures: 20 | - rust: nightly 21 | script: 22 | - cargo build --verbose --all 23 | - cargo test --verbose --all 24 | - wasm-pack build 25 | - cd ./www && npm install --verbose 26 | - npm run build 27 | notifications: 28 | email: 29 | on_success: never 30 | on_failure: always 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "(gdb) Launch", 9 | "type": "cppdbg", 10 | "request": "launch", 11 | "program": "target/debug/sudoku_solver", 12 | "args": ["assets/problem_hard.txt", "-v"], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceFolder}", 15 | "environment": [], 16 | "externalConsole": false, 17 | "MIMode": "gdb", 18 | "setupCommands": [ 19 | { 20 | "description": "Enable pretty-printing for gdb", 21 | "text": "-enable-pretty-printing", 22 | "ignoreFailures": true 23 | } 24 | ] 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.18" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.2" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 64 | dependencies = [ 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.6" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 73 | dependencies = [ 74 | "anstyle", 75 | "windows-sys", 76 | ] 77 | 78 | [[package]] 79 | name = "autocfg" 80 | version = "1.4.0" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 83 | 84 | [[package]] 85 | name = "bumpalo" 86 | version = "3.16.0" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 89 | 90 | [[package]] 91 | name = "cc" 92 | version = "1.2.7" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" 95 | dependencies = [ 96 | "shlex", 97 | ] 98 | 99 | [[package]] 100 | name = "cfg-if" 101 | version = "1.0.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 104 | 105 | [[package]] 106 | name = "chrono" 107 | version = "0.4.39" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 110 | dependencies = [ 111 | "android-tzdata", 112 | "iana-time-zone", 113 | "js-sys", 114 | "num-traits", 115 | "wasm-bindgen", 116 | "windows-targets", 117 | ] 118 | 119 | [[package]] 120 | name = "clap" 121 | version = "4.5.24" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd" 124 | dependencies = [ 125 | "clap_builder", 126 | ] 127 | 128 | [[package]] 129 | name = "clap_builder" 130 | version = "4.5.24" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd" 133 | dependencies = [ 134 | "anstream", 135 | "anstyle", 136 | "clap_lex", 137 | "strsim", 138 | ] 139 | 140 | [[package]] 141 | name = "clap_lex" 142 | version = "0.7.4" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 145 | 146 | [[package]] 147 | name = "colorchoice" 148 | version = "1.0.3" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 151 | 152 | [[package]] 153 | name = "core-foundation-sys" 154 | version = "0.8.7" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 157 | 158 | [[package]] 159 | name = "env_filter" 160 | version = "0.1.3" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 163 | dependencies = [ 164 | "log", 165 | "regex", 166 | ] 167 | 168 | [[package]] 169 | name = "env_logger" 170 | version = "0.11.6" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" 173 | dependencies = [ 174 | "anstream", 175 | "anstyle", 176 | "env_filter", 177 | "humantime", 178 | "log", 179 | ] 180 | 181 | [[package]] 182 | name = "humantime" 183 | version = "2.1.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" 186 | 187 | [[package]] 188 | name = "iana-time-zone" 189 | version = "0.1.61" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 192 | dependencies = [ 193 | "android_system_properties", 194 | "core-foundation-sys", 195 | "iana-time-zone-haiku", 196 | "js-sys", 197 | "wasm-bindgen", 198 | "windows-core", 199 | ] 200 | 201 | [[package]] 202 | name = "iana-time-zone-haiku" 203 | version = "0.1.2" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 206 | dependencies = [ 207 | "cc", 208 | ] 209 | 210 | [[package]] 211 | name = "is_terminal_polyfill" 212 | version = "1.70.1" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 215 | 216 | [[package]] 217 | name = "js-sys" 218 | version = "0.3.76" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" 221 | dependencies = [ 222 | "once_cell", 223 | "wasm-bindgen", 224 | ] 225 | 226 | [[package]] 227 | name = "libc" 228 | version = "0.2.169" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 231 | 232 | [[package]] 233 | name = "log" 234 | version = "0.4.22" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 237 | 238 | [[package]] 239 | name = "memchr" 240 | version = "2.7.4" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 243 | 244 | [[package]] 245 | name = "num-traits" 246 | version = "0.2.19" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 249 | dependencies = [ 250 | "autocfg", 251 | ] 252 | 253 | [[package]] 254 | name = "once_cell" 255 | version = "1.20.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 258 | 259 | [[package]] 260 | name = "proc-macro2" 261 | version = "1.0.92" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 264 | dependencies = [ 265 | "unicode-ident", 266 | ] 267 | 268 | [[package]] 269 | name = "quote" 270 | version = "1.0.38" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 273 | dependencies = [ 274 | "proc-macro2", 275 | ] 276 | 277 | [[package]] 278 | name = "regex" 279 | version = "1.11.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 282 | dependencies = [ 283 | "aho-corasick", 284 | "memchr", 285 | "regex-automata", 286 | "regex-syntax", 287 | ] 288 | 289 | [[package]] 290 | name = "regex-automata" 291 | version = "0.4.9" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 294 | dependencies = [ 295 | "aho-corasick", 296 | "memchr", 297 | "regex-syntax", 298 | ] 299 | 300 | [[package]] 301 | name = "regex-syntax" 302 | version = "0.8.5" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 305 | 306 | [[package]] 307 | name = "shlex" 308 | version = "1.3.0" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 311 | 312 | [[package]] 313 | name = "strsim" 314 | version = "0.11.1" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 317 | 318 | [[package]] 319 | name = "sudoku_solver" 320 | version = "0.1.0" 321 | dependencies = [ 322 | "chrono", 323 | "clap", 324 | "env_logger", 325 | "log", 326 | "regex", 327 | "wasm-bindgen", 328 | ] 329 | 330 | [[package]] 331 | name = "syn" 332 | version = "2.0.95" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" 335 | dependencies = [ 336 | "proc-macro2", 337 | "quote", 338 | "unicode-ident", 339 | ] 340 | 341 | [[package]] 342 | name = "unicode-ident" 343 | version = "1.0.14" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 346 | 347 | [[package]] 348 | name = "utf8parse" 349 | version = "0.2.2" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 352 | 353 | [[package]] 354 | name = "wasm-bindgen" 355 | version = "0.2.99" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" 358 | dependencies = [ 359 | "cfg-if", 360 | "once_cell", 361 | "wasm-bindgen-macro", 362 | ] 363 | 364 | [[package]] 365 | name = "wasm-bindgen-backend" 366 | version = "0.2.99" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" 369 | dependencies = [ 370 | "bumpalo", 371 | "log", 372 | "proc-macro2", 373 | "quote", 374 | "syn", 375 | "wasm-bindgen-shared", 376 | ] 377 | 378 | [[package]] 379 | name = "wasm-bindgen-macro" 380 | version = "0.2.99" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" 383 | dependencies = [ 384 | "quote", 385 | "wasm-bindgen-macro-support", 386 | ] 387 | 388 | [[package]] 389 | name = "wasm-bindgen-macro-support" 390 | version = "0.2.99" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" 393 | dependencies = [ 394 | "proc-macro2", 395 | "quote", 396 | "syn", 397 | "wasm-bindgen-backend", 398 | "wasm-bindgen-shared", 399 | ] 400 | 401 | [[package]] 402 | name = "wasm-bindgen-shared" 403 | version = "0.2.99" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" 406 | 407 | [[package]] 408 | name = "windows-core" 409 | version = "0.52.0" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 412 | dependencies = [ 413 | "windows-targets", 414 | ] 415 | 416 | [[package]] 417 | name = "windows-sys" 418 | version = "0.59.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 421 | dependencies = [ 422 | "windows-targets", 423 | ] 424 | 425 | [[package]] 426 | name = "windows-targets" 427 | version = "0.52.6" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 430 | dependencies = [ 431 | "windows_aarch64_gnullvm", 432 | "windows_aarch64_msvc", 433 | "windows_i686_gnu", 434 | "windows_i686_gnullvm", 435 | "windows_i686_msvc", 436 | "windows_x86_64_gnu", 437 | "windows_x86_64_gnullvm", 438 | "windows_x86_64_msvc", 439 | ] 440 | 441 | [[package]] 442 | name = "windows_aarch64_gnullvm" 443 | version = "0.52.6" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 446 | 447 | [[package]] 448 | name = "windows_aarch64_msvc" 449 | version = "0.52.6" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 452 | 453 | [[package]] 454 | name = "windows_i686_gnu" 455 | version = "0.52.6" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 458 | 459 | [[package]] 460 | name = "windows_i686_gnullvm" 461 | version = "0.52.6" 462 | source = "registry+https://github.com/rust-lang/crates.io-index" 463 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 464 | 465 | [[package]] 466 | name = "windows_i686_msvc" 467 | version = "0.52.6" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 470 | 471 | [[package]] 472 | name = "windows_x86_64_gnu" 473 | version = "0.52.6" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 476 | 477 | [[package]] 478 | name = "windows_x86_64_gnullvm" 479 | version = "0.52.6" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 482 | 483 | [[package]] 484 | name = "windows_x86_64_msvc" 485 | version = "0.52.6" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 488 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sudoku_solver" 3 | version = "0.1.0" 4 | authors = ["Stefan Andreas Baur "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type=["rlib", "cdylib"] 9 | name = "sudoku_solver" 10 | path = "src/lib.rs" 11 | 12 | [[bin]] 13 | name = "sudoku_solver_bin" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | clap = "4.5.24" 18 | log = "0.4.22" 19 | chrono = "0.4" 20 | env_logger = "0.11.6" 21 | regex = "1" 22 | wasm-bindgen = "0.2.99" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 baurst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sudoku Solver 2 | This is a solver for regular 9x9 Sudoku problems written in Rust, compiled to WebAssembly with corresponding React client. 3 | It uses backtracking in combination with constraint propagation to quickly and efficiently solve sudokus. 4 | 5 | Try it here: [Sudoku Solver](https://baurst.github.io/sudoku_solver/ "Sudoku Solver"). 6 | 7 | [![Build Status](https://travis-ci.com/baurst/sudoku_solver.svg?token=KGmoNyosUqTq92iqGZE9&branch=master)](https://travis-ci.com/baurst/sudoku_solver) 8 | 9 | 10 |

11 | Screencast 12 |

13 | 14 | 15 | ## Implementation Details 16 | The core sudoku solver itself is implemented using Rust. 17 | The Rust lib is cross-compiled to WebAssembly (using wasm-bindgen) and exported as JavaScript module (using wasm-pack). 18 | A React client is used as web interface to interact with the solver. 19 | 20 | 21 | ## Algorithmic Details 22 | We look at the sudoku problem as a grid of positions with many possible "candidate" entries. 23 | At each iteration, the most promising position (i.e. the one with the fewest candidates) for trial is selected, because in a position where there are two possible options, the solver has as 50% chance of being right, but when there are five options, there is only a 20% chance of it being right. 24 | Each time a new digit is inserted, the new constraints are propagated: Candidate digits from the corresponding row, column and cell are removed. 25 | As soon as a unresolvable conlfict is met, the solver reverts the latest step and chooses another option for the most promising candidate, removing the old candidate. 26 | This process is repeated until the sudoku is solved. 27 | 28 | 29 | ## Steps to run the React client in the browser 30 | 31 | ```bash 32 | sudo apt install git curl 33 | 34 | # install rust and wasm-pack 35 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 36 | rustup target add wasm32-unknown-unknown 37 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 38 | 39 | # install nvm (manager for node & npm, adapt version to latest!) 40 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.35.3/install.sh | bash 41 | nvm install node 42 | 43 | #clone repo 44 | git clone https://github.com/baurst/sudoku_solver.git 45 | cd sudoku_solver 46 | 47 | # build rust components 48 | wasm-pack build 49 | 50 | # build js components 51 | cd www 52 | npm install 53 | 54 | # start in dev mode 55 | npm start 56 | 57 | # deploy to github pages 58 | npm run build 59 | npm run deploy 60 | ``` 61 | 62 | 63 | ## Building for standalone usage of the solver 64 | In order to use the solver without the web front end, the user must provide an input file containing one or more sudoku problems. 65 | Problems can be either comma separated or without separator. 66 | The convention is to use one problem per line, empty fields can be represented either by zero or any other non-numeric character except for ",". 67 | 68 | ```bash 69 | git clone https://github.com/baurst/sudoku_solver.git 70 | cd sudoku_solver 71 | cargo run --release -- sample_data/problem_hard.txt 72 | # or in case you want to trace the steps the solver is taking: add -v 73 | cargo run --release -- -v sample_data/problem_hard.txt 74 | ``` 75 | -------------------------------------------------------------------------------- /sample_data/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/sample_data/example.gif -------------------------------------------------------------------------------- /sample_data/problem_easy.txt: -------------------------------------------------------------------------------- 1 | 503070190000006750047190600400038000950200300000010072000804001300001860086720005 2 | -------------------------------------------------------------------------------- /sample_data/problem_hard.txt: -------------------------------------------------------------------------------- 1 | 100007860007008010800200009000000002400010000009005000608000000000050900000009304 2 | -------------------------------------------------------------------------------- /sample_data/readme_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/sample_data/readme_example.png -------------------------------------------------------------------------------- /sample_data/solution_easy.txt: -------------------------------------------------------------------------------- 1 | 563472198219386754847195623472638519951247386638519472795864231324951867186723945 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | #[macro_use] 3 | extern crate log; 4 | extern crate chrono; 5 | extern crate clap; 6 | extern crate env_logger; 7 | extern crate regex; 8 | 9 | use regex::Regex; 10 | use std::collections::HashSet; 11 | use std::fs; 12 | use std::hash::Hash; 13 | 14 | #[wasm_bindgen] 15 | pub fn wasm_solve_sudoku(input_str: &str) -> String { 16 | let input_str = input_str.trim(); 17 | assert_eq!(input_str.len(), 81, "Incorrect length of input string!"); 18 | let sudoku_problem = SudokuCandidates::from_vec(convert_problem_str(input_str)); 19 | let solution_opt = solve_sudoku(sudoku_problem, 0); 20 | if let Some(solution) = solution_opt { 21 | solution.to_continuous_string() 22 | } else { 23 | println!("Failed to solve {}", input_str); 24 | input_str.to_owned() 25 | } 26 | } 27 | 28 | #[wasm_bindgen] 29 | pub fn wasm_sudoku_contains_conflicts(input_str: &str) -> bool { 30 | let input_str = input_str.trim(); 31 | assert_eq!(input_str.len(), 81, "Incorrect length of input string!"); 32 | let sudoku_problem = SudokuCandidates::from_vec(convert_problem_str(input_str)); 33 | if let Some(prob) = sudoku_problem { 34 | prob.has_unresolvable_conflicts() 35 | } else { 36 | true 37 | } 38 | } 39 | 40 | #[derive(Clone, Debug)] 41 | pub struct SudokuCandidates { 42 | grid: Vec>>, 43 | } 44 | 45 | fn convert_problem_str(problem_raw_in: &str) -> Vec { 46 | problem_raw_in 47 | .split("") 48 | .map(|s| s.trim()) 49 | .filter(|s| !s.is_empty()) 50 | .map(|s| s.parse().unwrap()) 51 | .collect() 52 | } 53 | 54 | impl SudokuCandidates { 55 | fn initial() -> SudokuCandidates { 56 | SudokuCandidates { 57 | // 9 x 9 grid of u8s 58 | grid: vec![vec![(1..10).collect::>(); 9]; 9], 59 | } 60 | } 61 | 62 | fn from_vec(numbers: Vec) -> Option { 63 | assert!(numbers.len() == 81); 64 | let mut problem = SudokuCandidates::initial(); 65 | 66 | for (i, item) in numbers.iter().enumerate() { 67 | if *item == 0_u8 { 68 | continue; 69 | } 70 | let row_idx = i / 9; 71 | let col_idx = i % 9; 72 | 73 | problem.grid[row_idx][col_idx] = vec![*item]; 74 | let prob_tmp_opt = remove_duplicates(&problem, row_idx, col_idx, *item); 75 | 76 | if let Some(prob) = prob_tmp_opt { 77 | problem = prob; 78 | } else { 79 | return None; 80 | } 81 | } 82 | Some(problem) 83 | } 84 | 85 | fn to_continuous_string(&self) -> String { 86 | let mut res_str: String = "".to_owned(); 87 | for row_idx in 0..9 { 88 | for col_idx in 0..9 { 89 | let el = self.grid[row_idx][col_idx][0].to_string(); 90 | res_str += ⪙ 91 | } 92 | } 93 | res_str 94 | } 95 | 96 | fn get_duplicates_in_column( 97 | &self, 98 | element: u8, 99 | el_row_idx: usize, 100 | el_col_idx: usize, 101 | ) -> HashSet<(usize, usize)> { 102 | let mut duplicates: HashSet<(usize, usize)> = HashSet::new(); 103 | // find duplicates in column 104 | for row_idx in 0..9 { 105 | if row_idx == el_row_idx { 106 | continue; 107 | } 108 | if self.grid[row_idx][el_col_idx].iter().any(|x| *x == element) { 109 | duplicates.insert((row_idx, el_col_idx)); 110 | } 111 | } 112 | duplicates 113 | } 114 | 115 | fn get_duplicates_in_row( 116 | &self, 117 | element: u8, 118 | el_row_idx: usize, 119 | el_col_idx: usize, 120 | ) -> HashSet<(usize, usize)> { 121 | let mut duplicates: HashSet<(usize, usize)> = HashSet::new(); 122 | // find duplicates in row 123 | for col_idx in 0..9 { 124 | if col_idx == el_col_idx { 125 | continue; 126 | } 127 | if self.grid[el_row_idx][col_idx].iter().any(|x| *x == element) { 128 | duplicates.insert((el_row_idx, col_idx)); 129 | } 130 | } 131 | duplicates 132 | } 133 | 134 | fn get_duplicates_in_cell( 135 | &self, 136 | element: u8, 137 | el_row_idx: usize, 138 | el_col_idx: usize, 139 | ) -> HashSet<(usize, usize)> { 140 | let mut duplicates: HashSet<(usize, usize)> = HashSet::new(); 141 | // find duplicates in cell 142 | let cell_row_idx = el_row_idx / 3; 143 | let cell_col_idx = el_col_idx / 3; 144 | 145 | for row_idx_in_cell in 0..3 { 146 | for col_idx_in_cell in 0..3 { 147 | let row_idx = cell_row_idx * 3 + row_idx_in_cell; 148 | let col_idx = cell_col_idx * 3 + col_idx_in_cell; 149 | if row_idx == el_row_idx && col_idx == el_col_idx { 150 | continue; 151 | } 152 | if self.grid[row_idx][col_idx].iter().any(|x| *x == element) { 153 | duplicates.insert((row_idx, col_idx)); 154 | } 155 | } 156 | } 157 | duplicates 158 | } 159 | 160 | fn get_duplicates( 161 | &self, 162 | element: u8, 163 | el_row_idx: usize, 164 | el_col_idx: usize, 165 | ) -> HashSet<(usize, usize)> { 166 | let mut duplicates: HashSet<(usize, usize)> = HashSet::new(); 167 | 168 | let row_duplicates = self.get_duplicates_in_row(element, el_row_idx, el_col_idx); 169 | duplicates.extend(row_duplicates); 170 | 171 | let col_duplicates = self.get_duplicates_in_column(element, el_row_idx, el_col_idx); 172 | duplicates.extend(&col_duplicates); 173 | 174 | let cell_duplicates = self.get_duplicates_in_cell(element, el_row_idx, el_col_idx); 175 | duplicates.extend(&cell_duplicates); 176 | 177 | duplicates 178 | } 179 | 180 | fn is_correct(&self) -> bool { 181 | for row in &self.grid { 182 | for col in row { 183 | if col.len() != 1 { 184 | return false; 185 | } 186 | } 187 | } 188 | for col_idx in 0..9 { 189 | let mut sum_col_elems = 0; 190 | for row_idx in 0..9 { 191 | sum_col_elems += self.grid[row_idx][col_idx][0]; 192 | } 193 | if sum_col_elems != 9 * 10 / 2 { 194 | return false; 195 | } 196 | } 197 | for row_idx in 0..9 { 198 | let mut sum_row_elems = 0; 199 | for col_idx in 0..9 { 200 | sum_row_elems += self.grid[row_idx][col_idx][0]; 201 | } 202 | if sum_row_elems != 9 * 10 / 2 { 203 | return false; 204 | } 205 | } 206 | !self.has_unresolvable_conflicts() 207 | } 208 | 209 | fn has_unresolvable_conflicts(&self) -> bool { 210 | // rows okay 211 | for row_idx in 0..9 { 212 | let mut elems = vec![]; 213 | for col_idx in 0..9 { 214 | if self.grid[row_idx][col_idx].len() == 1 { 215 | elems.push(self.grid[row_idx][col_idx][0]); 216 | } 217 | } 218 | if !has_unique_elements(elems) { 219 | debug!("Unresolvable conflict at row: {}", row_idx); 220 | return true; 221 | } 222 | } 223 | 224 | // cols okay 225 | for col_idx in 0..9 { 226 | let mut elems = vec![]; 227 | for row_idx in 0..9 { 228 | if self.grid[row_idx][col_idx].len() == 1 { 229 | elems.push(self.grid[row_idx][col_idx][0]); 230 | } 231 | } 232 | if !has_unique_elements(elems) { 233 | debug!("Unresolvable conflict at col: {}", col_idx); 234 | return true; 235 | } 236 | } 237 | 238 | // cells okay 239 | for meta_col_idx in 0..3 { 240 | for meta_row_idx in 0..3 { 241 | let mut cell_elems = vec![]; 242 | for col_idx_in_cell in 0..3 { 243 | for row_idx_in_cell in 0..3 { 244 | let row_idx = meta_row_idx * 3 + row_idx_in_cell; 245 | let col_idx = meta_col_idx * 3 + col_idx_in_cell; 246 | if self.grid[row_idx][col_idx].len() == 1 { 247 | cell_elems.push(self.grid[row_idx][col_idx][0]); 248 | } 249 | } 250 | } 251 | if !has_unique_elements(cell_elems) { 252 | debug!( 253 | "Unresolvable conflict at cell: meta_row_idx {} meta_col_idx {}", 254 | meta_row_idx, meta_col_idx 255 | ); 256 | return true; 257 | } 258 | } 259 | } 260 | 261 | false 262 | } 263 | 264 | fn get_best_place_and_number_to_insert(&self) -> Option { 265 | // get place with least options, but more than one option 266 | 267 | let mut best_row = 0; 268 | let mut best_col = 0; 269 | let mut best_els = vec![]; 270 | let mut shortest_len = 100; 271 | 272 | // check for single option in row/col/cell 273 | // elemnt mindestens länge 2 274 | 'single_el_search: for row_idx in 0..9 { 275 | for col_idx in 0..9 { 276 | let current_prob_len = self.grid[row_idx][col_idx].len(); 277 | if current_prob_len == 1 { 278 | continue; 279 | } 280 | for el in &self.grid[row_idx][col_idx] { 281 | // check if single possible el 282 | if is_single_element_in_col(self, row_idx, col_idx, *el) 283 | || is_single_element_in_row(self, row_idx, col_idx, *el) 284 | || is_single_element_in_cell(self, row_idx, col_idx, *el) 285 | { 286 | best_row = row_idx; 287 | best_col = col_idx; 288 | best_els = vec![*el]; 289 | break 'single_el_search; 290 | } 291 | } 292 | } 293 | } 294 | 295 | if best_els.is_empty() { 296 | 'outer: for row_idx in 0..9 { 297 | for col_idx in 0..9 { 298 | let current_prob_len = self.grid[row_idx][col_idx].len(); 299 | if current_prob_len == 1 { 300 | continue; 301 | } else if current_prob_len == 2 { 302 | best_row = row_idx; 303 | best_col = col_idx; 304 | best_els = self.grid[row_idx][col_idx].clone(); 305 | break 'outer; 306 | } else if current_prob_len > 2 && current_prob_len < shortest_len { 307 | best_row = row_idx; 308 | best_col = col_idx; 309 | best_els = self.grid[row_idx][col_idx].clone(); 310 | shortest_len = current_prob_len; 311 | } 312 | } 313 | } 314 | } 315 | if best_els.is_empty() { 316 | return None; 317 | } 318 | 319 | Some(InsertionCandidate { 320 | row_idx: best_row, 321 | col_idx: best_col, 322 | candidates: best_els, 323 | }) 324 | } 325 | } 326 | 327 | impl std::fmt::Display for SudokuCandidates { 328 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 329 | let mut some_str = "".to_string(); 330 | for row_idx in 0..9 { 331 | for col_idx in 0..9 { 332 | let cand_str = self.grid[row_idx][col_idx] 333 | .iter() 334 | .map(|i| i.to_string()) 335 | .collect::(); 336 | let sym = format!("{: >9},", cand_str); 337 | some_str.push_str(&sym); 338 | } 339 | some_str.push('\n'); 340 | } 341 | 342 | write!(f, "\n{}", some_str) 343 | } 344 | } 345 | 346 | pub fn parse_sudokus(filepath: &str) -> Vec { 347 | let contents = fs::read_to_string(filepath).expect("Something went wrong reading the file"); 348 | let lines = contents.lines(); 349 | 350 | let mut candidates = vec![]; 351 | for line in lines { 352 | // replace any non numeric characters in the line with 0 353 | let non_numeric_chars = Regex::new(r"[^0-9]").unwrap(); 354 | let line_no_commas = line.replace(",", ""); 355 | let problem_str_raw = non_numeric_chars 356 | .replace_all(&line_no_commas, "0") 357 | .into_owned(); 358 | 359 | let problem_raw: Vec = convert_problem_str(&problem_str_raw); 360 | let cand = SudokuCandidates::from_vec(problem_raw); 361 | 362 | if let Some(cand) = cand { 363 | candidates.push(cand); 364 | } else { 365 | println!( 366 | "Failed to parse sudoku from {} - possibly containts conflicts.", 367 | &problem_str_raw 368 | ); 369 | } 370 | } 371 | candidates 372 | } 373 | 374 | fn remove_duplicates( 375 | problem: &SudokuCandidates, 376 | el_row_idx: usize, 377 | el_col_idx: usize, 378 | el: u8, 379 | ) -> Option { 380 | debug!("removing {} {} {}", el_row_idx, el_col_idx, el); 381 | let mut problem = problem.clone(); 382 | let duplicates = problem.get_duplicates(el, el_row_idx, el_col_idx); 383 | for (dup_row_idx, dup_col_idx) in duplicates { 384 | let dupl_idx = problem.grid[dup_row_idx][dup_col_idx] 385 | .iter() 386 | .position(|x| *x == el); 387 | if let Some(x) = dupl_idx { 388 | problem.grid[dup_row_idx][dup_col_idx].remove(x); 389 | if problem.grid[dup_row_idx][dup_col_idx].is_empty() { 390 | // conflict detected 391 | return None; 392 | } else if problem.grid[dup_row_idx][dup_col_idx].len() == 1 { 393 | let new_prob_opt = remove_duplicates( 394 | &problem, 395 | dup_row_idx, 396 | dup_col_idx, 397 | problem.grid[dup_row_idx][dup_col_idx][0], 398 | ); 399 | if let Some(new_prob) = new_prob_opt { 400 | problem = new_prob; 401 | } else { 402 | return None; 403 | } 404 | } 405 | } 406 | } 407 | Some(problem) 408 | } 409 | 410 | fn has_unique_elements(iter: T) -> bool 411 | where 412 | T: IntoIterator, 413 | T::Item: Eq + Hash, 414 | { 415 | let mut uniq = HashSet::new(); 416 | iter.into_iter().all(move |x| uniq.insert(x)) 417 | } 418 | 419 | #[derive(Clone, Debug)] 420 | struct InsertionCandidate { 421 | row_idx: usize, 422 | col_idx: usize, 423 | candidates: Vec, 424 | } 425 | 426 | fn is_single_element_in_col( 427 | problem: &SudokuCandidates, 428 | row_idx: usize, 429 | col_idx: usize, 430 | el: u8, 431 | ) -> bool { 432 | for row_inner_idx in 0..9 { 433 | if row_idx == row_inner_idx { 434 | continue; 435 | } 436 | if problem.grid[row_inner_idx][col_idx] 437 | .iter() 438 | .any(|x| x == &el) 439 | { 440 | return false; 441 | } 442 | } 443 | true 444 | } 445 | 446 | fn is_single_element_in_row( 447 | problem: &SudokuCandidates, 448 | row_idx: usize, 449 | col_idx: usize, 450 | el: u8, 451 | ) -> bool { 452 | for col_inner_idx in 0..9 { 453 | if col_idx == col_inner_idx { 454 | continue; 455 | } 456 | if problem.grid[row_idx][col_inner_idx] 457 | .iter() 458 | .any(|x| x == &el) 459 | { 460 | return false; 461 | } 462 | } 463 | true 464 | } 465 | 466 | fn is_single_element_in_cell( 467 | problem: &SudokuCandidates, 468 | row_idx: usize, 469 | col_idx: usize, 470 | el: u8, 471 | ) -> bool { 472 | let cell_row_idx = row_idx / 3; 473 | let cell_col_idx = col_idx / 3; 474 | 475 | for row_idx_within_cell in 0..3 { 476 | for col_idx_within_cell in 0..3 { 477 | let row_idx_abs = 3 * cell_row_idx + row_idx_within_cell; 478 | let col_idx_abs = 3 * cell_col_idx + col_idx_within_cell; 479 | if row_idx_abs == row_idx && col_idx_abs == col_idx { 480 | continue; 481 | } 482 | if problem.grid[row_idx_abs][col_idx_abs] 483 | .iter() 484 | .any(|x| x == &el) 485 | { 486 | return false; 487 | } 488 | } 489 | } 490 | true 491 | } 492 | 493 | pub fn solve_sudoku( 494 | problem_opt: Option, 495 | recursion_depth: usize, 496 | ) -> Option { 497 | let recursion_depth = recursion_depth + 1; 498 | 499 | if let Some(problem) = problem_opt { 500 | debug!("Depth: {}\n {}", recursion_depth, problem); 501 | 502 | if problem.is_correct() { 503 | // base case: only one possible number in each cell, solution found 504 | Some(problem) 505 | } else if problem.has_unresolvable_conflicts() { 506 | None 507 | } else { 508 | let insertion_cand_opt = problem.get_best_place_and_number_to_insert(); 509 | 510 | debug!( 511 | "Depth: {} Found insertion candidate {:?}", 512 | recursion_depth, insertion_cand_opt 513 | ); 514 | 515 | if let Some(insertion_candidate) = insertion_cand_opt { 516 | // try all possible solutions 517 | let mut solution_candidates = vec![]; 518 | 519 | for el in insertion_candidate.candidates { 520 | let mut problem_bkp = problem.clone(); 521 | 522 | problem_bkp.grid[insertion_candidate.row_idx][insertion_candidate.col_idx] = 523 | vec![el]; 524 | let prob_tmp = remove_duplicates( 525 | &problem_bkp, 526 | insertion_candidate.row_idx, 527 | insertion_candidate.col_idx, 528 | el, 529 | ); 530 | if prob_tmp.is_some() { 531 | solution_candidates.push(prob_tmp); 532 | } 533 | } 534 | 535 | debug!( 536 | "Found {} candidates at depth {}", 537 | solution_candidates.len(), 538 | recursion_depth 539 | ); 540 | 541 | let solution = solution_candidates 542 | .iter() 543 | .cloned() 544 | .find_map(|x| solve_sudoku(x, recursion_depth)); 545 | 546 | match solution { 547 | Some(x) => solve_sudoku(Some(x), recursion_depth), 548 | _ => { 549 | debug!("No solution found at depth {}", recursion_depth); 550 | None 551 | } 552 | } 553 | } else { 554 | debug!("No candidates found at depth {}", recursion_depth); 555 | None 556 | } 557 | } 558 | } else { 559 | debug!("Received None: {}", recursion_depth); 560 | None 561 | } 562 | } 563 | 564 | #[wasm_bindgen] 565 | pub fn wasm_get_sample_sudoku_string(random_number: f64) -> String { 566 | // tried using the rand method from cate rand but it panicks: 567 | // panicked at 'could not initialize thread_rng: getrandom: this target is not supported' 568 | // use rand::seq::SliceRandom; 569 | // use rand::thread_rng; 570 | 571 | let problems = [ 572 | "015040002020560098300010007200000600940001000030680704458000000090872050600430900", 573 | "270600050000070406006059030040005600081000040029006173390000002000097800807140005", 574 | "020980040030047601019006080700490000800023907000605000904800006001000300350014020", 575 | "006030010300605000070029000020300984794000300000001005530008200069047000041200590", 576 | "040038500905000000000010460001650043000700901082300050830100074276000090000960002", 577 | "800000000003600000070090200050007000000045700000100030001000068008500010090000400", 578 | ]; 579 | 580 | let random_idx = random_number * problems.len() as f64; 581 | problems[random_idx as usize].to_string() 582 | } 583 | 584 | #[cfg(test)] 585 | mod tests { 586 | use super::*; 587 | 588 | #[test] 589 | fn test_has_no_conflicts_js_interface() { 590 | assert!(!wasm_sudoku_contains_conflicts( 591 | "006037508700010900130050020002908000050020430600000090200005704003100060498600000" 592 | )); 593 | } 594 | 595 | #[test] 596 | fn test_has_conflicts_js_interface() { 597 | assert!(wasm_sudoku_contains_conflicts( 598 | "066037508700010900130050020002908000050020430600000090200005704003100060498600000" 599 | )); 600 | } 601 | 602 | #[test] 603 | fn test_solve_js_interface() { 604 | assert_eq!( 605 | wasm_solve_sudoku( 606 | "006037508700010900130050020002908000050020430600000090200005704003100060498600000" 607 | ), 608 | "926437518785216943134859627342968175859721436617543892261395784573184269498672351" 609 | ); 610 | } 611 | 612 | #[test] 613 | fn test_unsolvable_solve_js_interface() { 614 | assert_eq!( 615 | wasm_solve_sudoku( 616 | "066037508700010900130050020002908000050020430600000090200005704003100060498600000" 617 | ), 618 | "066037508700010900130050020002908000050020430600000090200005704003100060498600000" 619 | ); 620 | } 621 | 622 | #[test] 623 | fn test_trivial_problem_is_solvable() { 624 | let sudoku_vec = convert_problem_str( 625 | "000000000000000000000000000000000000000000000000000000000000000000000000000000000", 626 | ); 627 | assert!(solve_sudoku(SudokuCandidates::from_vec(sudoku_vec), 0).is_some()); 628 | } 629 | 630 | #[test] 631 | fn test_trivial_wrong_problem_is_unsolvable() { 632 | let sudoku_vec = convert_problem_str( 633 | "110000000000000000000000000000000000000000000000000000000000000000000000000000000", 634 | ); 635 | let sol_opt = solve_sudoku(SudokuCandidates::from_vec(sudoku_vec), 0); 636 | assert!(sol_opt.is_none()); 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | extern crate chrono; 4 | extern crate clap; 5 | extern crate env_logger; 6 | extern crate regex; 7 | 8 | use chrono::Local; 9 | use clap::{Arg, ArgAction, Command}; 10 | use env_logger::Builder; 11 | use log::LevelFilter; 12 | use std::io::Write; 13 | use std::time::Instant; 14 | 15 | use sudoku_solver::{parse_sudokus, solve_sudoku}; 16 | 17 | fn main() { 18 | let matches = Command::new("Sudoku Solver") 19 | .version("0.2.0") 20 | .author("baurst") 21 | .about("Fast Sudoku solver.") 22 | .arg( 23 | Arg::new("INPUT") 24 | .help("Sudoku input file to use. One problem per line.") 25 | .required(true) 26 | .index(1), 27 | ) 28 | .arg( 29 | Arg::new("v") 30 | .short('v') 31 | .action(ArgAction::Count) 32 | .help("Sets the level of verbosity"), 33 | ) 34 | .get_matches(); 35 | 36 | let loglevel = match *matches.get_one::("v").unwrap_or(&0) { 37 | 0 => LevelFilter::Info, 38 | _ => LevelFilter::Debug, 39 | }; 40 | 41 | Builder::new() 42 | .format(|buf, record| { 43 | writeln!( 44 | buf, 45 | "[{}] {}: {}", 46 | record.level(), 47 | Local::now().format("%Y-%m-%d-%H:%M:%S"), 48 | record.args() 49 | ) 50 | }) 51 | .filter(None, loglevel) 52 | .init(); 53 | 54 | let prob = matches.get_one::("INPUT").unwrap(); 55 | 56 | let sudoku_problems = parse_sudokus(prob); 57 | 58 | let num_sudokus = sudoku_problems.len(); 59 | let mut num_unsolvable_sudokus = 0; 60 | let time_start = Instant::now(); 61 | 62 | for prob in sudoku_problems { 63 | info!("Starting with problem: {}", prob); 64 | let solution = solve_sudoku(Some(prob), 0); 65 | 66 | if let Some(solved) = solution { 67 | info!("Problem solved:{}", solved); 68 | } else { 69 | warn!("Problem unsolvable!"); 70 | num_unsolvable_sudokus += 1; 71 | } 72 | } 73 | 74 | let duration = Instant::now() - time_start; 75 | 76 | info!( 77 | "{:?} for {} sudokus, on average {:?} per problem.", 78 | duration, 79 | num_sudokus, 80 | duration / num_sudokus as u32 81 | ); 82 | 83 | if num_unsolvable_sudokus > 0 { 84 | warn!( 85 | "Failed to solve {} out of {} sudokus.", 86 | num_unsolvable_sudokus, num_sudokus, 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /www/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-typescript", 5 | "@babel/preset-react" 6 | ], 7 | "plugins": [ 8 | "@babel/proposal-class-properties", 9 | "@babel/proposal-object-rest-spread" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /www/.bin/create-wasm-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawn } = require("child_process"); 4 | const fs = require("fs"); 5 | 6 | let folderName = '.'; 7 | 8 | if (process.argv.length >= 3) { 9 | folderName = process.argv[2]; 10 | if (!fs.existsSync(folderName)) { 11 | fs.mkdirSync(folderName); 12 | } 13 | } 14 | 15 | const clone = spawn("git", ["clone", "https://github.com/rustwasm/create-wasm-app.git", folderName]); 16 | 17 | clone.on("close", code => { 18 | if (code !== 0) { 19 | console.error("cloning the template failed!") 20 | process.exit(code); 21 | } else { 22 | console.log("🦀 Rust + 🕸 Wasm = ❤"); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /www/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: "10" 3 | 4 | script: 5 | - ./node_modules/.bin/webpack 6 | -------------------------------------------------------------------------------- /www/LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /www/LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) [year] [name] 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

create-wasm-app

4 | 5 | An npm init template for kick starting a project that uses NPM packages containing Rust-generated WebAssembly and bundles them with Webpack. 6 | 7 |

8 | Build Status 9 |

10 | 11 |

12 | Usage 13 | | 14 | Chat 15 |

16 | 17 | Built with 🦀🕸 by The Rust and WebAssembly Working Group 18 |
19 | 20 | ## About 21 | 22 | This template is designed for depending on NPM packages that contain 23 | Rust-generated WebAssembly and using them to create a Website. 24 | 25 | * Want to create an NPM package with Rust and WebAssembly? [Check out 26 | `wasm-pack-template`.](https://github.com/rustwasm/wasm-pack-template) 27 | * Want to make a monorepo-style Website without publishing to NPM? Check out 28 | [`rust-webpack-template`](https://github.com/rustwasm/rust-webpack-template) 29 | and/or 30 | [`rust-parcel-template`](https://github.com/rustwasm/rust-parcel-template). 31 | 32 | ## 🚴 Usage 33 | 34 | ``` 35 | npm init wasm-app 36 | ``` 37 | 38 | ## 🔋 Batteries Included 39 | 40 | - `.gitignore`: ignores `node_modules` 41 | - `LICENSE-APACHE` and `LICENSE-MIT`: most Rust projects are licensed this way, so these are included for you 42 | - `README.md`: the file you are reading now! 43 | - `index.html`: a bare bones html document that includes the webpack bundle 44 | - `index.js`: example js file with a comment showing how to import and use a wasm pkg 45 | - `package.json` and `package-lock.json`: 46 | - pulls in devDependencies for using webpack: 47 | - [`webpack`](https://www.npmjs.com/package/webpack) 48 | - [`webpack-cli`](https://www.npmjs.com/package/webpack-cli) 49 | - [`webpack-dev-server`](https://www.npmjs.com/package/webpack-dev-server) 50 | - defines a `start` script to run `webpack-dev-server` 51 | - `webpack.config.js`: configuration file for bundling your js with webpack 52 | 53 | ## License 54 | 55 | Licensed under either of 56 | 57 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 58 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 59 | 60 | at your option. 61 | 62 | ### Contribution 63 | 64 | Unless you explicitly state otherwise, any contribution intentionally 65 | submitted for inclusion in the work by you, as defined in the Apache-2.0 66 | license, shall be dual licensed as above, without any additional terms or 67 | conditions. 68 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sudoku-wasm-client", 3 | "version": "0.1.0", 4 | "main": "src/index.tsx", 5 | "homepage": "http://baurst.github.io/sudoku_solver", 6 | "bin": { 7 | "create-wasm-app": ".bin/create-wasm-app.js" 8 | }, 9 | "scripts": { 10 | "build": "webpack --config webpack.config.js", 11 | "start": "webpack-dev-server", 12 | "deploy": "gh-pages -d dist" 13 | }, 14 | "keywords": [ 15 | "webassembly", 16 | "wasm", 17 | "rust", 18 | "webpack" 19 | ], 20 | "dependencies": { 21 | "@types/react": "^16.9.2", 22 | "@types/react-dom": "^16.9.0", 23 | "bootstrap": "^5.3.3", 24 | "node-gyp": "^8.0.0", 25 | "react": "^16.9.0", 26 | "react-bootstrap": "^1.0.0", 27 | "react-dom": "^16.9.0", 28 | "sudoku_solver": "file:../pkg", 29 | "typescript": "^3.6.4" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.6.0", 33 | "@babel/plugin-proposal-class-properties": "^7.5.5", 34 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 35 | "@babel/preset-env": "^7.6.0", 36 | "@babel/preset-react": "^7.0.0", 37 | "@babel/preset-typescript": "^7.6.0", 38 | "babel-loader": "^8.0.6", 39 | "copy-webpack-plugin": "^5.0.0", 40 | "css-loader": "^7.1.2", 41 | "file-loader": "^6.0.0", 42 | "gh-pages": "^6.3.0", 43 | "hello-wasm-pack": "^0.1.0", 44 | "html-webpack-plugin": "^5.6.3", 45 | "node-sass": "^9.0.0", 46 | "style-loader": "^1.0.0", 47 | "svg-url-loader": "^5.0.0", 48 | "tslint": "^5.19.0", 49 | "tslint-immutable": "^6.0.1", 50 | "typescript": "^3.6.4", 51 | "webpack": "^5.97.1", 52 | "webpack-cli": "^6.0.1", 53 | "webpack-dev-server": "^5.2.0" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /www/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/www/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /www/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/www/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /www/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/www/public/apple-touch-icon.png -------------------------------------------------------------------------------- /www/public/bootstrap.js: -------------------------------------------------------------------------------- 1 | // A dependency graph that contains any wasm must all be imported 2 | // asynchronously. This `bootstrap.js` file does the single async import, so 3 | // that no one else needs to worry about it again. 4 | import("../src/index.tsx") 5 | .catch(e => console.error("Error importing `index.js`:", e)); 6 | -------------------------------------------------------------------------------- /www/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/www/public/favicon-16x16.png -------------------------------------------------------------------------------- /www/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/www/public/favicon-32x32.png -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baurst/sudoku_solver/385d9ed40ea0636c6fc6350e7abb90d083377a94/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | SudokuSolver 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /www/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "SudokuSolver", 3 | "name": "Sudoku Solver Application", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "32x32 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "android-chrome-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /www/src/App.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | @media (prefers-reduced-motion: no-preference) { 4 | .sudoku-logo { 5 | animation: logo-spin infinite 3s linear; 6 | } 7 | } 8 | 9 | @keyframes logo-spin { 10 | from { 11 | transform: rotate(0deg); 12 | } 13 | to { 14 | transform: rotate(360deg); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /www/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SudokuTable from './sudoku/SudokuTable'; 3 | import { Container, Navbar, Row } from "react-bootstrap"; 4 | import logo from './logo.svg' 5 | import './App.css' 6 | 7 | function App() { 8 | return ( 9 | <> 10 |
11 | 12 | 13 | bla 20 |   Sudoku 21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | ); 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /www/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /www/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'bootstrap/dist/css/bootstrap.min.css'; 4 | import App from './App'; 5 | ReactDOM.render(<>, document.getElementById('root')); -------------------------------------------------------------------------------- /www/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /www/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /www/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /www/src/sudoku/SudokuTable.css: -------------------------------------------------------------------------------- 1 | .sudoku-table { 2 | border-spacing: 0; 3 | top: 0; 4 | bottom: 0; 5 | left: 0; 6 | right: 0; 7 | } 8 | 9 | .sudoku-table td { 10 | padding: 0; 11 | margin: 0; 12 | width: 2.0rem; 13 | height: 2.0rem; 14 | } 15 | 16 | .sudoku-table input[type=text] { 17 | font-size: 20pt; 18 | width: 100%; 19 | height: 100%; 20 | line-height: 2.0rem; 21 | margin: 0.0rem; 22 | padding: 0; 23 | text-align: center; 24 | border: 0; 25 | box-sizing: border-box; 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | aspect-ratio: 1/1; 30 | } 31 | 32 | .sudoku-table .right-middle { 33 | border-left: 1px solid black; 34 | border-right: 3px solid black; 35 | border-top: 1px solid black; 36 | border-bottom: 1px solid black; 37 | } 38 | 39 | .sudoku-table .left-middle { 40 | border-left: 3px solid black; 41 | border-right: 1px solid black; 42 | border-top: 1px solid black; 43 | border-bottom: 1px solid black; 44 | } 45 | 46 | .sudoku-table .middle-middle { 47 | border-left: 1px solid black; 48 | border-right: 1px solid black; 49 | border-top: 1px solid black; 50 | border-bottom: 1px solid black; 51 | } 52 | 53 | .sudoku-table .left-top { 54 | border-left: 3px solid black; 55 | border-right: 1px solid black; 56 | border-top: 3px solid black; 57 | border-bottom: 1px solid black; 58 | } 59 | 60 | .sudoku-table .left-bottom { 61 | border-left: 3px solid black; 62 | border-right: 1px solid black; 63 | border-top: 1px solid black; 64 | border-bottom: 3px solid black; 65 | } 66 | 67 | .sudoku-table .middle-bottom { 68 | border-left: 1px solid black; 69 | border-right: 1px solid black; 70 | border-top: 1px solid black; 71 | border-bottom: 3px solid black; 72 | } 73 | 74 | .sudoku-table .middle-top { 75 | border-left: 1px solid black; 76 | border-right: 1px solid black; 77 | border-top: 3px solid black; 78 | border-bottom: 1px solid black; 79 | } 80 | 81 | .sudoku-table .right-bottom { 82 | border-left: 1px solid black; 83 | border-right: 3px solid black; 84 | border-top: 1px solid black; 85 | border-bottom: 3px solid black; 86 | } 87 | 88 | .sudoku-table .right-top { 89 | border-left: 1px solid black; 90 | border-right: 3px solid black; 91 | border-top: 3px solid black; 92 | border-bottom: 1px solid black; 93 | } 94 | 95 | .button-container .btn { 96 | width: 10rem; 97 | } -------------------------------------------------------------------------------- /www/src/sudoku/SudokuTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import useSudokuTableCore from "./SudokuTableCore"; 3 | import { Button, Container, Row, Modal, Col } from "react-bootstrap"; 4 | import './SudokuTable.css'; 5 | 6 | function tableRow( 7 | size: number, 8 | rowNr: number, 9 | sudoku: number[], 10 | setValueInSudoku: Function, 11 | isUserInput: boolean[], 12 | ) { 13 | const sizeElements = Array(size) 14 | .fill(0) 15 | .map((v, i) => rowNr * 9 + i); 16 | 17 | return ( 18 | <> 19 | 20 | {sizeElements.map((entry) => 21 | tableEntry(entry, sudoku, setValueInSudoku, isUserInput) 22 | )} 23 | 24 | 25 | ); 26 | } 27 | 28 | const styles = { 29 | text_normal: { 30 | fontWeight: "normal", 31 | } as React.CSSProperties, 32 | text_bold: { 33 | fontWeight: "bold", 34 | color: "red", 35 | } as React.CSSProperties, 36 | }; 37 | 38 | function tableEntry( 39 | entryNr: number, 40 | sudoku: number[], 41 | setValueInSudoku: Function, 42 | isUserInput: boolean[], 43 | ) { 44 | const entryStr = `${entryNr}`; 45 | const cellNames = [ 46 | "left-top", 47 | "middle-top", 48 | "right-top", 49 | "left-middle", 50 | "middle-middle", 51 | "right-middle", 52 | "left-bottom", 53 | "middle-bottom", 54 | "right-bottom", 55 | ]; 56 | 57 | const row = Math.floor(entryNr / 9); 58 | const col = entryNr % 9; 59 | const meta_cell_row = Math.floor(row / 3); 60 | const meta_cell_col = Math.floor(col / 3); 61 | const pos_row = row - meta_cell_row * 3; 62 | const pos_col = col - meta_cell_col * 3; 63 | const pos_idx = pos_row * 3 + pos_col; 64 | const styleName = cellNames[pos_idx]; 65 | 66 | return ( 67 | <> 68 | 69 | ) => { 78 | console.log( 79 | `change ${event.currentTarget.name} to ${event.currentTarget.value}` 80 | ); 81 | if (!isNaN(parseInt(event.currentTarget.value)) || event.currentTarget.value === "") { 82 | let parseValue = parseInt(event.currentTarget.value); 83 | if (isNaN(parseValue)) { 84 | setValueInSudoku(entryNr, 0); 85 | } else { 86 | setValueInSudoku(entryNr, parseInt(event.currentTarget.value)); 87 | } 88 | } 89 | }} 90 | /> 91 | 92 | 93 | ); 94 | } 95 | 96 | const SudokuTable: React.FC = () => { 97 | const size = 9; 98 | const sudokuTableCore = useSudokuTableCore(size); 99 | const sizeElements = Array(size) 100 | .fill(0) 101 | .map((v, i) => i); 102 | const [isSolving, setSolving] = useState(false); 103 | 104 | const [showWarning, setShowWarning] = useState(false); 105 | 106 | const ConflictWarning: React.FC = () => { 107 | const handleClose = () => setShowWarning(false); 108 | 109 | return ( 110 | 111 | 112 | WARNING 113 | 114 | 115 |

Sudoku contains unsolvable conflict(s)!

116 |

Please fix the conflicts and try again!

117 |
118 | 119 | 122 | 123 |
124 | ); 125 | }; 126 | 127 | const handleClick = () => { 128 | if (!isSolving) { 129 | let sudokuIsOkay = sudokuTableCore.checkSudokuIsSolvable(); 130 | if (sudokuIsOkay) { 131 | setSolving(true); 132 | sudokuTableCore.solveSudoku(); 133 | setSolving(false); 134 | } else { 135 | setShowWarning(true); 136 | } 137 | } 138 | }; 139 | 140 | const handleClearClick = () => { 141 | sudokuTableCore.clearSudoku(); 142 | }; 143 | 144 | const handleInsertClick = () => { 145 | sudokuTableCore.insertSampleSudoku(); 146 | }; 147 | 148 | return ( 149 | 150 | 151 | 152 | 153 | 154 | {sizeElements.map((row) => 155 | tableRow( 156 | size, 157 | row, 158 | sudokuTableCore.sudoku, 159 | sudokuTableCore.setValueInSudoku, 160 | sudokuTableCore.isUserInput 161 | ) 162 | )} 163 | 164 |
165 |
166 | 167 | 170 | 173 | 176 | 177 |
178 | ); 179 | }; 180 | 181 | export default SudokuTable; -------------------------------------------------------------------------------- /www/src/sudoku/SudokuTableCore.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import * as wasm from "sudoku_solver"; 3 | 4 | 5 | interface SudokuTableProps { 6 | sudoku: number[]; 7 | isUserInput: boolean[]; 8 | setValueInSudoku: Function; 9 | checkSudokuIsSolvable: Function; 10 | solveSudoku: Function; 11 | insertSampleSudoku: Function; 12 | clearSudoku: Function; 13 | } 14 | 15 | function useSudokuTableCore(size: number): SudokuTableProps { 16 | const [sudoku, setSudoku] = useState(new Array(size * size).fill(0)); 17 | const [isUserInput, setIsUserInput] = useState(new Array(size * size).fill(false)); 18 | 19 | const setValueInSudoku = (place: number, value: number) => { 20 | const currentSudoku = [...sudoku]; 21 | console.log(`set value ${value} in place ${place}.`); 22 | console.log(currentSudoku.toString()); 23 | currentSudoku[place] = value; 24 | isUserInput[place] = true; 25 | console.log(currentSudoku.toString()); 26 | setSudoku(currentSudoku); 27 | 28 | }; 29 | 30 | const insertSampleSudoku = () => { 31 | let example_sudoku = ""; 32 | example_sudoku = wasm.wasm_get_sample_sudoku_string(Math.random()); 33 | let isUserInput = new Array(size * size).fill(false); 34 | let example_sudoku_array = example_sudoku.split('').map(x=>+x) 35 | for (let index = 0; index < example_sudoku.length; ++index) { 36 | isUserInput[index] = example_sudoku_array[index] !== 0; 37 | } 38 | setIsUserInput(isUserInput); 39 | setSudoku(example_sudoku_array); 40 | }; 41 | 42 | const solveSudoku = () => { 43 | let solvedSudoku = ""; 44 | solvedSudoku = wasm.wasm_solve_sudoku(sudoku.join("")); 45 | setSudoku(solvedSudoku.split('').map(x=>+x)); 46 | }; 47 | 48 | const checkSudokuIsSolvable = () => { 49 | let sudokuHasConflict = wasm.wasm_sudoku_contains_conflicts(sudoku.join("")); 50 | return !sudokuHasConflict; 51 | }; 52 | 53 | const clearSudoku = () => { 54 | let emptySudoku = new Array(size * size).fill(0); 55 | let emptyIsUserInput = new Array(size * size).fill(false); 56 | setSudoku(emptySudoku); 57 | setIsUserInput(emptyIsUserInput); 58 | }; 59 | 60 | useEffect(() => { 61 | console.log(sudoku.toString()); 62 | }, [sudoku]); 63 | 64 | return { 65 | sudoku: sudoku, 66 | isUserInput: isUserInput, 67 | setValueInSudoku: setValueInSudoku, 68 | checkSudokuIsSolvable: checkSudokuIsSolvable, 69 | insertSampleSudoku: insertSampleSudoku, 70 | solveSudoku: solveSudoku, 71 | clearSudoku: clearSudoku, 72 | }; 73 | } 74 | 75 | export default useSudokuTableCore; 76 | -------------------------------------------------------------------------------- /www/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /www/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: "./public/bootstrap.js", 6 | output: { 7 | path: path.resolve(__dirname, "dist"), 8 | filename: "bootstrap.js", 9 | }, 10 | mode: "development", 11 | 12 | 13 | // adding .ts and .tsx to resolve.extensions will help babel look for .ts and .tsx files to transpile 14 | resolve: { 15 | extensions: ['.ts', '.tsx', '.js'] 16 | }, 17 | module: { 18 | rules: [ 19 | 20 | // we use babel-loader to load our jsx and tsx files 21 | { 22 | test: /\.(ts|js)x?$/, 23 | exclude: /node_modules/, 24 | use: { 25 | loader: 'babel-loader' 26 | }, 27 | }, 28 | { 29 | test: /\.css$/, 30 | use: ['style-loader', 'css-loader'] 31 | }, 32 | { 33 | test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, 34 | use: [ 35 | { 36 | loader: 'file-loader' 37 | } 38 | ] 39 | }, 40 | { 41 | test: /\.svg$/, 42 | use: [ 43 | { 44 | loader: 'svg-url-loader' 45 | }, 46 | ] 47 | } 48 | ] 49 | }, 50 | plugins: [ 51 | new CopyWebpackPlugin(['./public/index.html']), 52 | new CopyWebpackPlugin([ 53 | // relative path is from src 54 | //{ from: './public/favicon.ico' }, // <- your path to favicon 55 | {from: 'public'}, // <- your path to favicon 56 | ]) 57 | ], 58 | experiments: { 59 | asyncWebAssembly: true, 60 | }, 61 | }; 62 | --------------------------------------------------------------------------------