├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── screenshot.png └── src ├── main.rs └── sudoku.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /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 = "allocator-api2" 7 | version = "0.2.18" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" 10 | 11 | [[package]] 12 | name = "anstream" 13 | version = "0.6.18" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 16 | dependencies = [ 17 | "anstyle", 18 | "anstyle-parse", 19 | "anstyle-query", 20 | "anstyle-wincon", 21 | "colorchoice", 22 | "is_terminal_polyfill", 23 | "utf8parse", 24 | ] 25 | 26 | [[package]] 27 | name = "anstyle" 28 | version = "1.0.10" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 31 | 32 | [[package]] 33 | name = "anstyle-parse" 34 | version = "0.2.6" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 37 | dependencies = [ 38 | "utf8parse", 39 | ] 40 | 41 | [[package]] 42 | name = "anstyle-query" 43 | version = "1.1.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 46 | dependencies = [ 47 | "windows-sys 0.59.0", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-wincon" 52 | version = "3.0.6" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 55 | dependencies = [ 56 | "anstyle", 57 | "windows-sys 0.59.0", 58 | ] 59 | 60 | [[package]] 61 | name = "autocfg" 62 | version = "1.4.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 65 | 66 | [[package]] 67 | name = "bitflags" 68 | version = "2.6.0" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 71 | 72 | [[package]] 73 | name = "cassowary" 74 | version = "0.3.0" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" 77 | 78 | [[package]] 79 | name = "castaway" 80 | version = "0.2.3" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" 83 | dependencies = [ 84 | "rustversion", 85 | ] 86 | 87 | [[package]] 88 | name = "cfg-if" 89 | version = "1.0.0" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 92 | 93 | [[package]] 94 | name = "clap" 95 | version = "4.5.21" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" 98 | dependencies = [ 99 | "clap_builder", 100 | "clap_derive", 101 | ] 102 | 103 | [[package]] 104 | name = "clap_builder" 105 | version = "4.5.21" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" 108 | dependencies = [ 109 | "anstream", 110 | "anstyle", 111 | "clap_lex", 112 | "strsim", 113 | ] 114 | 115 | [[package]] 116 | name = "clap_derive" 117 | version = "4.5.18" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 120 | dependencies = [ 121 | "heck", 122 | "proc-macro2", 123 | "quote", 124 | "syn", 125 | ] 126 | 127 | [[package]] 128 | name = "clap_lex" 129 | version = "0.7.3" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" 132 | 133 | [[package]] 134 | name = "colorchoice" 135 | version = "1.0.3" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 138 | 139 | [[package]] 140 | name = "compact_str" 141 | version = "0.8.0" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644" 144 | dependencies = [ 145 | "castaway", 146 | "cfg-if", 147 | "itoa", 148 | "rustversion", 149 | "ryu", 150 | "static_assertions", 151 | ] 152 | 153 | [[package]] 154 | name = "crossterm" 155 | version = "0.28.1" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" 158 | dependencies = [ 159 | "bitflags", 160 | "crossterm_winapi", 161 | "mio", 162 | "parking_lot", 163 | "rustix", 164 | "signal-hook", 165 | "signal-hook-mio", 166 | "winapi", 167 | ] 168 | 169 | [[package]] 170 | name = "crossterm_winapi" 171 | version = "0.9.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" 174 | dependencies = [ 175 | "winapi", 176 | ] 177 | 178 | [[package]] 179 | name = "either" 180 | version = "1.13.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 183 | 184 | [[package]] 185 | name = "equivalent" 186 | version = "1.0.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 189 | 190 | [[package]] 191 | name = "errno" 192 | version = "0.3.9" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" 195 | dependencies = [ 196 | "libc", 197 | "windows-sys 0.52.0", 198 | ] 199 | 200 | [[package]] 201 | name = "foldhash" 202 | version = "0.1.3" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" 205 | 206 | [[package]] 207 | name = "getrandom" 208 | version = "0.3.2" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 211 | dependencies = [ 212 | "cfg-if", 213 | "libc", 214 | "r-efi", 215 | "wasi 0.14.2+wasi-0.2.4", 216 | ] 217 | 218 | [[package]] 219 | name = "hashbrown" 220 | version = "0.15.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 223 | dependencies = [ 224 | "allocator-api2", 225 | "equivalent", 226 | "foldhash", 227 | ] 228 | 229 | [[package]] 230 | name = "heck" 231 | version = "0.5.0" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 234 | 235 | [[package]] 236 | name = "hermit-abi" 237 | version = "0.3.9" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 240 | 241 | [[package]] 242 | name = "indoc" 243 | version = "2.0.5" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" 246 | 247 | [[package]] 248 | name = "instability" 249 | version = "0.3.2" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "b23a0c8dfe501baac4adf6ebbfa6eddf8f0c07f56b058cc1288017e32397846c" 252 | dependencies = [ 253 | "quote", 254 | "syn", 255 | ] 256 | 257 | [[package]] 258 | name = "is_terminal_polyfill" 259 | version = "1.70.1" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 262 | 263 | [[package]] 264 | name = "itertools" 265 | version = "0.13.0" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 268 | dependencies = [ 269 | "either", 270 | ] 271 | 272 | [[package]] 273 | name = "itoa" 274 | version = "1.0.11" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 277 | 278 | [[package]] 279 | name = "libc" 280 | version = "0.2.161" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" 283 | 284 | [[package]] 285 | name = "linux-raw-sys" 286 | version = "0.4.14" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 289 | 290 | [[package]] 291 | name = "lock_api" 292 | version = "0.4.12" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 295 | dependencies = [ 296 | "autocfg", 297 | "scopeguard", 298 | ] 299 | 300 | [[package]] 301 | name = "log" 302 | version = "0.4.22" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 305 | 306 | [[package]] 307 | name = "lru" 308 | version = "0.12.5" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" 311 | dependencies = [ 312 | "hashbrown", 313 | ] 314 | 315 | [[package]] 316 | name = "mio" 317 | version = "1.0.2" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 320 | dependencies = [ 321 | "hermit-abi", 322 | "libc", 323 | "log", 324 | "wasi 0.11.0+wasi-snapshot-preview1", 325 | "windows-sys 0.52.0", 326 | ] 327 | 328 | [[package]] 329 | name = "parking_lot" 330 | version = "0.12.3" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 333 | dependencies = [ 334 | "lock_api", 335 | "parking_lot_core", 336 | ] 337 | 338 | [[package]] 339 | name = "parking_lot_core" 340 | version = "0.9.10" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 343 | dependencies = [ 344 | "cfg-if", 345 | "libc", 346 | "redox_syscall", 347 | "smallvec", 348 | "windows-targets", 349 | ] 350 | 351 | [[package]] 352 | name = "paste" 353 | version = "1.0.15" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 356 | 357 | [[package]] 358 | name = "ppv-lite86" 359 | version = "0.2.21" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 362 | dependencies = [ 363 | "zerocopy", 364 | ] 365 | 366 | [[package]] 367 | name = "proc-macro2" 368 | version = "1.0.89" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 371 | dependencies = [ 372 | "unicode-ident", 373 | ] 374 | 375 | [[package]] 376 | name = "quote" 377 | version = "1.0.37" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 380 | dependencies = [ 381 | "proc-macro2", 382 | ] 383 | 384 | [[package]] 385 | name = "r-efi" 386 | version = "5.2.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 389 | 390 | [[package]] 391 | name = "rand" 392 | version = "0.9.0" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" 395 | dependencies = [ 396 | "rand_chacha", 397 | "rand_core", 398 | "zerocopy", 399 | ] 400 | 401 | [[package]] 402 | name = "rand_chacha" 403 | version = "0.9.0" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" 406 | dependencies = [ 407 | "ppv-lite86", 408 | "rand_core", 409 | ] 410 | 411 | [[package]] 412 | name = "rand_core" 413 | version = "0.9.3" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" 416 | dependencies = [ 417 | "getrandom", 418 | ] 419 | 420 | [[package]] 421 | name = "ratatui" 422 | version = "0.29.0" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" 425 | dependencies = [ 426 | "bitflags", 427 | "cassowary", 428 | "compact_str", 429 | "crossterm", 430 | "indoc", 431 | "instability", 432 | "itertools", 433 | "lru", 434 | "paste", 435 | "strum", 436 | "unicode-segmentation", 437 | "unicode-truncate", 438 | "unicode-width 0.2.0", 439 | ] 440 | 441 | [[package]] 442 | name = "redox_syscall" 443 | version = "0.5.7" 444 | source = "registry+https://github.com/rust-lang/crates.io-index" 445 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" 446 | dependencies = [ 447 | "bitflags", 448 | ] 449 | 450 | [[package]] 451 | name = "rustix" 452 | version = "0.38.38" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" 455 | dependencies = [ 456 | "bitflags", 457 | "errno", 458 | "libc", 459 | "linux-raw-sys", 460 | "windows-sys 0.52.0", 461 | ] 462 | 463 | [[package]] 464 | name = "rustversion" 465 | version = "1.0.18" 466 | source = "registry+https://github.com/rust-lang/crates.io-index" 467 | checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" 468 | 469 | [[package]] 470 | name = "ryu" 471 | version = "1.0.18" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 474 | 475 | [[package]] 476 | name = "scopeguard" 477 | version = "1.2.0" 478 | source = "registry+https://github.com/rust-lang/crates.io-index" 479 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 480 | 481 | [[package]] 482 | name = "signal-hook" 483 | version = "0.3.17" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 486 | dependencies = [ 487 | "libc", 488 | "signal-hook-registry", 489 | ] 490 | 491 | [[package]] 492 | name = "signal-hook-mio" 493 | version = "0.2.4" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" 496 | dependencies = [ 497 | "libc", 498 | "mio", 499 | "signal-hook", 500 | ] 501 | 502 | [[package]] 503 | name = "signal-hook-registry" 504 | version = "1.4.2" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 507 | dependencies = [ 508 | "libc", 509 | ] 510 | 511 | [[package]] 512 | name = "smallvec" 513 | version = "1.13.2" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 516 | 517 | [[package]] 518 | name = "static_assertions" 519 | version = "1.1.0" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 522 | 523 | [[package]] 524 | name = "strsim" 525 | version = "0.11.1" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 528 | 529 | [[package]] 530 | name = "strum" 531 | version = "0.26.3" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 534 | dependencies = [ 535 | "strum_macros", 536 | ] 537 | 538 | [[package]] 539 | name = "strum_macros" 540 | version = "0.26.4" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 543 | dependencies = [ 544 | "heck", 545 | "proc-macro2", 546 | "quote", 547 | "rustversion", 548 | "syn", 549 | ] 550 | 551 | [[package]] 552 | name = "sudoku-term" 553 | version = "1.0.1" 554 | dependencies = [ 555 | "clap", 556 | "crossterm", 557 | "rand", 558 | "ratatui", 559 | ] 560 | 561 | [[package]] 562 | name = "syn" 563 | version = "2.0.86" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "e89275301d38033efb81a6e60e3497e734dfcc62571f2854bf4b16690398824c" 566 | dependencies = [ 567 | "proc-macro2", 568 | "quote", 569 | "unicode-ident", 570 | ] 571 | 572 | [[package]] 573 | name = "unicode-ident" 574 | version = "1.0.13" 575 | source = "registry+https://github.com/rust-lang/crates.io-index" 576 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 577 | 578 | [[package]] 579 | name = "unicode-segmentation" 580 | version = "1.12.0" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" 583 | 584 | [[package]] 585 | name = "unicode-truncate" 586 | version = "1.1.0" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" 589 | dependencies = [ 590 | "itertools", 591 | "unicode-segmentation", 592 | "unicode-width 0.1.14", 593 | ] 594 | 595 | [[package]] 596 | name = "unicode-width" 597 | version = "0.1.14" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 600 | 601 | [[package]] 602 | name = "unicode-width" 603 | version = "0.2.0" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 606 | 607 | [[package]] 608 | name = "utf8parse" 609 | version = "0.2.2" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 612 | 613 | [[package]] 614 | name = "wasi" 615 | version = "0.11.0+wasi-snapshot-preview1" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 618 | 619 | [[package]] 620 | name = "wasi" 621 | version = "0.14.2+wasi-0.2.4" 622 | source = "registry+https://github.com/rust-lang/crates.io-index" 623 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 624 | dependencies = [ 625 | "wit-bindgen-rt", 626 | ] 627 | 628 | [[package]] 629 | name = "winapi" 630 | version = "0.3.9" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 633 | dependencies = [ 634 | "winapi-i686-pc-windows-gnu", 635 | "winapi-x86_64-pc-windows-gnu", 636 | ] 637 | 638 | [[package]] 639 | name = "winapi-i686-pc-windows-gnu" 640 | version = "0.4.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 643 | 644 | [[package]] 645 | name = "winapi-x86_64-pc-windows-gnu" 646 | version = "0.4.0" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 649 | 650 | [[package]] 651 | name = "windows-sys" 652 | version = "0.52.0" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 655 | dependencies = [ 656 | "windows-targets", 657 | ] 658 | 659 | [[package]] 660 | name = "windows-sys" 661 | version = "0.59.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 664 | dependencies = [ 665 | "windows-targets", 666 | ] 667 | 668 | [[package]] 669 | name = "windows-targets" 670 | version = "0.52.6" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 673 | dependencies = [ 674 | "windows_aarch64_gnullvm", 675 | "windows_aarch64_msvc", 676 | "windows_i686_gnu", 677 | "windows_i686_gnullvm", 678 | "windows_i686_msvc", 679 | "windows_x86_64_gnu", 680 | "windows_x86_64_gnullvm", 681 | "windows_x86_64_msvc", 682 | ] 683 | 684 | [[package]] 685 | name = "windows_aarch64_gnullvm" 686 | version = "0.52.6" 687 | source = "registry+https://github.com/rust-lang/crates.io-index" 688 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 689 | 690 | [[package]] 691 | name = "windows_aarch64_msvc" 692 | version = "0.52.6" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 695 | 696 | [[package]] 697 | name = "windows_i686_gnu" 698 | version = "0.52.6" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 701 | 702 | [[package]] 703 | name = "windows_i686_gnullvm" 704 | version = "0.52.6" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 707 | 708 | [[package]] 709 | name = "windows_i686_msvc" 710 | version = "0.52.6" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 713 | 714 | [[package]] 715 | name = "windows_x86_64_gnu" 716 | version = "0.52.6" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 719 | 720 | [[package]] 721 | name = "windows_x86_64_gnullvm" 722 | version = "0.52.6" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 725 | 726 | [[package]] 727 | name = "windows_x86_64_msvc" 728 | version = "0.52.6" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 731 | 732 | [[package]] 733 | name = "wit-bindgen-rt" 734 | version = "0.39.0" 735 | source = "registry+https://github.com/rust-lang/crates.io-index" 736 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 737 | dependencies = [ 738 | "bitflags", 739 | ] 740 | 741 | [[package]] 742 | name = "zerocopy" 743 | version = "0.8.24" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" 746 | dependencies = [ 747 | "zerocopy-derive", 748 | ] 749 | 750 | [[package]] 751 | name = "zerocopy-derive" 752 | version = "0.8.24" 753 | source = "registry+https://github.com/rust-lang/crates.io-index" 754 | checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" 755 | dependencies = [ 756 | "proc-macro2", 757 | "quote", 758 | "syn", 759 | ] 760 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sudoku-term" 3 | version = "1.0.1" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | clap = { version = "4.5.21", features = ["derive"] } 8 | crossterm = "0.28.1" 9 | rand = "0.9.0" 10 | ratatui = "0.29.0" 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 mehmet 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 game on your terminal! It generates board by itself, with 4 different difficulty levels. 2 | 3 | ![Screenshot](https://github.com/mhmtipek/sudoku-term/blob/7a1bd663c3cc29c683012780e5857f01f63a57e8/screenshot.png) 4 | 5 | Use arrow keys to navigate, use num keys to set values. Initial values can't be changed. If there is a conflict, it'll be highlighted. Other keys are described on game screen. 6 | Difficulty should be passed as argument. Here's --help output: 7 | 8 | ``` 9 | Usage: sudoku-term [OPTIONS] [DIFFICULTY] 10 | 11 | Arguments: 12 | [DIFFICULTY] Difficulty [default: medium] [possible values: easy, medium, hard] 13 | 14 | Options: 15 | --hide-elapsed-time Hide elapsed time 16 | -h, --help Print help 17 | -V, --version Print version 18 | 19 | ``` 20 | 21 | Installation: 22 | 23 | - Arch Linux ([AUR](https://aur.archlinux.org/packages/sudoku-term)): `paru -S sudoku-term` 24 | 25 | Thanks to projects :heart:: 26 | 27 | - [ratatui](https://github.com/ratatui/ratatui) 28 | - [clap](https://github.com/clap-rs/clap) 29 | - [crossterm](https://github.com/crossterm-rs/crossterm) 30 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhmtipek/sudoku-term/df1f0d7226914ae75c9963155307f9d35bc7a88a/screenshot.png -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | time::{Duration, Instant}, 4 | }; 5 | //use std::fmt; 6 | use clap::{Parser, ValueEnum}; 7 | use ratatui::{ 8 | crossterm::event::{self, KeyCode, KeyEventKind}, 9 | layout::Rect, 10 | style::{Color, Stylize}, 11 | text::Text, 12 | widgets::{Cell, Row, Table}, 13 | DefaultTerminal, 14 | }; 15 | use std::thread; 16 | 17 | pub mod sudoku; 18 | 19 | #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)] 20 | enum Difficulty { 21 | // Easy 22 | Easy, 23 | // Medium 24 | Medium, 25 | // Hard 26 | Hard, 27 | } 28 | 29 | #[derive(Parser, Debug)] 30 | #[command(version, about, long_about = None)] 31 | struct Args { 32 | /// Difficulty 33 | #[arg(value_enum, default_value_t = Difficulty::Medium)] 34 | difficulty: Difficulty, 35 | 36 | /// Hide elapsed time 37 | #[arg(long, default_value_t = false)] 38 | hide_elapsed_time: bool, 39 | } 40 | 41 | #[derive(Copy, Clone)] 42 | struct CellData { 43 | editable: bool, 44 | conflict: bool, 45 | highlight: bool, 46 | } 47 | 48 | struct Board { 49 | rows: [[u8; 9]; 9], 50 | cell_data: [[CellData; 9]; 9], 51 | current_cell: (u8, u8), 52 | difficulty: Difficulty, 53 | } 54 | 55 | impl<'a> Board { 56 | fn new(difficulty: Difficulty) -> Self { 57 | let current_cell = (0u8, 0u8); 58 | let rows: [[u8; 9]; 9] = [[2; 9]; 9]; 59 | let cell_data: [[CellData; 9]; 9] = [([CellData { 60 | conflict: false, 61 | editable: false, 62 | highlight: false, 63 | }; 9]); 9]; 64 | Self { 65 | rows, 66 | cell_data, 67 | current_cell, 68 | difficulty, 69 | } 70 | } 71 | 72 | fn create_table(&self) -> Table<'a> { 73 | let mut rows: Vec = Vec::with_capacity(9); 74 | let finished = sudoku::sudoku::is_finished(&self.rows); 75 | for row in 0..9 { 76 | let mut cells: Vec = Vec::with_capacity(9); 77 | for col in 0..9 { 78 | let bg_color = { 79 | if row == self.current_cell.0 && col == self.current_cell.1 { 80 | Color::Indexed(180) 81 | } else if self.cell_data[row as usize][col as usize].conflict { 82 | Color::Indexed(162) 83 | } else { 84 | let is_cell_darker = (row % 2) ^ (col % 2) == 0; 85 | let is_rect_darker = ((row / 3) % 2) ^ ((col / 3) % 2) == 0; 86 | if is_cell_darker { 87 | if is_rect_darker { 88 | if self.current_cell.0 == row || self.current_cell.1 == col { 89 | Color::Indexed(241) 90 | } else { 91 | Color::Indexed(240) 92 | } 93 | } else { 94 | if self.current_cell.0 == row || self.current_cell.1 == col { 95 | Color::Indexed(245) 96 | } else { 97 | Color::Indexed(244) 98 | } 99 | } 100 | } else { 101 | if is_rect_darker { 102 | if self.current_cell.0 == row || self.current_cell.1 == col { 103 | Color::Indexed(243) 104 | } else { 105 | Color::Indexed(242) 106 | } 107 | } else { 108 | if self.current_cell.0 == row || self.current_cell.1 == col { 109 | Color::Indexed(247) 110 | } else { 111 | Color::Indexed(246) 112 | } 113 | } 114 | } 115 | } 116 | }; 117 | let fg_color = { 118 | if finished { 119 | Color::Indexed(155) 120 | } else if row == self.current_cell.0 && col == self.current_cell.1 { 121 | if self.cell_data[row as usize][col as usize].editable { 122 | Color::Indexed(123) 123 | } else { 124 | Color::Black 125 | } 126 | } else if self.cell_data[row as usize][col as usize].highlight { 127 | Color::Indexed(230) 128 | } else { 129 | Color::Black 130 | } 131 | }; 132 | let mut char = String::from(" "); 133 | if self.rows[row as usize][col as usize] > 0 { 134 | char = format!("{}", self.rows[row as usize][col as usize]); 135 | } else if row as u8 == self.current_cell.0 && col as u8 == self.current_cell.1 { 136 | char = String::from("_"); 137 | } 138 | cells.insert( 139 | col as usize, 140 | Cell::from(Text::from(char).centered()) 141 | .bg(bg_color) 142 | .fg(fg_color), 143 | ); 144 | } 145 | rows.insert(row as usize, Row::new(cells)); 146 | } 147 | let widths = [3; 9]; 148 | Table::new(rows, widths) 149 | .column_spacing(0) 150 | .bg(Color::Indexed(0)) 151 | } 152 | 153 | fn set_current(&mut self, row: u8, col: u8) { 154 | self.current_cell = (row, col); 155 | self.update_cell_data(); 156 | } 157 | 158 | fn update_cell_data(&mut self) { 159 | for row in 0..9 { 160 | for col in 0..9 { 161 | self.cell_data[row as usize][col as usize].conflict = 162 | !sudoku::sudoku::is_valid(&self.rows, row, col); 163 | self.cell_data[row as usize][col as usize].highlight = sudoku::sudoku::are_related( 164 | (self.current_cell.0, self.current_cell.1), 165 | (row, col), 166 | ); 167 | } 168 | } 169 | } 170 | 171 | fn set_value(&mut self, val: u8) { 172 | self.rows[self.current_cell.0 as usize][self.current_cell.1 as usize] = val; 173 | self.update_cell_data(); 174 | } 175 | 176 | fn set_initial_rows(&mut self, rows: [[u8; 9]; 9]) { 177 | self.rows = rows; 178 | 179 | // Init cell data 180 | for row in 0..9 { 181 | for col in 0..9 { 182 | self.cell_data[row][col].editable = self.rows[row][col] == 0; 183 | } 184 | } 185 | self.update_cell_data(); 186 | } 187 | } 188 | 189 | fn main() -> io::Result<()> { 190 | let args = Args::parse(); 191 | 192 | let mut terminal = ratatui::init(); 193 | terminal.clear()?; 194 | 195 | let app_result = run(terminal, args.difficulty, args.hide_elapsed_time); 196 | ratatui::restore(); 197 | app_result 198 | } 199 | 200 | fn run( 201 | mut terminal: DefaultTerminal, 202 | difficulty: Difficulty, 203 | hide_elapsed_time: bool, 204 | ) -> io::Result<()> { 205 | let mut board: Board = Board::new(difficulty); 206 | 207 | let difficulty_val = match difficulty { 208 | Difficulty::Easy => 100, 209 | Difficulty::Medium => 140, 210 | Difficulty::Hard => 160, 211 | }; 212 | 213 | // Start the thread which creates the initial board here. 214 | let init_thread_handle = 215 | thread::spawn(move || sudoku::sudoku::generate_initial_board(difficulty_val)); 216 | 217 | // The loop until initial board is created 218 | let mut counter = 0; 219 | let board_generation_start_time = Instant::now(); 220 | loop { 221 | if init_thread_handle.is_finished() { 222 | break; 223 | } 224 | 225 | // Animate text 226 | let text = "Generating board ... "; 227 | let shift = counter % text.len(); 228 | let print_text = format!("{}{}", &text[shift..], &text[0..shift]); 229 | 230 | terminal.draw(|frame| { 231 | frame.render_widget( 232 | Text::from(print_text).centered(), 233 | Rect::new(0, frame.area().height / 2, frame.area().width, 1), 234 | ); 235 | if board_generation_start_time.elapsed() > Duration::from_secs(10) { 236 | frame.render_widget( 237 | Text::from("It may take time depending on difficulty").centered(), 238 | Rect::new(0, frame.area().height - 3, frame.area().width, 1), 239 | ); 240 | } 241 | })?; 242 | 243 | thread::sleep(Duration::from_millis(100)); 244 | counter = counter + 1; 245 | } 246 | board.set_initial_rows(init_thread_handle.join().unwrap()); 247 | 248 | // The game loop 249 | let start_time = Instant::now(); 250 | let mut finish_time = start_time; 251 | let mut finished = false; 252 | let mut undo_data: Option<(u8, u8, u8)> = Option::None; // row, col, val 253 | loop { 254 | terminal.draw(|frame| { 255 | let board_rect = Rect::new( 256 | (frame.area().width as u16 - 27) / 2, 257 | (frame.area().height as u16 - 9) / 2, 258 | 27, 259 | 9, 260 | ); 261 | frame.render_widget(board.create_table(), board_rect); 262 | if !hide_elapsed_time { 263 | let secs = { 264 | if start_time == finish_time { 265 | start_time.elapsed().as_secs() 266 | } else { 267 | finish_time.duration_since(start_time).as_secs() 268 | } 269 | }; 270 | let mut time_label = Text::from(format!("{} secs", secs)).right_aligned(); 271 | if secs >= 60 { 272 | if secs >= 120 { 273 | time_label = Text::from(format!("{} mins {} secs", secs / 60, secs % 60)) 274 | .right_aligned(); 275 | } else { 276 | time_label = 277 | Text::from(format!("1 min {} secs", secs % 60)).right_aligned(); 278 | } 279 | } 280 | frame.render_widget( 281 | time_label, 282 | Rect::new( 283 | frame.area().width / 2, 284 | frame.area().height - 1, 285 | frame.area().width / 2, 286 | 1, 287 | ), 288 | ); 289 | } 290 | let difficulty_label = Text::from(format!("{:?}", board.difficulty)).left_aligned(); 291 | frame.render_widget( 292 | difficulty_label, 293 | Rect::new(0, frame.area().height - 1, frame.area().width / 2, 1), 294 | ); 295 | frame.render_widget( 296 | Text::from("d: delete, u: undo, q: quit").centered(), 297 | Rect::new(0, frame.area().height - 1, frame.area().width, 1), 298 | ); 299 | })?; 300 | 301 | match event::poll(Duration::from_millis(200)) { 302 | Ok(true) => { 303 | if let event::Event::Key(key) = event::read()? { 304 | if key.kind == KeyEventKind::Press { 305 | if key.code == KeyCode::Char('q') { 306 | return Ok(()); 307 | } else if finished { 308 | continue; 309 | } else if key.code == KeyCode::Char('u') { 310 | match undo_data { 311 | Some(data) => { 312 | board.set_current(data.0, data.1); 313 | board.set_value(data.2); 314 | } 315 | None => {} 316 | } 317 | undo_data = Option::None; 318 | } else if key.code == KeyCode::Char('d') { 319 | if board.cell_data[board.current_cell.0 as usize] 320 | [board.current_cell.1 as usize] 321 | .editable 322 | { 323 | board.set_value(0); 324 | } 325 | } else if key.code == KeyCode::Right { 326 | board.set_current(board.current_cell.0, (board.current_cell.1 + 1) % 9); 327 | } else if key.code == KeyCode::Left { 328 | if board.current_cell.1 == 0 { 329 | board.set_current(board.current_cell.0, 8); 330 | } else { 331 | board.set_current(board.current_cell.0, board.current_cell.1 - 1); 332 | } 333 | } else if key.code == KeyCode::Up { 334 | if board.current_cell.0 == 0 { 335 | board.set_current(8, board.current_cell.1); 336 | } else { 337 | board.set_current(board.current_cell.0 - 1, board.current_cell.1); 338 | } 339 | } else if key.code == KeyCode::Down { 340 | board.set_current((board.current_cell.0 + 1) % 9, board.current_cell.1); 341 | } else if key.code >= KeyCode::Char('1') && key.code <= KeyCode::Char('9') { 342 | if board.cell_data[board.current_cell.0 as usize] 343 | [board.current_cell.1 as usize] 344 | .editable 345 | { 346 | undo_data = Some(( 347 | board.current_cell.0, 348 | board.current_cell.1, 349 | board.rows[board.current_cell.0 as usize] 350 | [board.current_cell.1 as usize], 351 | )); 352 | board.set_value(key.code.to_string().parse().unwrap()); 353 | finished = sudoku::sudoku::is_finished(&board.rows); 354 | if finished { 355 | finish_time = Instant::now(); 356 | } 357 | } 358 | } 359 | } 360 | } 361 | } 362 | _ => {} 363 | } 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/sudoku.rs: -------------------------------------------------------------------------------- 1 | pub mod sudoku { 2 | use rand::prelude::*; 3 | use std::boxed::Box; 4 | use std::collections::HashSet; 5 | use std::collections::LinkedList; 6 | use std::thread; 7 | 8 | #[derive(Debug)] 9 | struct Node { 10 | row: u8, 11 | col: u8, 12 | board: [[u8; 9]; 9], 13 | } 14 | 15 | impl Node { 16 | fn new(row: u8, col: u8, board: [[u8; 9]; 9]) -> Self { 17 | Self { row, col, board } 18 | } 19 | } 20 | 21 | impl Clone for Node { 22 | fn clone(&self) -> Self { 23 | let row = self.row; 24 | let col = self.col; 25 | let board = self.board.clone(); 26 | Self { row, col, board } 27 | } 28 | } 29 | 30 | // TODO: write unit test 31 | pub fn available_values(board: &[[u8; 9]; 9], row: u8, col: u8) -> Vec { 32 | let val = board[row as usize][col as usize]; 33 | // Return current value in case it's non zero 34 | if val > 0 { 35 | return Vec::from([val]); 36 | } 37 | 38 | // Note that value zero means empty 39 | let mut values = HashSet::from([1, 2, 3, 4, 5, 6, 7, 8, 9]); 40 | 41 | // Check same row 42 | for i in 0..9 { 43 | values.remove(&board[row as usize][i]); 44 | } 45 | // Check same column 46 | for i in 0..9 { 47 | values.remove(&board[i][col as usize]); 48 | } 49 | // Check same rect 50 | for r in ((row / 3) * 3)..((row / 3) * 3 + 3) { 51 | for c in ((col / 3) * 3)..((col / 3) * 3 + 3) { 52 | values.remove(&board[r as usize][c as usize]); 53 | } 54 | } 55 | 56 | values.into_iter().collect() 57 | } 58 | 59 | // Checks whether index 1 and index 2 are either; 60 | // in same row 61 | // in same col 62 | // in same rect 63 | pub fn are_related(index1: (u8, u8), index2: (u8, u8)) -> bool { 64 | // Check same row 65 | if index1.0 == index2.0 { 66 | return true; 67 | } 68 | // Check same col 69 | if index1.1 == index2.1 { 70 | return true; 71 | } 72 | // Check same rect 73 | if index1.0 / 3 == index2.0 / 3 && index1.1 / 3 == index2.1 / 3 { 74 | return true; 75 | } 76 | 77 | false 78 | } 79 | 80 | // TODO: write unit test 81 | pub fn is_valid(board: &[[u8; 9]; 9], row: u8, col: u8) -> bool { 82 | let value = board[row as usize][col as usize]; 83 | 84 | if value == 0 { 85 | return true; 86 | } 87 | 88 | // Check same col 89 | for row_i in 0..9 { 90 | if row_i == row { 91 | continue; 92 | } 93 | if board[row_i as usize][col as usize] == value { 94 | return false; 95 | } 96 | } 97 | 98 | // Check same row 99 | for col_i in 0..9 { 100 | if col_i == col { 101 | continue; 102 | } 103 | if board[row as usize][col_i as usize] == value { 104 | return false; 105 | } 106 | } 107 | 108 | // Check same rect 109 | for row_i in ((row / 3) * 3)..((row / 3) * 3 + 3) { 110 | for col_i in ((col / 3) * 3)..((col / 3) * 3 + 3) { 111 | if row_i == row && col_i == col { 112 | continue; 113 | } 114 | if board[row_i as usize][col_i as usize] == value { 115 | return false; 116 | } 117 | } 118 | } 119 | 120 | true 121 | } 122 | 123 | // TODO: write unit test 124 | pub fn is_finished(board: &[[u8; 9]; 9]) -> bool { 125 | for row in 0..9 { 126 | for col in 0..9 { 127 | if board[row as usize][col as usize] == 0 { 128 | return false; 129 | } 130 | if !is_valid(board, row, col) { 131 | return false; 132 | } 133 | } 134 | } 135 | 136 | true 137 | } 138 | 139 | // Performs depth first search on node 140 | // @return solutions 141 | fn search(node: &mut Box) -> Vec<[[u8; 9]; 9]> { 142 | let mut all_nodes: Vec<[[u8; 9]; 9]> = Vec::new(); 143 | 144 | // Use linked list as stack because we'll be worknig with last element all the time 145 | let mut nodes_stack: LinkedList> = LinkedList::new(); 146 | for child_node in create_children(node) { 147 | nodes_stack.push_back(child_node); 148 | } 149 | 150 | while !nodes_stack.is_empty() { 151 | let node = nodes_stack.pop_back().unwrap(); 152 | 153 | if node.row == 8 { 154 | if node.col == 8 { 155 | all_nodes.push(node.board); 156 | continue; 157 | } 158 | } 159 | 160 | for child_node in create_children(&node) { 161 | nodes_stack.push_back(child_node); 162 | } 163 | } 164 | 165 | all_nodes 166 | } 167 | 168 | fn create_children(node: &Box) -> Vec> { 169 | let child_row; 170 | let child_col; 171 | 172 | if node.col == 8 { 173 | if node.row == 8 { 174 | return Vec::new(); // We reached end of the board 175 | } 176 | child_row = node.row + 1; 177 | child_col = 0; 178 | } else { 179 | child_row = node.row; 180 | child_col = node.col + 1; 181 | } 182 | 183 | let mut all_nodes: Vec> = Vec::new(); 184 | 185 | for val in available_values(&node.board, child_row, child_col) { 186 | let mut child_board = node.board.clone(); 187 | child_board[child_row as usize][child_col as usize] = val; 188 | all_nodes.push(Box::new(Node::new(child_row, child_col, child_board))); 189 | } 190 | 191 | all_nodes 192 | } 193 | 194 | fn all_solutions(board: &[[u8; 9]; 9]) -> Vec<[[u8; 9]; 9]> { 195 | let mut solutions: Vec<[[u8; 9]; 9]> = Vec::new(); 196 | 197 | // Create initial nodes. Ideally it'll be one for each thread but 198 | // it can be a little more, depending on available values for a cell 199 | let ideal_thread_count = thread::available_parallelism().unwrap().get(); 200 | 201 | let mut head_nodes: Vec> = Vec::new(); 202 | 203 | for val in available_values(&board, 0, 0) { 204 | let mut copy_board = board.clone(); 205 | copy_board[0][0] = val; 206 | head_nodes.push(Box::new(Node::new(0, 0, copy_board))); 207 | } 208 | 209 | let mut c = 0; 210 | while head_nodes.len() < ideal_thread_count { 211 | if c == 30 { 212 | // If it takes too long to reach, just break 213 | break; 214 | } 215 | let node; 216 | match head_nodes.pop() { 217 | Some(_node) => node = _node, 218 | None => { 219 | break; 220 | } 221 | } 222 | if node.row == 8 && node.col == 8 { 223 | solutions.push(node.board.clone()); 224 | } 225 | let mut child_nodes = create_children(&node); 226 | head_nodes.append(&mut child_nodes); 227 | c = c + 1; 228 | } 229 | 230 | let mut join_handles: Vec>> = Vec::new(); 231 | while !head_nodes.is_empty() { 232 | let mut node = head_nodes.pop().unwrap(); 233 | join_handles.push(thread::spawn(move || search(&mut node))); 234 | } 235 | 236 | for handle in join_handles { 237 | match handle.join() { 238 | Ok(boards) => { 239 | let mut mut_boards = boards.clone(); 240 | solutions.append(&mut mut_boards); 241 | } 242 | Err(_) => { 243 | eprintln!("Thread failed") 244 | } 245 | } 246 | } 247 | 248 | solutions 249 | } 250 | 251 | fn adjust_difficulty(solved_board: &[[u8; 9]; 9], difficulty: u8) -> (u8, [[u8; 9]; 9]) { 252 | let mut board = solved_board.clone(); 253 | let mut current_difficulty: u8 = 0; // 0: easiest, 255: hardest 254 | 255 | // Remove random cell and verify board has still one solution until desired difficulty is reached 256 | let mut rng = rand::rng(); 257 | let mut all_indexes: Vec = (0..81).collect(); 258 | let mut current_index = 0; 259 | all_indexes.shuffle(&mut rng); 260 | while current_difficulty < difficulty { 261 | if current_index >= all_indexes.len() { 262 | break; 263 | } 264 | let row: u8 = all_indexes[current_index] / 9; 265 | let col: u8 = all_indexes[current_index] % 9; 266 | 267 | let val: u8 = board[row as usize][col as usize]; 268 | board[row as usize][col as usize] = 0; // Remove data from cell 269 | let solutions = all_solutions(&board); 270 | if solutions.len() > 1 { 271 | // Revert removal 272 | board[row as usize][col as usize] = val; 273 | } else { 274 | // Since we're working on solved board, solution count should be at least one. 275 | // Else case assumes solution count is one. 276 | current_difficulty = current_difficulty + 3; 277 | } 278 | current_index = current_index + 1; 279 | } 280 | 281 | (current_difficulty, board) 282 | } 283 | 284 | // TODO: write unit test 285 | // difficulty is in between 0-255 286 | pub fn generate_initial_board(difficulty: u8) -> [[u8; 9]; 9] { 287 | let mut solutions: Vec<[[u8; 9]; 9]> = Vec::new(); 288 | 289 | let mut try_count = 1; 290 | let mut rng = rand::rng(); 291 | while solutions.is_empty() { 292 | // Value 0 (zero) means cell is empty 293 | let mut board: [[u8; 9]; 9] = [[0; 9]; 9]; 294 | 295 | // Assign random but valid initial values 296 | let mut all_indexes = [0; 81]; 297 | for i in 1..81 { 298 | all_indexes[i] = i; 299 | } 300 | all_indexes.shuffle(&mut rng); 301 | // 30 is magic number, an optimized value 302 | for i in 0..30 { 303 | let row = all_indexes[i] / 9; 304 | let col = all_indexes[i] % 9; 305 | let available_values = available_values(&board, row as u8, col as u8); 306 | if available_values.is_empty() { 307 | continue; 308 | } 309 | let index = rng.random::() % available_values.len() as u8; 310 | board[row][col] = available_values[index as usize]; 311 | } 312 | 313 | solutions = all_solutions(&board); 314 | try_count = try_count + 1; 315 | } 316 | 317 | let mut join_handles: Vec> = Vec::new(); 318 | for solved_board in solutions { 319 | join_handles.push(thread::spawn(move || { 320 | adjust_difficulty(&solved_board, difficulty) 321 | })); 322 | } 323 | 324 | let mut game_board: [[u8; 9]; 9] = [[0; 9]; 9]; 325 | let mut best_match_score = 255; 326 | for handle in join_handles { 327 | match handle.join() { 328 | Ok((difficulty_score, board)) => { 329 | let score = difficulty_score.abs_diff(difficulty); 330 | if score < best_match_score { 331 | best_match_score = score; 332 | game_board = board.clone(); 333 | } 334 | } 335 | Err(_) => { 336 | eprintln!("Thread failed"); 337 | } 338 | } 339 | } 340 | 341 | game_board 342 | } 343 | } 344 | --------------------------------------------------------------------------------