├── .firebaserc ├── .github └── workflows │ └── rust.yml ├── .gitignore ├── .prettierrc ├── Cargo.lock ├── Cargo.toml ├── README.md ├── firebase.json ├── package-lock.json ├── package.json ├── src ├── board.rs ├── bot.rs ├── lib.rs ├── main.rs └── train.rs ├── tsconfig.json └── www ├── 404.html ├── favicon.ico ├── index.html └── src ├── actions.ts ├── board.ts ├── game.ts ├── game.worker.ts └── main.ts /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "wasm-games" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: jetli/wasm-pack-action@v0.3.0 19 | 20 | - name: Build 21 | run: wasm-pack build 22 | 23 | - name: Run tests 24 | run: cargo test --verbose 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | /pkg 4 | .firebase 5 | /build 6 | /node_modules 7 | /www/pkg 8 | /www/dist 9 | /www/*_brain.bin -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 160 4 | } 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "atty" 7 | version = "0.2.14" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 10 | dependencies = [ 11 | "hermit-abi", 12 | "libc", 13 | "winapi", 14 | ] 15 | 16 | [[package]] 17 | name = "autocfg" 18 | version = "1.0.1" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 21 | 22 | [[package]] 23 | name = "bincode" 24 | version = "1.3.3" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 27 | dependencies = [ 28 | "serde", 29 | ] 30 | 31 | [[package]] 32 | name = "bitflags" 33 | version = "1.3.2" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 36 | 37 | [[package]] 38 | name = "brain_games" 39 | version = "0.1.0" 40 | dependencies = [ 41 | "bincode", 42 | "clap", 43 | "getrandom", 44 | "rand", 45 | "wasm-bindgen", 46 | "wee_alloc", 47 | ] 48 | 49 | [[package]] 50 | name = "bumpalo" 51 | version = "3.6.1" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" 54 | 55 | [[package]] 56 | name = "cfg-if" 57 | version = "0.1.10" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 60 | 61 | [[package]] 62 | name = "cfg-if" 63 | version = "1.0.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 66 | 67 | [[package]] 68 | name = "clap" 69 | version = "3.0.5" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "f6f34b09b9ee8c7c7b400fe2f8df39cafc9538b03d6ba7f4ae13e4cb90bfbb7d" 72 | dependencies = [ 73 | "atty", 74 | "bitflags", 75 | "clap_derive", 76 | "indexmap", 77 | "lazy_static", 78 | "os_str_bytes", 79 | "strsim", 80 | "termcolor", 81 | "textwrap", 82 | ] 83 | 84 | [[package]] 85 | name = "clap_derive" 86 | version = "3.0.5" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "41a0645a430ec9136d2d701e54a95d557de12649a9dd7109ced3187e648ac824" 89 | dependencies = [ 90 | "heck", 91 | "proc-macro-error", 92 | "proc-macro2", 93 | "quote", 94 | "syn", 95 | ] 96 | 97 | [[package]] 98 | name = "getrandom" 99 | version = "0.2.3" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" 102 | dependencies = [ 103 | "cfg-if 1.0.0", 104 | "js-sys", 105 | "libc", 106 | "wasi", 107 | "wasm-bindgen", 108 | ] 109 | 110 | [[package]] 111 | name = "hashbrown" 112 | version = "0.11.2" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" 115 | 116 | [[package]] 117 | name = "heck" 118 | version = "0.4.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" 121 | 122 | [[package]] 123 | name = "hermit-abi" 124 | version = "0.1.19" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 127 | dependencies = [ 128 | "libc", 129 | ] 130 | 131 | [[package]] 132 | name = "indexmap" 133 | version = "1.7.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" 136 | dependencies = [ 137 | "autocfg", 138 | "hashbrown", 139 | ] 140 | 141 | [[package]] 142 | name = "js-sys" 143 | version = "0.3.55" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" 146 | dependencies = [ 147 | "wasm-bindgen", 148 | ] 149 | 150 | [[package]] 151 | name = "lazy_static" 152 | version = "1.4.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 155 | 156 | [[package]] 157 | name = "libc" 158 | version = "0.2.66" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 161 | 162 | [[package]] 163 | name = "log" 164 | version = "0.4.14" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" 167 | dependencies = [ 168 | "cfg-if 1.0.0", 169 | ] 170 | 171 | [[package]] 172 | name = "memchr" 173 | version = "2.4.1" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 176 | 177 | [[package]] 178 | name = "memory_units" 179 | version = "0.4.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" 182 | 183 | [[package]] 184 | name = "os_str_bytes" 185 | version = "6.0.0" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" 188 | dependencies = [ 189 | "memchr", 190 | ] 191 | 192 | [[package]] 193 | name = "ppv-lite86" 194 | version = "0.2.16" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 197 | 198 | [[package]] 199 | name = "proc-macro-error" 200 | version = "1.0.4" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 203 | dependencies = [ 204 | "proc-macro-error-attr", 205 | "proc-macro2", 206 | "quote", 207 | "syn", 208 | "version_check", 209 | ] 210 | 211 | [[package]] 212 | name = "proc-macro-error-attr" 213 | version = "1.0.4" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 216 | dependencies = [ 217 | "proc-macro2", 218 | "quote", 219 | "version_check", 220 | ] 221 | 222 | [[package]] 223 | name = "proc-macro2" 224 | version = "1.0.36" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" 227 | dependencies = [ 228 | "unicode-xid", 229 | ] 230 | 231 | [[package]] 232 | name = "quote" 233 | version = "1.0.14" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" 236 | dependencies = [ 237 | "proc-macro2", 238 | ] 239 | 240 | [[package]] 241 | name = "rand" 242 | version = "0.8.4" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" 245 | dependencies = [ 246 | "libc", 247 | "rand_chacha", 248 | "rand_core", 249 | "rand_hc", 250 | ] 251 | 252 | [[package]] 253 | name = "rand_chacha" 254 | version = "0.3.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 257 | dependencies = [ 258 | "ppv-lite86", 259 | "rand_core", 260 | ] 261 | 262 | [[package]] 263 | name = "rand_core" 264 | version = "0.6.3" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" 267 | dependencies = [ 268 | "getrandom", 269 | ] 270 | 271 | [[package]] 272 | name = "rand_hc" 273 | version = "0.3.1" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" 276 | dependencies = [ 277 | "rand_core", 278 | ] 279 | 280 | [[package]] 281 | name = "serde" 282 | version = "1.0.133" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" 285 | 286 | [[package]] 287 | name = "strsim" 288 | version = "0.10.0" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 291 | 292 | [[package]] 293 | name = "syn" 294 | version = "1.0.85" 295 | source = "registry+https://github.com/rust-lang/crates.io-index" 296 | checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" 297 | dependencies = [ 298 | "proc-macro2", 299 | "quote", 300 | "unicode-xid", 301 | ] 302 | 303 | [[package]] 304 | name = "termcolor" 305 | version = "1.1.2" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" 308 | dependencies = [ 309 | "winapi-util", 310 | ] 311 | 312 | [[package]] 313 | name = "textwrap" 314 | version = "0.14.2" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" 317 | 318 | [[package]] 319 | name = "unicode-xid" 320 | version = "0.2.0" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 323 | 324 | [[package]] 325 | name = "version_check" 326 | version = "0.9.4" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 329 | 330 | [[package]] 331 | name = "wasi" 332 | version = "0.10.2+wasi-snapshot-preview1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 335 | 336 | [[package]] 337 | name = "wasm-bindgen" 338 | version = "0.2.78" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" 341 | dependencies = [ 342 | "cfg-if 1.0.0", 343 | "wasm-bindgen-macro", 344 | ] 345 | 346 | [[package]] 347 | name = "wasm-bindgen-backend" 348 | version = "0.2.78" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" 351 | dependencies = [ 352 | "bumpalo", 353 | "lazy_static", 354 | "log", 355 | "proc-macro2", 356 | "quote", 357 | "syn", 358 | "wasm-bindgen-shared", 359 | ] 360 | 361 | [[package]] 362 | name = "wasm-bindgen-macro" 363 | version = "0.2.78" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" 366 | dependencies = [ 367 | "quote", 368 | "wasm-bindgen-macro-support", 369 | ] 370 | 371 | [[package]] 372 | name = "wasm-bindgen-macro-support" 373 | version = "0.2.78" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" 376 | dependencies = [ 377 | "proc-macro2", 378 | "quote", 379 | "syn", 380 | "wasm-bindgen-backend", 381 | "wasm-bindgen-shared", 382 | ] 383 | 384 | [[package]] 385 | name = "wasm-bindgen-shared" 386 | version = "0.2.78" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" 389 | 390 | [[package]] 391 | name = "wee_alloc" 392 | version = "0.4.5" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" 395 | dependencies = [ 396 | "cfg-if 0.1.10", 397 | "libc", 398 | "memory_units", 399 | "winapi", 400 | ] 401 | 402 | [[package]] 403 | name = "winapi" 404 | version = "0.3.9" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 407 | dependencies = [ 408 | "winapi-i686-pc-windows-gnu", 409 | "winapi-x86_64-pc-windows-gnu", 410 | ] 411 | 412 | [[package]] 413 | name = "winapi-i686-pc-windows-gnu" 414 | version = "0.4.0" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 417 | 418 | [[package]] 419 | name = "winapi-util" 420 | version = "0.1.5" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 423 | dependencies = [ 424 | "winapi", 425 | ] 426 | 427 | [[package]] 428 | name = "winapi-x86_64-pc-windows-gnu" 429 | version = "0.4.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 432 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "brain_games" 3 | version = "0.1.0" 4 | authors = ["dblue "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [dependencies] 11 | rand = { version = "0.8.4" } 12 | getrandom = { version = "0.2", features = ["js"] } 13 | wasm-bindgen = "0.2" 14 | bincode = "1.3.2" 15 | wee_alloc = { version = "0.4.5", optional = true} 16 | clap = { version = "3.0.5", features = ["derive"] } 17 | 18 | [profile.release] 19 | lto = true 20 | opt-level = "s" 21 | 22 | [features] 23 | default = ["wee_alloc"] 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # "Brain" games in rust 2 | 3 | [Try it here.](https://wasm-games.web.app/) 4 | 5 | A simple tic tac toe bot. 6 | The program that trains two "dumb" bots, one playing Xs and one playing Os. 7 | The training portion of the program is written in rust and compiled with wasm-pack. 8 | You can run the training program in the browser directly or build them directly and import the brains. 9 | 10 | Those output files are loading via a Web Worker to keep the training loop off of the main thread. 11 | 12 | ## Running in browser 13 | 14 | ```BASH 15 | npm run build 16 | npm start 17 | ``` 18 | 19 | Then point a local server to www and play right away or train first. 20 | The wasm specific code is in "lib" and the standard rust program starts in "main" (will be updated soon to play and train). 21 | 22 | ## Prebuilding Brain 23 | 24 | If you want to prebuild brains you can run the rust program directly. 25 | 26 | ```BASH 27 | cargo run --release 28 | ``` 29 | 30 | ## NOTE 31 | 32 | currently only works in chrome or chromium since I am using the "module" type of web worker. Will work fine in others with some bundling. 33 | 34 | ## Explanation: 35 | 36 | The implementation here isn't machine learning as we would think about it with neural networks and the like. It is very brute force and isn't something that would scale much past something like "tic tac toe" since it requires the bot to be aware of all potential game states. This bot is an emulation of the match box "computer" described in the video below. A fun exercise and nothing more. 37 | 38 | [![Alt text](https://img.youtube.com/vi/R9c-_neaxeU/0.jpg)](https://www.youtube.com/watch?v=R9c-_neaxeU) 39 | 40 | ## Observations 41 | 42 | When training the bot the first time (500000) games. You will generally end up with ~10000 wins for Xs and ~5000 for Os with the rest being ties. 43 | The bots should eventually no longer being 44 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "www/dist", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brain-games-rust", 3 | "version": "1.0.0", 4 | "description": "[Try it here.](https://wasm-games.web.app/)", 5 | "main": "index.js", 6 | "scripts": { 7 | "preinstall": "npm run build:wasm", 8 | "start": "vite ./www", 9 | "build": "npm run build:wasm && vite build ./www", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "build:wasm": "wasm-pack build --target web -d www/pkg --release" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/deebloo/brain-games-rust.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/deebloo/brain-games-rust/issues" 21 | }, 22 | "homepage": "https://github.com/deebloo/brain-games-rust#readme", 23 | "dependencies": { 24 | "@joist/di": "2.0.1", 25 | "@joist/observable": "2.0.1", 26 | "@joist/query": "2.0.1", 27 | "@joist/styled": "2.0.1" 28 | }, 29 | "devDependencies": { 30 | "@snowpack/plugin-typescript": "^1.2.1", 31 | "firebase-tools": "^9.23.0", 32 | "prettier": "^2.7.1", 33 | "typescript": "^4.5.2", 34 | "vite": "^3.0.9" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/board.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Debug, PartialEq, Copy, Clone)] 4 | pub enum Player { 5 | X, 6 | O, 7 | } 8 | 9 | #[derive(Debug, PartialEq, Copy, Clone)] 10 | pub enum Space { 11 | Empty, 12 | Player(Player), 13 | } 14 | 15 | impl Space { 16 | pub fn to_str(&self) -> &str { 17 | match self { 18 | Space::Player(Player::X) => "X", 19 | Space::Player(Player::O) => "O", 20 | Space::Empty => "-", 21 | } 22 | } 23 | } 24 | 25 | impl fmt::Display for Space { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | write!(f, "{}", self.to_str()) 28 | } 29 | } 30 | 31 | #[derive(Debug, PartialEq, Clone, Copy)] 32 | pub enum GameResult { 33 | Winner(Player), 34 | Tie, 35 | } 36 | 37 | impl fmt::Display for GameResult { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | match self { 40 | GameResult::Winner(Player::X) => write!(f, "X"), 41 | GameResult::Winner(Player::O) => write!(f, "O"), 42 | GameResult::Tie => write!(f, "TIE"), 43 | } 44 | } 45 | } 46 | 47 | pub type BoardState = Vec; 48 | 49 | #[derive(Debug, PartialEq)] 50 | pub struct Move { 51 | pub key: u32, 52 | pub space: Space, 53 | pub index: usize, 54 | } 55 | 56 | pub struct Board { 57 | pub rows: usize, 58 | pub cols: usize, 59 | pub spaces: BoardState, 60 | pub moves: Vec, 61 | } 62 | 63 | impl Board { 64 | pub fn new(rows: usize, cols: usize) -> Self { 65 | let mut spaces: Vec = Vec::new(); 66 | 67 | for _ in 0..(rows * cols) { 68 | spaces.push(Space::Empty); 69 | } 70 | 71 | Self { 72 | rows, 73 | cols, 74 | spaces, 75 | moves: vec![], 76 | } 77 | } 78 | 79 | pub fn key_as_u32(&self) -> u32 { 80 | let board_size = self.spaces.len() as u32; 81 | let mut index = 0; 82 | let mut total = 0; 83 | 84 | for space in &self.spaces { 85 | let space_value = match space { 86 | &Space::Player(Player::X) => 2, 87 | &Space::Player(Player::O) => 1, 88 | &Space::Empty => 0, 89 | }; 90 | 91 | if space_value > 0 { 92 | total += space_value * board_size.pow(index); 93 | } 94 | 95 | index += 1; 96 | } 97 | 98 | total 99 | } 100 | 101 | #[allow(unused)] 102 | pub fn key_as_string(&self) -> String { 103 | let mut result = String::new(); 104 | 105 | for space in &self.spaces { 106 | result = result + &space.to_string(); 107 | } 108 | 109 | result 110 | } 111 | 112 | pub fn get_available_spaces(&self) -> Vec { 113 | self.spaces 114 | .iter() 115 | .enumerate() 116 | .filter_map(|(i, space)| { 117 | if *space == Space::Empty { 118 | Some(i) 119 | } else { 120 | None 121 | } 122 | }) 123 | .collect::>() 124 | } 125 | 126 | pub fn set_by_index(&mut self, index: usize, space: Space) -> Result<(), ()> { 127 | if let Some(current_space) = self.spaces.get(index) { 128 | if current_space == &Space::Empty { 129 | let key = self.key_as_u32(); 130 | 131 | self.moves.push(Move { index, key, space }); 132 | 133 | self.spaces[index] = space; 134 | 135 | Ok(()) 136 | } else { 137 | Err(()) 138 | } 139 | } else { 140 | Err(()) 141 | } 142 | } 143 | 144 | #[allow(unused)] 145 | pub fn set(&mut self, row: usize, col: usize, space: Space) -> Result<(), ()> { 146 | let index = row * self.rows + col; 147 | 148 | self.set_by_index(index, space) 149 | } 150 | 151 | pub fn moves_available(&self) -> bool { 152 | self.spaces.iter().any(|&space| space == Space::Empty) 153 | } 154 | 155 | pub fn determine_winner(&self) -> Option { 156 | let mut rl_res: Vec = Vec::new(); 157 | let mut lr_res: Vec = Vec::new(); 158 | 159 | for x in 0..self.rows { 160 | rl_res.push(self.spaces[x * self.rows + x]); 161 | lr_res.push(self.spaces[x * self.rows + (self.rows - 1 - x)]); 162 | 163 | let mut row_res: Vec = Vec::new(); 164 | let mut col_res: Vec = Vec::new(); 165 | 166 | for y in 0..self.cols { 167 | row_res.push(self.spaces[x * self.rows + y]); 168 | col_res.push(self.spaces[y * self.rows + x]); 169 | } 170 | 171 | if row_res.windows(2).all(|w| w[0] == w[1]) { 172 | if let Space::Player(player) = row_res[0] { 173 | return Some(GameResult::Winner(player)); 174 | } 175 | } 176 | 177 | if col_res.windows(2).all(|w| w[0] == w[1]) { 178 | if let Space::Player(player) = col_res[0] { 179 | return Some(GameResult::Winner(player)); 180 | } 181 | } 182 | } 183 | 184 | if rl_res.windows(2).all(|w| w[0] == w[1]) { 185 | if let Space::Player(player) = rl_res[0] { 186 | return Some(GameResult::Winner(player)); 187 | } 188 | } 189 | 190 | if lr_res.windows(2).all(|w| w[0] == w[1]) { 191 | if let Space::Player(player) = lr_res[0] { 192 | return Some(GameResult::Winner(player)); 193 | } 194 | } 195 | 196 | if !self.moves_available() { 197 | return Some(GameResult::Tie); 198 | } 199 | 200 | None 201 | } 202 | } 203 | 204 | impl fmt::Display for Board { 205 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 206 | let mut grid = String::new(); 207 | 208 | for (i, space) in self.spaces.iter().enumerate() { 209 | if i % self.rows == 0 { 210 | grid += "\n" 211 | } 212 | 213 | grid += space.to_str(); 214 | grid += " "; 215 | } 216 | 217 | write!(f, "{}", grid) 218 | } 219 | } 220 | 221 | #[cfg(test)] 222 | mod tests { 223 | use super::*; 224 | 225 | #[test] 226 | fn should_set_by_index() { 227 | let mut board = Board::new(3, 3); 228 | 229 | board.set_by_index(0, Space::Player(Player::X)).unwrap(); 230 | 231 | assert_eq!( 232 | board.spaces, 233 | [ 234 | Space::Player(Player::X), 235 | Space::Empty, 236 | Space::Empty, 237 | Space::Empty, 238 | Space::Empty, 239 | Space::Empty, 240 | Space::Empty, 241 | Space::Empty, 242 | Space::Empty, 243 | ] 244 | ) 245 | } 246 | 247 | #[test] 248 | fn should_set_by_row_col() { 249 | let mut board = Board::new(3, 3); 250 | 251 | board.set(0, 0, Space::Player(Player::X)).unwrap(); 252 | board.set(1, 1, Space::Player(Player::X)).unwrap(); 253 | board.set(2, 2, Space::Player(Player::X)).unwrap(); 254 | 255 | assert_eq!( 256 | board.spaces, 257 | [ 258 | Space::Player(Player::X), 259 | Space::Empty, 260 | Space::Empty, 261 | Space::Empty, 262 | Space::Player(Player::X), 263 | Space::Empty, 264 | Space::Empty, 265 | Space::Empty, 266 | Space::Player(Player::X), 267 | ] 268 | ) 269 | } 270 | 271 | #[test] 272 | fn should_track_past_moves() { 273 | let mut board = Board::new(3, 3); 274 | 275 | board.set(0, 0, Space::Player(Player::X)).unwrap(); 276 | board.set(1, 1, Space::Player(Player::O)).unwrap(); 277 | board.set(2, 2, Space::Player(Player::X)).unwrap(); 278 | 279 | assert_eq!( 280 | board.moves, 281 | [ 282 | Move { 283 | index: 0, 284 | key: 0 as u32, 285 | space: Space::Player(Player::X) 286 | }, 287 | Move { 288 | index: 4, 289 | key: 2 as u32, 290 | space: Space::Player(Player::O) 291 | }, 292 | Move { 293 | index: 8, 294 | key: 6563 as u32, 295 | space: Space::Player(Player::X) 296 | } 297 | ] 298 | ) 299 | } 300 | 301 | #[test] 302 | fn should_check_if_moves_available_1() { 303 | let board = Board::new(3, 3); 304 | 305 | assert_eq!(board.moves_available(), true); 306 | 307 | let mut board = Board::new(3, 3); 308 | 309 | for space in 0..9 { 310 | board.set_by_index(space, Space::Player(Player::X)).unwrap(); 311 | } 312 | 313 | assert_eq!(board.moves_available(), false); 314 | } 315 | 316 | #[test] 317 | fn should_check_if_moves_available_2() { 318 | let mut board = Board::new(3, 3); 319 | 320 | board.set_by_index(0, Space::Player(Player::X)).unwrap(); 321 | board.set_by_index(1, Space::Player(Player::O)).unwrap(); 322 | 323 | assert_eq!(board.moves_available(), true); 324 | } 325 | 326 | #[test] 327 | fn should_determine_row_winner_1() { 328 | let mut board = Board::new(3, 3); 329 | let row = 0; 330 | 331 | board.set(row, 0, Space::Player(Player::X)).unwrap(); 332 | board.set(row, 1, Space::Player(Player::X)).unwrap(); 333 | board.set(row, 2, Space::Player(Player::X)).unwrap(); 334 | assert_eq!( 335 | board.determine_winner(), 336 | Some(GameResult::Winner(Player::X)) 337 | ); 338 | } 339 | 340 | #[test] 341 | fn should_determine_row_winner_2() { 342 | let mut board = Board::new(3, 3); 343 | let row = 1; 344 | 345 | board.set(row, 0, Space::Player(Player::X)).unwrap(); 346 | board.set(row, 1, Space::Player(Player::X)).unwrap(); 347 | board.set(row, 2, Space::Player(Player::X)).unwrap(); 348 | 349 | assert_eq!( 350 | board.determine_winner(), 351 | Some(GameResult::Winner(Player::X)) 352 | ); 353 | } 354 | 355 | #[test] 356 | fn should_determine_row_winner_3() { 357 | let mut board = Board::new(3, 3); 358 | let row = 2; 359 | 360 | board.set(row, 0, Space::Player(Player::X)).unwrap(); 361 | board.set(row, 1, Space::Player(Player::X)).unwrap(); 362 | board.set(row, 2, Space::Player(Player::X)).unwrap(); 363 | 364 | assert_eq!( 365 | board.determine_winner(), 366 | Some(GameResult::Winner(Player::X)) 367 | ); 368 | } 369 | 370 | #[test] 371 | fn should_determine_col_winner_1() { 372 | let mut board = Board::new(3, 3); 373 | let col = 0; 374 | 375 | board.set(0, col, Space::Player(Player::O)).unwrap(); 376 | board.set(1, col, Space::Player(Player::O)).unwrap(); 377 | board.set(2, col, Space::Player(Player::O)).unwrap(); 378 | 379 | assert_eq!( 380 | board.determine_winner(), 381 | Some(GameResult::Winner(Player::O)) 382 | ); 383 | } 384 | 385 | #[test] 386 | fn should_determine_col_winner_2() { 387 | let mut board = Board::new(3, 3); 388 | let col = 1; 389 | 390 | board.set(0, col, Space::Player(Player::O)).unwrap(); 391 | board.set(1, col, Space::Player(Player::O)).unwrap(); 392 | board.set(2, col, Space::Player(Player::O)).unwrap(); 393 | 394 | assert_eq!( 395 | board.determine_winner(), 396 | Some(GameResult::Winner(Player::O)) 397 | ); 398 | } 399 | 400 | #[test] 401 | fn should_determine_col_winner_3() { 402 | let mut board = Board::new(3, 3); 403 | let col = 2; 404 | 405 | board.set(0, col, Space::Player(Player::O)).unwrap(); 406 | board.set(1, col, Space::Player(Player::O)).unwrap(); 407 | board.set(2, col, Space::Player(Player::O)).unwrap(); 408 | 409 | assert_eq!( 410 | board.determine_winner(), 411 | Some(GameResult::Winner(Player::O)) 412 | ); 413 | } 414 | 415 | #[test] 416 | fn should_determine_diag_winner_1() { 417 | let mut board = Board::new(3, 3); 418 | 419 | board.set(0, 0, Space::Player(Player::X)).unwrap(); 420 | board.set(1, 1, Space::Player(Player::X)).unwrap(); 421 | board.set(2, 2, Space::Player(Player::X)).unwrap(); 422 | 423 | assert_eq!( 424 | board.determine_winner(), 425 | Some(GameResult::Winner(Player::X)) 426 | ); 427 | } 428 | 429 | #[test] 430 | fn should_determine_diag_winner_2() { 431 | let mut board = Board::new(3, 3); 432 | 433 | board.set(0, 2, Space::Player(Player::O)).unwrap(); 434 | board.set(1, 1, Space::Player(Player::O)).unwrap(); 435 | board.set(2, 0, Space::Player(Player::O)).unwrap(); 436 | 437 | assert_eq!( 438 | board.determine_winner(), 439 | Some(GameResult::Winner(Player::O)) 440 | ); 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /src/bot.rs: -------------------------------------------------------------------------------- 1 | use rand::Rng; 2 | use std::collections::HashMap; 3 | 4 | use crate::board::{Board, GameResult, Player, Space}; 5 | 6 | pub type BotMemory = HashMap>; 7 | 8 | pub struct BotConfig { 9 | pub player: Player, 10 | pub winning_move_boost: Option, 11 | pub win_boost: Option, 12 | pub loose_boost: Option, 13 | pub tie_boost: Option, 14 | } 15 | 16 | pub struct Bot { 17 | pub memory: BotMemory, 18 | pub player: Player, 19 | pub winning_move_boost: i32, 20 | pub win_boost: i32, 21 | pub loose_boost: i32, 22 | pub tie_boost: i32, 23 | } 24 | 25 | impl Bot { 26 | pub fn new(config: BotConfig) -> Self { 27 | Self { 28 | memory: HashMap::new(), 29 | player: config.player, 30 | winning_move_boost: config.winning_move_boost.unwrap_or(1000), 31 | win_boost: config.win_boost.unwrap_or(3), 32 | loose_boost: config.loose_boost.unwrap_or(-1), 33 | tie_boost: config.tie_boost.unwrap_or(0), 34 | } 35 | } 36 | 37 | pub fn load_brain(&mut self, encode_brain: Vec) { 38 | self.memory = bincode::deserialize(&encode_brain).unwrap(); 39 | } 40 | 41 | pub fn export_brain(&self) -> Vec { 42 | bincode::serialize(&self.memory).unwrap() 43 | } 44 | 45 | pub fn determine_move(&mut self, board: &Board) -> Option { 46 | let memory = self 47 | .memory 48 | .entry(board.key_as_u32()) 49 | .or_insert(Bot::get_default_moves(&board)); 50 | 51 | let total = memory.iter().fold(0, |a, b| a + b); 52 | 53 | let mut random = if total > 1 { 54 | rand::thread_rng().gen_range(0..=total) 55 | } else { 56 | 1 57 | }; 58 | 59 | for (index, current) in memory.iter().enumerate() { 60 | let val = *current; 61 | 62 | if val > 0 && random <= val { 63 | return Some(index); 64 | } 65 | 66 | // account for negative numbers in arrays 67 | random = if val >= 0 { random - val } else { random + val } 68 | } 69 | 70 | None 71 | } 72 | 73 | pub fn learn(&mut self, board: &Board, game_result: GameResult) { 74 | let max_moves = board.moves.len(); 75 | 76 | let did_win = match game_result { 77 | GameResult::Winner(res) => self.player == res, 78 | GameResult::Tie => false, 79 | }; 80 | 81 | for (i, m) in board.moves.iter().enumerate() { 82 | if m.space == Space::Player(self.player) { 83 | let game_state_entry = self.memory.entry(m.key).or_insert(vec![]); 84 | 85 | // this should be safe. If we panic here something went wrong as the bot was deciding moves 86 | game_state_entry[m.index] = if did_win { 87 | // give boosts for winning 88 | if i == max_moves - 1 { 89 | // If winning move give larger boost 90 | game_state_entry[m.index] + self.winning_move_boost 91 | } else { 92 | // all other moves get standard boost 93 | game_state_entry[m.index] + self.win_boost 94 | } 95 | } else if game_result == GameResult::Tie { 96 | // add different boost if the game is a tie 97 | game_state_entry[m.index] + self.tie_boost 98 | } else if i == max_moves - 2 { 99 | // bot lost! if the last move made lead to loss 0 it out 100 | 0 101 | } else { 102 | // standard move during a loss get loose boost applied 103 | game_state_entry[m.index] + self.loose_boost 104 | }; 105 | 106 | // if all values are 0 remove it and let the bot start over 107 | // This keeps the bot for "dying" 108 | if game_state_entry.iter().all(|&val| val <= 0) { 109 | self.memory.remove(&m.key); 110 | } 111 | } 112 | } 113 | } 114 | 115 | pub fn get_default_moves(board: &Board) -> Vec { 116 | let mut spaces: Vec = board.spaces.clone().iter().map(|_| 0).collect(); 117 | 118 | for available_space in board.get_available_spaces() { 119 | spaces[available_space] = 10; 120 | } 121 | 122 | spaces 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod board; 2 | mod bot; 3 | mod train; 4 | 5 | use wasm_bindgen::prelude::*; 6 | 7 | use crate::board::{Board, GameResult, Player, Space}; 8 | use crate::bot::{Bot, BotConfig}; 9 | 10 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 11 | // allocator. 12 | #[cfg(feature = "wee_alloc")] 13 | #[global_allocator] 14 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 15 | 16 | #[wasm_bindgen] 17 | pub struct Game { 18 | board: Board, 19 | player_x: Bot, 20 | player_o: Bot, 21 | } 22 | 23 | #[wasm_bindgen] 24 | impl Game { 25 | #[wasm_bindgen(constructor)] 26 | pub fn new( 27 | winning_move_boost: Option, 28 | win_boost: Option, 29 | loose_boost: Option, 30 | tie_boost: Option, 31 | ) -> Self { 32 | Self { 33 | board: Board::new(3, 3), 34 | player_x: Bot::new(BotConfig { 35 | player: Player::X, 36 | winning_move_boost: winning_move_boost, 37 | win_boost: win_boost, 38 | loose_boost: loose_boost, 39 | tie_boost: tie_boost, 40 | }), 41 | player_o: Bot::new(BotConfig { 42 | player: Player::O, 43 | winning_move_boost: winning_move_boost, 44 | win_boost: win_boost, 45 | loose_boost: loose_boost, 46 | tie_boost: tie_boost, 47 | }), 48 | } 49 | } 50 | 51 | pub fn load_x_brain(&mut self, brain: Vec) { 52 | self.player_x.load_brain(brain); 53 | } 54 | 55 | pub fn load_o_brain(&mut self, brain: Vec) { 56 | self.player_o.load_brain(brain); 57 | } 58 | 59 | pub fn export_x_brain(&mut self) -> Vec { 60 | self.player_x.export_brain() 61 | } 62 | 63 | pub fn export_o_brain(&mut self) -> Vec { 64 | self.player_o.export_brain() 65 | } 66 | 67 | pub fn board(&self) -> String { 68 | self.board.key_as_string() 69 | } 70 | 71 | pub fn reset_board(&mut self) { 72 | self.board = Board::new(3, 3); 73 | } 74 | 75 | pub fn make_move_x(&mut self, index: usize) -> Option { 76 | self.board 77 | .set_by_index(index, Space::Player(Player::X)) 78 | .unwrap(); 79 | 80 | if let Some(res) = self.board.determine_winner() { 81 | return Some(res.to_string()); 82 | } 83 | 84 | let bot_move = self.player_o.determine_move(&self.board); 85 | 86 | self.board 87 | .set_by_index(bot_move.unwrap(), Space::Player(Player::O)) 88 | .unwrap(); 89 | 90 | if let Some(res) = self.board.determine_winner() { 91 | return Some(res.to_string()); 92 | } 93 | 94 | None 95 | } 96 | 97 | pub fn make_bot_move_x(&mut self) { 98 | let bot_move = self.player_x.determine_move(&self.board); 99 | 100 | self.board 101 | .set_by_index(bot_move.unwrap(), Space::Player(Player::X)) 102 | .unwrap(); 103 | } 104 | 105 | pub fn make_move_o(&mut self, index: usize) -> Option { 106 | self.board 107 | .set_by_index(index, Space::Player(Player::O)) 108 | .unwrap(); 109 | 110 | if let Some(res) = self.board.determine_winner() { 111 | return Some(res.to_string()); 112 | } 113 | 114 | let bot_move = self.player_x.determine_move(&self.board); 115 | 116 | self.board 117 | .set_by_index(bot_move.unwrap(), Space::Player(Player::X)) 118 | .unwrap(); 119 | 120 | if let Some(res) = self.board.determine_winner() { 121 | return Some(res.to_string()); 122 | } 123 | 124 | None 125 | } 126 | 127 | pub fn train(&mut self, game_count: u32) -> String { 128 | let mut x_win = 0; 129 | let mut o_win = 0; 130 | let mut tie = 0; 131 | 132 | for _ in 1..=game_count { 133 | match train::play(&mut self.board, &mut self.player_x, &mut self.player_o) { 134 | Ok(GameResult::Winner(Player::X)) => x_win += 1, 135 | Ok(GameResult::Winner(Player::O)) => o_win += 1, 136 | Ok(GameResult::Tie) => tie += 1, 137 | Err(player) => { 138 | println!( 139 | "{:?} was unable to find an available move on board:\n {}", 140 | player, self.board 141 | ); 142 | } 143 | } 144 | 145 | self.reset_board(); 146 | } 147 | 148 | "X: ".to_string() 149 | + &x_win.to_string() 150 | + "\nO: " 151 | + &o_win.to_string() 152 | + "\nTIE: " 153 | + &tie.to_string() 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod board; 2 | mod bot; 3 | mod train; 4 | 5 | use clap::Parser; 6 | use std::fs::{read, write}; 7 | use std::io; 8 | 9 | use crate::board::{Board, GameResult, Player, Space}; 10 | use crate::bot::{Bot, BotConfig}; 11 | 12 | #[derive(Parser, Debug)] 13 | #[clap(version)] 14 | struct Args { 15 | #[clap(short, long, default_value = "train")] 16 | mode: String, 17 | #[clap(short, long, default_value = "1000000")] 18 | game_count: u32, 19 | #[clap(short, long, default_value = "3")] 20 | rows: usize, 21 | #[clap(short, long, default_value = "3")] 22 | cols: usize, 23 | } 24 | 25 | fn main() -> Result<(), ()> { 26 | let args = Args::parse(); 27 | 28 | let mut player_x = Bot::new(BotConfig { 29 | player: Player::X, 30 | winning_move_boost: None, 31 | win_boost: None, 32 | loose_boost: None, 33 | tie_boost: None, 34 | }); 35 | 36 | let mut player_o = Bot::new(BotConfig { 37 | player: Player::O, 38 | winning_move_boost: None, 39 | win_boost: None, 40 | loose_boost: None, 41 | tie_boost: None, 42 | }); 43 | 44 | if let Ok(bin) = read(format!("www/bot_{}x{}_x_brain.bin", args.rows, args.cols)) { 45 | player_x.load_brain(bin); 46 | } 47 | 48 | if let Ok(bin) = read(format!("www/bot_{}x{}_o_brain.bin", args.rows, args.cols)) { 49 | player_o.load_brain(bin); 50 | } 51 | 52 | if args.mode == "train" { 53 | train(&mut player_x, &mut player_o, &args) 54 | } else if args.mode == "play_as_x" { 55 | play(Player::X, &mut player_o, &args) 56 | } else if args.mode == "play_as_o" { 57 | play(Player::O, &mut player_x, &args) 58 | } else { 59 | Err(()) 60 | } 61 | } 62 | 63 | fn train(player_x: &mut Bot, player_o: &mut Bot, args: &Args) -> Result<(), ()> { 64 | let mut board = Board::new(args.rows, args.cols); 65 | 66 | let mut x_win = 0; 67 | let mut o_win = 0; 68 | let mut tie = 0; 69 | 70 | for count in 1..=args.game_count { 71 | let game_res = train::play(&mut board, player_x, player_o); 72 | 73 | match game_res { 74 | Ok(GameResult::Winner(Player::X)) => x_win += 1, 75 | Ok(GameResult::Winner(Player::O)) => o_win += 1, 76 | Ok(GameResult::Tie) => tie += 1, 77 | Err(player) => println!( 78 | "{:?} was unable to find an available move on board:\n {}", 79 | player, board 80 | ), 81 | } 82 | 83 | if count % 10_000 == 0 { 84 | println!("{}", board); 85 | println!("============"); 86 | println!("Result: {:?}", game_res); 87 | println!("X: {}", x_win); 88 | println!("O: {}", o_win); 89 | println!("TIE: {}", tie); 90 | } 91 | 92 | board = Board::new(args.rows, args.cols); 93 | } 94 | 95 | write( 96 | format!("www/bot_{}x{}_x_brain.bin", args.rows, args.cols), 97 | player_x.export_brain(), 98 | ) 99 | .expect("Could not save X brain"); 100 | 101 | write( 102 | format!("www/bot_{}x{}_o_brain.bin", args.rows, args.cols), 103 | player_o.export_brain(), 104 | ) 105 | .expect("Could not save O brain"); 106 | 107 | Ok(()) 108 | } 109 | 110 | fn play(player: Player, bot: &mut Bot, args: &Args) -> Result<(), ()> { 111 | let mut board = Board::new(args.rows, args.cols); 112 | let mut current_player: Player = Player::X; 113 | 114 | println!("{}", board); 115 | 116 | loop { 117 | if current_player == player { 118 | let mut complete = false; 119 | 120 | while !complete { 121 | let input = parse_user_input(); 122 | 123 | if board.set(input[0], input[1], Space::Player(player)).is_ok() { 124 | println!("{}", board); 125 | 126 | complete = true; 127 | } else { 128 | println!("Move is invalid, Try again"); 129 | } 130 | } 131 | } else { 132 | let m = bot.determine_move(&board).unwrap(); 133 | 134 | // This should be safe since the bot should not be able to make an invalid move 135 | board.set_by_index(m, Space::Player(bot.player)).unwrap(); 136 | 137 | println!("{}", board); 138 | } 139 | 140 | if let Some(res) = board.determine_winner() { 141 | println!("Game Over! Result: {}", res); 142 | 143 | return Ok(()); 144 | } 145 | 146 | // Toggle current player 147 | current_player = if current_player == Player::X { 148 | Player::O 149 | } else { 150 | Player::X 151 | }; 152 | } 153 | } 154 | 155 | fn parse_user_input() -> Vec { 156 | let mut buf = String::new(); 157 | 158 | io::stdin() 159 | .read_line(&mut buf) 160 | .expect("could not read from stdin"); 161 | 162 | buf.trim_matches('\n') 163 | .split(&['-', '.', ':', ',', '/', ' '][..]) 164 | .map(|i| i.parse::().unwrap()) 165 | .collect::>() 166 | } 167 | -------------------------------------------------------------------------------- /src/train.rs: -------------------------------------------------------------------------------- 1 | use crate::board::{Board, GameResult, Player, Space}; 2 | use crate::bot::Bot; 3 | 4 | pub fn play( 5 | board: &mut Board, 6 | player_x: &mut Bot, 7 | player_o: &mut Bot, 8 | ) -> Result { 9 | let mut current_player: Player = Player::X; 10 | 11 | loop { 12 | if current_player == Player::X { 13 | if let Some(current_move) = player_x.determine_move(board) { 14 | board 15 | .set_by_index(current_move, Space::Player(current_player)) 16 | .unwrap(); 17 | 18 | current_player = Player::O; 19 | } else { 20 | return Err(Player::X); 21 | } 22 | } else { 23 | if let Some(current_move) = player_o.determine_move(board) { 24 | board 25 | .set_by_index(current_move, Space::Player(current_player)) 26 | .unwrap(); 27 | 28 | current_player = Player::X; 29 | } else { 30 | return Err(Player::O); 31 | } 32 | } 33 | 34 | if let Some(res) = board.determine_winner() { 35 | player_x.learn(&board, res); 36 | player_o.learn(&board, res); 37 | 38 | return Ok(res); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "allowJs": true /* Allow javascript files to be compiled. */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "target" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | "noUnusedLocals": true /* Report errors on unused locals. */, 39 | "noUnusedParameters": true /* Report errors on unused parameters. */, 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true /* Skip type checking of declaration files. */, 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | }, 71 | "include": ["www"], 72 | "exclude": ["node_modules"] 73 | } 74 | -------------------------------------------------------------------------------- /www/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-jugglers/brain-games-rust/bdb79963c2e2c1fe349e21bc2cbae8ec84e550c9/www/favicon.ico -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Wasm Games 7 | 8 | 9 | 10 | 82 | 83 |
84 |

Can you beat me?

85 | 86 |
87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 |
95 | 96 |
97 | 98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
114 | 115 | 120 | source code 121 | 122 |
123 | 124 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /www/src/actions.ts: -------------------------------------------------------------------------------- 1 | export enum Action { 2 | Train = 'TRAIN', 3 | GetBoard = 'GET_BOARD', 4 | PlayX = 'PLAY_X', 5 | PlayBotX = 'PLAY_BOT_X', 6 | PlayO = 'PLAY_O', 7 | ResetBoard = 'RESET_BOARD', 8 | } 9 | 10 | export enum ActionComplete { 11 | Train = 'TRAIN_COMPLETE', 12 | GetBoard = 'GET_BOARD_COMPLETE', 13 | PlayX = 'PLAY_X_COMPLETE', 14 | PlayBotX = 'PLAY_BOT_X_COMPLETE', 15 | PlayO = 'PLAY_O_COMPLETE', 16 | ResetBoard = 'RESET_BOARD_COMPLETE', 17 | } 18 | -------------------------------------------------------------------------------- /www/src/board.ts: -------------------------------------------------------------------------------- 1 | import { attr, observable, observe, OnPropertyChanged } from '@joist/observable'; 2 | import { styled, css } from '@joist/styled'; 3 | import { queryAll } from '@joist/query'; 4 | 5 | export class BoardChangeEvent extends Event { 6 | constructor(public index: number) { 7 | super('boardchange'); 8 | } 9 | } 10 | 11 | @observable 12 | @styled 13 | export class BoardElement extends HTMLElement implements OnPropertyChanged { 14 | static styles = [ 15 | css` 16 | :host { 17 | padding: 1rem 0; 18 | min-height: 300px; 19 | width: 100%; 20 | display: flex; 21 | flex-wrap: wrap; 22 | } 23 | 24 | * { 25 | box-sizing: border-box; 26 | } 27 | 28 | button { 29 | background: none; 30 | border: none; 31 | flex: 0 0 33.333333%; 32 | height: calc(300px / 3); 33 | font-size: 1.5rem; 34 | cursor: pointer; 35 | margin: 0; 36 | } 37 | 38 | button:disabled { 39 | background: none; 40 | } 41 | 42 | button.X { 43 | color: #140078; 44 | } 45 | 46 | button.O { 47 | color: #9c27b0; 48 | } 49 | 50 | button:nth-child(2n) { 51 | border-left: solid 1px gray; 52 | border-right: solid 1px gray; 53 | } 54 | 55 | button:nth-child(5), 56 | button:nth-child(6), 57 | button:nth-child(7) { 58 | border-top: solid 1px gray; 59 | border-bottom: solid 1px gray; 60 | } 61 | `, 62 | ]; 63 | 64 | @observe 65 | @attr({ 66 | read: (val) => val.split(''), 67 | write: (val) => val.join(''), 68 | }) 69 | board_state = ['-', '-', '-', '-', '-', '-', '-', '-', '-']; 70 | 71 | @observe @attr disabled = false; 72 | 73 | @queryAll('button') 74 | spaces!: NodeListOf; 75 | 76 | constructor() { 77 | super(); 78 | 79 | const shadow = this.attachShadow({ mode: 'open' }); 80 | 81 | this.board_state.forEach((space, i) => { 82 | if (i > 0 && i % 3 === 0) { 83 | shadow.appendChild(document.createElement('br')); 84 | } 85 | 86 | const btn = document.createElement('button'); 87 | btn.id = i.toString(); 88 | btn.className = space; 89 | btn.disabled = space !== '-' || this.disabled; 90 | btn.innerHTML = space !== '-' ? space : ''; 91 | btn.onclick = () => { 92 | this.dispatchEvent(new BoardChangeEvent(i)); 93 | }; 94 | 95 | shadow.appendChild(btn); 96 | }); 97 | } 98 | 99 | onPropertyChanged(): void { 100 | this.spaces.forEach((btn, i) => { 101 | const space = this.board_state[i]; 102 | 103 | btn.className = space; 104 | btn.disabled = space !== '-' || this.disabled; 105 | btn.innerHTML = space !== '-' ? space : ''; 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /www/src/game.ts: -------------------------------------------------------------------------------- 1 | import { Action } from './actions'; 2 | 3 | export interface TrainConfig { 4 | game_count?: number; 5 | winning_move_boost?: number; 6 | win_boost?: number; 7 | loose_boost?: number; 8 | tie_boost?: number; 9 | } 10 | 11 | const initializeToken = Symbol(); 12 | 13 | export class Game { 14 | worker = new Worker(new URL('./game.worker.ts', import.meta.url), { type: 'module' }); 15 | 16 | static create() { 17 | return new Promise((resolve) => { 18 | const game = new Game(initializeToken); 19 | 20 | 21 | game.worker.addEventListener('message', (msg) => { 22 | if (msg.data.status === 'READY') { 23 | resolve(game); 24 | } 25 | }); 26 | }); 27 | } 28 | 29 | constructor(token: Symbol) { 30 | if(token !== initializeToken) { 31 | throw new Error('Game needs to be initialized async. Use Game.create instead'); 32 | } 33 | } 34 | 35 | train(train_config: TrainConfig) { 36 | return this.run_command(Action.Train, train_config); 37 | } 38 | 39 | get_board(): Promise { 40 | return this.run_command(Action.GetBoard); 41 | } 42 | 43 | play_x(index: number) { 44 | return this.run_command(Action.PlayX, index); 45 | } 46 | 47 | play_bot_x() { 48 | return this.run_command(Action.PlayBotX); 49 | } 50 | 51 | play_o(index: number) { 52 | return this.run_command(Action.PlayO, index); 53 | } 54 | 55 | reset_board() { 56 | return this.run_command(Action.ResetBoard); 57 | } 58 | 59 | private run_command(action: string, payload?: any) { 60 | return new Promise((resolve) => { 61 | const listen = (msg: MessageEvent) => { 62 | if (msg.data.status === `${action}_COMPLETE`) { 63 | this.worker.removeEventListener('message', listen); 64 | 65 | resolve(msg.data.payload); 66 | } 67 | }; 68 | 69 | this.worker.addEventListener('message', listen); 70 | this.worker.postMessage({ action, payload }); 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /www/src/game.worker.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import init, { Game } from '../pkg/brain_games'; 4 | 5 | import { Action, ActionComplete } from './actions'; 6 | 7 | console.log('TEST') 8 | 9 | main().then(() => { 10 | console.log('GAME INITIALIZED!'); 11 | }); 12 | 13 | async function main() { 14 | await init( 15 | new URL('../pkg/brain_games_bg.wasm', import.meta.url) 16 | ); 17 | 18 | // const [bot_x_brain, bot_o_brain] = await Promise.all([ 19 | // fetch('../bot_x_brain.bin').then((res) => res.arrayBuffer()), 20 | // fetch('../bot_o_brain.bin').then((res) => res.arrayBuffer()), 21 | // ]).then((res) => res.map((buffer) => new Uint8Array(buffer))); 22 | 23 | let game = new Game(); // initialize game 24 | 25 | // game.load_x_brain(bot_x_brain); 26 | // game.load_o_brain(bot_o_brain); 27 | 28 | self.postMessage({ status: 'READY' }); // signal that game is ready 29 | 30 | // Listen for actions 31 | self.onmessage = (msg: MessageEvent) => { 32 | switch (msg.data.action) { 33 | case Action.Train: 34 | const { 35 | game_count, 36 | winning_move_boost, 37 | win_boost, 38 | loose_boost, 39 | tie_boost, 40 | } = msg.data.payload; 41 | 42 | game = new Game(winning_move_boost, win_boost, loose_boost, tie_boost); 43 | 44 | self.postMessage({ 45 | status: ActionComplete.Train, 46 | payload: game.train(game_count), 47 | }); 48 | 49 | break; 50 | 51 | case Action.GetBoard: 52 | self.postMessage({ 53 | status: ActionComplete.GetBoard, 54 | payload: game.board(), 55 | }); 56 | 57 | break; 58 | 59 | case Action.PlayX: 60 | self.postMessage({ 61 | status: ActionComplete.PlayX, 62 | payload: game.make_move_x(msg.data.payload), 63 | }); 64 | 65 | break; 66 | 67 | case Action.PlayBotX: 68 | self.postMessage({ 69 | status: ActionComplete.PlayBotX, 70 | payload: game.make_bot_move_x(), 71 | }); 72 | 73 | break; 74 | 75 | case Action.PlayO: 76 | self.postMessage({ 77 | status: ActionComplete.PlayO, 78 | payload: game.make_move_o(msg.data.payload), 79 | }); 80 | 81 | break; 82 | 83 | case Action.ResetBoard: 84 | self.postMessage({ 85 | status: ActionComplete.ResetBoard, 86 | payload: game.reset_board(), 87 | }); 88 | 89 | break; 90 | } 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /www/src/main.ts: -------------------------------------------------------------------------------- 1 | import { BoardChangeEvent, BoardElement } from './board'; 2 | import { Game } from './game'; 3 | 4 | const train_btn = document.getElementById('train') as HTMLButtonElement; 5 | const reset_btn = document.getElementById('reset') as HTMLButtonElement; 6 | const play_o_btn = document.getElementById('play_o') as HTMLButtonElement; 7 | const board = document.getElementById('board') as BoardElement; 8 | const title = document.getElementById('title') as HTMLElement; 9 | const config_form = document.getElementById('config_form') as HTMLFormElement; 10 | 11 | let player = 'X'; 12 | let has_trained = false; 13 | 14 | export async function main() { 15 | console.log('APP STARTING'); 16 | 17 | 18 | const worker = await Game.create(); 19 | 20 | 21 | await update(); 22 | 23 | train_btn.addEventListener('click', onTrainClick); 24 | reset_btn.addEventListener('click', onResetClick); 25 | play_o_btn.addEventListener('click', onPlayO); 26 | board.addEventListener('boardchange', onBoardChange); 27 | 28 | async function onTrainClick() { 29 | title.innerHTML = 'Let me practice for a bit.'; 30 | 31 | await worker.reset_board(); 32 | 33 | await update(); 34 | 35 | let timer = 0; 36 | 37 | train_btn.innerHTML = timer.toString(); 38 | 39 | disable(); 40 | 41 | const interval = setInterval(() => { 42 | timer++; 43 | 44 | train_btn.innerHTML = timer.toString(); 45 | }, 1000); 46 | 47 | const data = new FormData(config_form); 48 | 49 | const training_result = await worker.train({ 50 | game_count: Number(data.get('game_count')!), 51 | winning_move_boost: Number(data.get('winning_move_boost')!), 52 | win_boost: Number(data.get('win_boost')!), 53 | loose_boost: Number(data.get('loose_boost')!), 54 | tie_boost: Number(data.get('tie_boost')!), 55 | }); 56 | 57 | console.log(training_result); 58 | 59 | clearInterval(interval); 60 | 61 | train_btn.innerHTML = 'Train Again'; 62 | 63 | enable(); 64 | 65 | title.innerHTML = 'Now I am ready!'; 66 | 67 | has_trained = true; 68 | } 69 | 70 | async function onResetClick() { 71 | await worker.reset_board(); 72 | 73 | await update(); 74 | 75 | enable(); 76 | 77 | player = 'X'; 78 | title.innerHTML = 'Can you beat me?'; 79 | } 80 | 81 | async function onPlayO() { 82 | player = 'O'; 83 | play_o_btn.disabled = true; 84 | 85 | await worker.play_bot_x(); 86 | await update(); 87 | } 88 | 89 | async function onBoardChange(e: Event) { 90 | const evt = e as BoardChangeEvent; 91 | 92 | play_o_btn.disabled = true; 93 | 94 | let winner; 95 | 96 | if (player === 'X') { 97 | winner = await worker.play_x(evt.index); 98 | } else { 99 | winner = await worker.play_o(evt.index); 100 | } 101 | 102 | await update(); 103 | 104 | if (winner) { 105 | board.disabled = true; 106 | 107 | console.log(`Game Result: ${winner}`); 108 | 109 | if (winner === player) { 110 | if (has_trained) { 111 | title.innerHTML = `You win!`; 112 | } else { 113 | title.innerHTML = `Wait! I wasn't ready. \n`; 114 | } 115 | } else if (winner !== 'TIE') { 116 | if (has_trained) { 117 | title.innerHTML = `Gotcha! Well Played`; 118 | } else { 119 | title.innerHTML = `Wow I wasn't even trying!`; 120 | } 121 | } else { 122 | title.innerHTML = `A tie! Well played!`; 123 | } 124 | } 125 | } 126 | 127 | async function update() { 128 | board.setAttribute('board_state', await worker.get_board()) 129 | } 130 | } 131 | 132 | function disable() { 133 | train_btn.disabled = true; 134 | reset_btn.disabled = true; 135 | play_o_btn.disabled = true; 136 | board.disabled = true; 137 | } 138 | 139 | function enable() { 140 | train_btn.disabled = false; 141 | reset_btn.disabled = false; 142 | play_o_btn.disabled = false; 143 | board.disabled = false; 144 | } 145 | --------------------------------------------------------------------------------