Add Rust: minimax, runner, and codelens highlighter
This commit is contained in:
12
build.sh
Normal file
12
build.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
set -e
|
||||||
|
|
||||||
|
cd rust/rhai-codemirror
|
||||||
|
wasm-pack build --target web --out-dir "../../webui/src/wasm/rhai-codemirror";
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
cd rust/runner
|
||||||
|
wasm-pack build --target web --out-dir "../../webui/src/wasm/runner";
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
cd webui
|
||||||
|
bun install
|
||||||
615
rust/Cargo.lock
generated
Normal file
615
rust/Cargo.lock
generated
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"const-random",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memory_units"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimax"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"parking_lot",
|
||||||
|
"rand",
|
||||||
|
"rhai",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhai"
|
||||||
|
version = "1.23.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "527390cc333a8d2cd8237890e15c36518c26f8b54c903d86fc59f42f08d25594"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"bitflags",
|
||||||
|
"getrandom",
|
||||||
|
"instant",
|
||||||
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
|
"rhai_codegen",
|
||||||
|
"smallvec",
|
||||||
|
"smartstring",
|
||||||
|
"thin-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhai-codemirror"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"js-sys",
|
||||||
|
"rhai",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
"wee_alloc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhai_codegen"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "runner"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"getrandom",
|
||||||
|
"itertools",
|
||||||
|
"js-sys",
|
||||||
|
"minimax",
|
||||||
|
"rand",
|
||||||
|
"rhai",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
"wee_alloc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-wasm-bindgen"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.145"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smartstring"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"static_assertions",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thin-vec"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-keccak"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wee_alloc"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 0.1.10",
|
||||||
|
"libc",
|
||||||
|
"memory_units",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
41
rust/Cargo.toml
Normal file
41
rust/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
opt-level = 's'
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["runner", "minimax", "rhai-codemirror"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
minimax = { path = "./minimax" }
|
||||||
|
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
rand = { version = "0.8.5", features = ["alloc", "small_rng"] }
|
||||||
|
anyhow = "1.0.80"
|
||||||
|
itertools = "0.12.1"
|
||||||
|
rhai = { version = "1.23.4", default-features = false, features = [
|
||||||
|
"no_time",
|
||||||
|
"no_module",
|
||||||
|
"no_custom_syntax",
|
||||||
|
"only_i64",
|
||||||
|
"f32_float",
|
||||||
|
] }
|
||||||
|
|
||||||
|
|
||||||
|
# js ffi
|
||||||
|
getrandom = "0.2"
|
||||||
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
|
js-sys = "0.3"
|
||||||
|
web-sys = { version = "0.3", features = [
|
||||||
|
"console",
|
||||||
|
"Document",
|
||||||
|
"Element",
|
||||||
|
"HtmlElement",
|
||||||
|
"Window",
|
||||||
|
"Performance",
|
||||||
|
] }
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
wee_alloc = "0.4"
|
||||||
|
serde-wasm-bindgen = "0.4"
|
||||||
|
parking_lot = "0.12.5"
|
||||||
17
rust/minimax/Cargo.toml
Normal file
17
rust/minimax/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "minimax"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
|
||||||
|
rhai = { workspace = true }
|
||||||
|
parking_lot = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
rhai = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
web-sys = { workspace = true }
|
||||||
|
wasm-bindgen = { workspace = true }
|
||||||
18
rust/minimax/src/agents/mod.rs
Normal file
18
rust/minimax/src/agents/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
mod rhai;
|
||||||
|
|
||||||
|
pub use rhai::RhaiAgent;
|
||||||
|
|
||||||
|
use crate::game::{Board, PlayerAction};
|
||||||
|
|
||||||
|
pub trait Agent {
|
||||||
|
type ErrorType;
|
||||||
|
|
||||||
|
/// This agent's name
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Try to minimize the value of a board.
|
||||||
|
fn step_min(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType>;
|
||||||
|
|
||||||
|
/// Try to maximize the value of a board.
|
||||||
|
fn step_max(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType>;
|
||||||
|
}
|
||||||
308
rust/minimax/src/agents/rhai.rs
Normal file
308
rust/minimax/src/agents/rhai.rs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use rand::{seq::SliceRandom, Rng};
|
||||||
|
use rhai::{
|
||||||
|
packages::{
|
||||||
|
ArithmeticPackage, BasicArrayPackage, BasicFnPackage, BasicIteratorPackage,
|
||||||
|
BasicMathPackage, BasicStringPackage, LanguageCorePackage, LogicPackage, MoreStringPackage,
|
||||||
|
Package,
|
||||||
|
},
|
||||||
|
CallFnOptions, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError, Position, Scope,
|
||||||
|
AST,
|
||||||
|
};
|
||||||
|
use std::{sync::Arc, vec::IntoIter};
|
||||||
|
|
||||||
|
use super::Agent;
|
||||||
|
use crate::game::{Board, PlayerAction, Symb};
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: WasmTimer
|
||||||
|
//
|
||||||
|
|
||||||
|
// Native impl
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
struct WasmTimer {
|
||||||
|
start: std::time::Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
impl WasmTimer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
start: std::time::Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn elapsed_ms(&self) -> u128 {
|
||||||
|
self.start.elapsed().as_millis()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wasm impl
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
struct WasmTimer {
|
||||||
|
start: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl WasmTimer {
|
||||||
|
fn performance() -> web_sys::Performance {
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
let global = web_sys::js_sys::global();
|
||||||
|
let performance = web_sys::js_sys::Reflect::get(&global, &"performance".into())
|
||||||
|
.expect("performance should be available");
|
||||||
|
|
||||||
|
performance
|
||||||
|
.dyn_into::<web_sys::Performance>()
|
||||||
|
.expect("performance should be available")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
start: Self::performance().now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn elapsed_ms(&self) -> u128 {
|
||||||
|
let performance = Self::performance();
|
||||||
|
(performance.now() - self.start).round() as u128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: RhaiAgent
|
||||||
|
//
|
||||||
|
|
||||||
|
pub struct RhaiAgent<R: Rng + 'static> {
|
||||||
|
#[expect(dead_code)]
|
||||||
|
rng: Arc<Mutex<R>>,
|
||||||
|
|
||||||
|
engine: Engine,
|
||||||
|
script: AST,
|
||||||
|
scope: Scope<'static>,
|
||||||
|
print_callback: Arc<dyn Fn(&str) + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Rng + 'static> RhaiAgent<R> {
|
||||||
|
pub fn new(
|
||||||
|
script: &str,
|
||||||
|
rng: R,
|
||||||
|
print_callback: impl Fn(&str) + 'static,
|
||||||
|
debug_callback: impl Fn(&str) + 'static,
|
||||||
|
) -> Result<Self, ParseError> {
|
||||||
|
let rng = Arc::new(Mutex::new(rng));
|
||||||
|
let print_callback = Arc::new(print_callback);
|
||||||
|
|
||||||
|
let engine = {
|
||||||
|
let mut engine = Engine::new_raw();
|
||||||
|
|
||||||
|
let start = WasmTimer::new();
|
||||||
|
|
||||||
|
let max_secs: u64 = 5;
|
||||||
|
engine.on_progress(move |ops| {
|
||||||
|
if ops % 10_000 != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let elapsed_s = start.elapsed_ms() as u64 / 1000;
|
||||||
|
|
||||||
|
if elapsed_s > max_secs {
|
||||||
|
return Some(
|
||||||
|
format!("Turn ran for more than {max_secs} seconds, exiting.").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return None;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not use FULL, rand_* functions are not pure
|
||||||
|
engine.set_optimization_level(OptimizationLevel::Simple);
|
||||||
|
|
||||||
|
engine.disable_symbol("eval");
|
||||||
|
engine.set_max_expr_depths(100, 100);
|
||||||
|
engine.set_max_strings_interned(1024);
|
||||||
|
engine.set_strict_variables(false);
|
||||||
|
engine.on_print({
|
||||||
|
let callback = print_callback.clone();
|
||||||
|
move |s| callback(s)
|
||||||
|
});
|
||||||
|
engine.on_debug(move |text, source, pos| {
|
||||||
|
debug_callback(&match (source, pos) {
|
||||||
|
(Some(source), Position::NONE) => format!("{source} | {text}"),
|
||||||
|
(Some(source), pos) => format!("{source} @ {pos:?} | {text}"),
|
||||||
|
(None, Position::NONE) => format!("{text}"),
|
||||||
|
(None, pos) => format!("{pos:?} | {text}"),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
LanguageCorePackage::new().register_into_engine(&mut engine);
|
||||||
|
ArithmeticPackage::new().register_into_engine(&mut engine);
|
||||||
|
BasicIteratorPackage::new().register_into_engine(&mut engine);
|
||||||
|
LogicPackage::new().register_into_engine(&mut engine);
|
||||||
|
BasicStringPackage::new().register_into_engine(&mut engine);
|
||||||
|
MoreStringPackage::new().register_into_engine(&mut engine);
|
||||||
|
BasicMathPackage::new().register_into_engine(&mut engine);
|
||||||
|
BasicArrayPackage::new().register_into_engine(&mut engine);
|
||||||
|
BasicFnPackage::new().register_into_engine(&mut engine);
|
||||||
|
|
||||||
|
engine
|
||||||
|
.register_fn("rand_int", {
|
||||||
|
let rng = rng.clone();
|
||||||
|
move |from: i64, to: i64| rng.lock().gen_range(from..=to)
|
||||||
|
})
|
||||||
|
.register_fn("rand_bool", {
|
||||||
|
let rng = rng.clone();
|
||||||
|
move |p: f32| rng.lock().gen_bool((p as f64).clamp(0.0, 1.0))
|
||||||
|
})
|
||||||
|
.register_fn("rand_symb", {
|
||||||
|
let rng = rng.clone();
|
||||||
|
move || Symb::new_random(&mut *rng.lock()).to_string()
|
||||||
|
})
|
||||||
|
.register_fn("rand_op", {
|
||||||
|
let rng = rng.clone();
|
||||||
|
move || Symb::new_random_op(&mut *rng.lock()).to_string()
|
||||||
|
})
|
||||||
|
.register_fn("rand_action", {
|
||||||
|
let rng = rng.clone();
|
||||||
|
move |board: Board| PlayerAction::new_random(&mut *rng.lock(), &board)
|
||||||
|
})
|
||||||
|
.register_fn("rand_shuffle", {
|
||||||
|
let rng = rng.clone();
|
||||||
|
move |mut vec: Vec<Dynamic>| {
|
||||||
|
vec.shuffle(&mut *rng.lock());
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.register_fn("is_op", |s: &str| {
|
||||||
|
Symb::from_str(s).map(|x| x.is_op()).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.register_fn(
|
||||||
|
"permutations",
|
||||||
|
|v: Vec<Dynamic>, size: i64| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
|
let size: usize = match size.try_into() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(format!("Invalid permutation size {size}").into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let per = helpers::RhaiPer::new(v.into_iter().permutations(size).into());
|
||||||
|
|
||||||
|
Ok(Dynamic::from(per))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
engine
|
||||||
|
.build_type::<Board>()
|
||||||
|
.build_type::<PlayerAction>()
|
||||||
|
.build_type::<helpers::RhaiPer<Dynamic, IntoIter<Dynamic>>>();
|
||||||
|
engine
|
||||||
|
};
|
||||||
|
|
||||||
|
let script = engine.compile(script)?;
|
||||||
|
let scope = Scope::new(); // Not used
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
rng,
|
||||||
|
engine,
|
||||||
|
script,
|
||||||
|
scope,
|
||||||
|
print_callback,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(&self, text: &str) {
|
||||||
|
(self.print_callback)(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: Rng + 'static> Agent for RhaiAgent<R> {
|
||||||
|
type ErrorType = EvalAltResult;
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Rhai"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_min(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType> {
|
||||||
|
let res = self.engine.call_fn_with_options::<PlayerAction>(
|
||||||
|
CallFnOptions::new().eval_ast(false),
|
||||||
|
&mut self.scope,
|
||||||
|
&self.script,
|
||||||
|
"step_min",
|
||||||
|
(board.clone(),),
|
||||||
|
);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(err) => Err(*err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_max(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType> {
|
||||||
|
let res = self.engine.call_fn_with_options::<PlayerAction>(
|
||||||
|
CallFnOptions::new().eval_ast(false),
|
||||||
|
&mut self.scope,
|
||||||
|
&self.script,
|
||||||
|
"step_max",
|
||||||
|
(board.clone(),),
|
||||||
|
);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(err) => Err(*err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: rhai helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
mod helpers {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use itertools::Permutations;
|
||||||
|
use rhai::{CustomType, TypeBuilder};
|
||||||
|
|
||||||
|
/// A Rhai iterator that produces all permutations of length `n`
|
||||||
|
/// of the elements in an array
|
||||||
|
pub struct RhaiPer<T: Clone, I: Iterator<Item = T>> {
|
||||||
|
inner: Arc<Permutations<I>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone, I: Clone + Iterator<Item = T>> RhaiPer<T, I> {
|
||||||
|
pub fn new(inner: Permutations<I>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone, I: Clone + Iterator<Item = T>> IntoIterator for RhaiPer<T, I> {
|
||||||
|
type Item = Vec<T>;
|
||||||
|
type IntoIter = Permutations<I>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
(*self.inner).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone, I: Iterator<Item = T>> Clone for RhaiPer<T, I> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + 'static, I: Clone + Iterator<Item = T> + 'static> CustomType for RhaiPer<T, I> {
|
||||||
|
fn build(mut builder: TypeBuilder<Self>) {
|
||||||
|
builder
|
||||||
|
.with_name("Permutations")
|
||||||
|
.is_iterable()
|
||||||
|
.with_fn("to_string", |_s: &mut Self| "Permutation {}".to_owned())
|
||||||
|
.with_fn("to_debug", |_s: &mut Self| "Permutation {}".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
rust/minimax/src/game/action.rs
Normal file
70
rust/minimax/src/game/action.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use super::{Board, Symb};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct PlayerAction {
|
||||||
|
pub symb: Symb,
|
||||||
|
pub pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PlayerAction {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{} at {}", self.symb, self.pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerAction {
|
||||||
|
pub fn new_random<R: Rng>(rng: &mut R, board: &Board) -> Self {
|
||||||
|
let n = board.size();
|
||||||
|
let pos = rng.gen_range(0..n);
|
||||||
|
let symb = Symb::new_random(rng);
|
||||||
|
PlayerAction { symb, pos }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomType for PlayerAction {
|
||||||
|
fn build(mut builder: TypeBuilder<Self>) {
|
||||||
|
builder
|
||||||
|
.with_name("Action")
|
||||||
|
.with_fn(
|
||||||
|
"Action",
|
||||||
|
|symb: &str, pos: i64| -> Result<Self, Box<EvalAltResult>> {
|
||||||
|
let symb = match Symb::from_str(symb) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Err(format!("Invalid symbol {symb:?}").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
symb,
|
||||||
|
pos: pos as usize,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_fn(
|
||||||
|
"Action",
|
||||||
|
|symb: i64, pos: i64| -> Result<Self, Box<EvalAltResult>> {
|
||||||
|
let symb = symb.to_string();
|
||||||
|
let symb = match Symb::from_str(&symb) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Err(format!("Invalid symbol {symb:?}").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
symb,
|
||||||
|
pos: pos as usize,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_fn("to_string", |s: &mut Self| -> String {
|
||||||
|
format!("Action {{{} at {}}}", s.symb, s.pos)
|
||||||
|
})
|
||||||
|
.with_fn("to_debug", |s: &mut Self| -> String {
|
||||||
|
format!("Action {{{} at {}}}", s.symb, s.pos)
|
||||||
|
})
|
||||||
|
.with_get("symb", |s: &mut Self| s.symb.to_string())
|
||||||
|
.with_get("pos", |s: &mut Self| s.pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
632
rust/minimax/src/game/board.rs
Normal file
632
rust/minimax/src/game/board.rs
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use rhai::Array;
|
||||||
|
use rhai::CustomType;
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::EvalAltResult;
|
||||||
|
use rhai::Position;
|
||||||
|
use rhai::TypeBuilder;
|
||||||
|
use std::fmt::{Debug, Display, Write};
|
||||||
|
|
||||||
|
use super::{PlayerAction, Symb, TreeElement};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum InterTreeElement {
|
||||||
|
Unprocessed(Token),
|
||||||
|
Processed(TreeElement),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterTreeElement {
|
||||||
|
fn to_value(&self) -> Option<TreeElement> {
|
||||||
|
Some(match self {
|
||||||
|
InterTreeElement::Processed(x) => x.clone(),
|
||||||
|
InterTreeElement::Unprocessed(Token::Value(s)) => {
|
||||||
|
if let Some(s) = s.strip_prefix('-') {
|
||||||
|
TreeElement::Neg {
|
||||||
|
r: {
|
||||||
|
if s.contains('_') {
|
||||||
|
Box::new(TreeElement::Partial(s.to_string()))
|
||||||
|
} else {
|
||||||
|
Box::new(TreeElement::Number(match s.parse() {
|
||||||
|
Ok(x) => x,
|
||||||
|
_ => return None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if s.contains('_') {
|
||||||
|
TreeElement::Partial(s.to_string())
|
||||||
|
} else {
|
||||||
|
TreeElement::Number(match s.parse() {
|
||||||
|
Ok(x) => x,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
enum Token {
|
||||||
|
Value(String),
|
||||||
|
OpAdd,
|
||||||
|
OpSub,
|
||||||
|
OpMult,
|
||||||
|
OpDiv,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Board {
|
||||||
|
board: [Option<Symb>; 11],
|
||||||
|
placed_by: [Option<String>; 11],
|
||||||
|
|
||||||
|
/// Number of Nones in `board`
|
||||||
|
free_spots: usize,
|
||||||
|
|
||||||
|
/// Index of the last board index that was changed
|
||||||
|
last_placed: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Board {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for c in self.board {
|
||||||
|
write!(f, "{}", c.map(|s| s.get_char().unwrap()).unwrap_or('_'))?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Board {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl Board {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
free_spots: 11,
|
||||||
|
board: Default::default(),
|
||||||
|
placed_by: Default::default(),
|
||||||
|
last_placed: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_board(&self) -> &[Option<Symb>; 11] {
|
||||||
|
&self.board
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_board_mut(&mut self) -> &mut [Option<Symb>; 11] {
|
||||||
|
&mut self.board
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the index of the ith empty slot
|
||||||
|
pub fn ith_empty_slot(&self, mut idx: usize) -> Option<usize> {
|
||||||
|
for (i, c) in self.board.iter().enumerate() {
|
||||||
|
if c.is_none() {
|
||||||
|
if idx == 0 {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
Some(self.board.len() - 1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_full(&self) -> bool {
|
||||||
|
self.free_spots == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prettyprint(&self) -> String {
|
||||||
|
const RESET: &str = "\x1b[0m";
|
||||||
|
const MAGENTA: &str = "\x1b[35m";
|
||||||
|
|
||||||
|
let mut s = String::new();
|
||||||
|
// Print board
|
||||||
|
for (i, (symb, _)) in self.board.iter().zip(self.placed_by.iter()).enumerate() {
|
||||||
|
match symb {
|
||||||
|
Some(symb) => write!(
|
||||||
|
s,
|
||||||
|
"{}{}{RESET}",
|
||||||
|
// If index matches last placed, draw symbol in red.
|
||||||
|
// If last_placed is None, this check will always fail
|
||||||
|
// since self.board.len is always greater than i.
|
||||||
|
if self.last_placed.unwrap_or(self.board.len()) == i {
|
||||||
|
MAGENTA
|
||||||
|
} else {
|
||||||
|
RESET
|
||||||
|
},
|
||||||
|
symb,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
|
||||||
|
None => write!(s, "_").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
self.board.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_last_placed(&self) -> Option<usize> {
|
||||||
|
self.last_placed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, s: Symb) -> bool {
|
||||||
|
self.board.iter().contains(&Some(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the given action valid?
|
||||||
|
pub fn can_play(&self, action: &PlayerAction) -> bool {
|
||||||
|
match &self.board[action.pos] {
|
||||||
|
Some(_) => return false,
|
||||||
|
None => {
|
||||||
|
// Check for duplicate symbols
|
||||||
|
if self.contains(action.symb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check syntax
|
||||||
|
match action.symb {
|
||||||
|
Symb::Minus => {
|
||||||
|
if action.pos == self.board.len() - 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = &self.board[action.pos + 1];
|
||||||
|
if r.is_some_and(|s| s.is_op() && !s.is_minus()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Symb::Zero => {
|
||||||
|
if action.pos != 0 {
|
||||||
|
let l = &self.board[action.pos - 1];
|
||||||
|
if l == &Some(Symb::Div) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Symb::Div | Symb::Plus | Symb::Times => {
|
||||||
|
if action.pos == 0 || action.pos == self.board.len() - 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = &self.board[action.pos - 1];
|
||||||
|
let r = &self.board[action.pos + 1];
|
||||||
|
|
||||||
|
if action.symb == Symb::Div && r == &Some(Symb::Zero) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.is_some_and(|s| s.is_op())
|
||||||
|
|| r.is_some_and(|s| s.is_op() && !s.is_minus())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place the marked symbol at the given position.
|
||||||
|
/// Returns true for valid moves and false otherwise.
|
||||||
|
pub fn play(&mut self, action: PlayerAction, player: impl Into<String>) -> bool {
|
||||||
|
if !self.can_play(&action) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.board[action.pos] = Some(action.symb);
|
||||||
|
self.placed_by[action.pos] = Some(player.into());
|
||||||
|
self.free_spots -= 1;
|
||||||
|
self.last_placed = Some(action.pos);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize(&self) -> Vec<Token> {
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut is_neg = true; // if true, - is negative. if false, subtract.
|
||||||
|
let mut current_num = String::new();
|
||||||
|
|
||||||
|
for s in self.board.iter() {
|
||||||
|
match s {
|
||||||
|
Some(Symb::Div) => {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpDiv);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
Some(Symb::Minus) => {
|
||||||
|
if is_neg {
|
||||||
|
current_num = format!("-{}", current_num);
|
||||||
|
} else {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpSub);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Symb::Plus) => {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpAdd);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
Some(Symb::Times) => {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpMult);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
Some(Symb::Zero) => {
|
||||||
|
current_num.push('0');
|
||||||
|
is_neg = false;
|
||||||
|
}
|
||||||
|
Some(Symb::Number(x)) => {
|
||||||
|
current_num.push_str(&x.to_string());
|
||||||
|
is_neg = false;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
current_num.push('_');
|
||||||
|
is_neg = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_tree(&self) -> Option<TreeElement> {
|
||||||
|
let tokens = self.tokenize();
|
||||||
|
|
||||||
|
let mut tree: Vec<_> = tokens
|
||||||
|
.iter()
|
||||||
|
.map(|x| InterTreeElement::Unprocessed(x.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut priority_level = 0;
|
||||||
|
let mut did_something;
|
||||||
|
while tree.len() > 1 {
|
||||||
|
did_something = false;
|
||||||
|
for i in 0..tree.len() {
|
||||||
|
if match priority_level {
|
||||||
|
0 => matches!(
|
||||||
|
tree[i],
|
||||||
|
InterTreeElement::Unprocessed(Token::OpMult)
|
||||||
|
| InterTreeElement::Unprocessed(Token::OpDiv)
|
||||||
|
),
|
||||||
|
1 => matches!(
|
||||||
|
tree[i],
|
||||||
|
InterTreeElement::Unprocessed(Token::OpAdd)
|
||||||
|
| InterTreeElement::Unprocessed(Token::OpSub)
|
||||||
|
),
|
||||||
|
_ => false,
|
||||||
|
} {
|
||||||
|
did_something = true;
|
||||||
|
|
||||||
|
if i == 0 || i + 1 >= tree.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = tree[i - 1].to_value()?;
|
||||||
|
let r = tree[i + 1].to_value()?;
|
||||||
|
|
||||||
|
let v = match tree[i] {
|
||||||
|
InterTreeElement::Unprocessed(Token::OpAdd) => TreeElement::Add {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
InterTreeElement::Unprocessed(Token::OpDiv) => TreeElement::Div {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
InterTreeElement::Unprocessed(Token::OpMult) => TreeElement::Mul {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
InterTreeElement::Unprocessed(Token::OpSub) => TreeElement::Sub {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
tree.remove(i - 1);
|
||||||
|
tree.remove(i - 1);
|
||||||
|
tree[i - 1] = InterTreeElement::Processed(v);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !did_something {
|
||||||
|
priority_level += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(match tree.into_iter().next().unwrap() {
|
||||||
|
InterTreeElement::Processed(x) => x,
|
||||||
|
x => x.to_value()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&self) -> Option<f32> {
|
||||||
|
self.to_tree()?.evaluate()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_board(board: [Option<Symb>; 11]) -> Self {
|
||||||
|
let free_spots = board.iter().filter(|x| x.is_none()).count();
|
||||||
|
Self {
|
||||||
|
board,
|
||||||
|
placed_by: Default::default(),
|
||||||
|
free_spots,
|
||||||
|
last_placed: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a board from a string
|
||||||
|
pub fn from_string(s: &str) -> Option<Self> {
|
||||||
|
if s.len() != 11 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = s
|
||||||
|
.chars()
|
||||||
|
.filter_map(|c| {
|
||||||
|
if c == '_' {
|
||||||
|
Some(None)
|
||||||
|
} else {
|
||||||
|
Symb::from_char(c).map(Some)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if x.len() != 11 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut free_spots = 11;
|
||||||
|
let mut board = [None; 11];
|
||||||
|
for i in 0..x.len() {
|
||||||
|
board[i] = x[i];
|
||||||
|
if x[i].is_some() {
|
||||||
|
free_spots -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
board,
|
||||||
|
placed_by: Default::default(),
|
||||||
|
free_spots,
|
||||||
|
last_placed: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If true, this board is not done and has no valid moves
|
||||||
|
pub fn is_stuck(&self) -> bool {
|
||||||
|
if self.is_full() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can only happen in a few cases,
|
||||||
|
// enumerated below.
|
||||||
|
|
||||||
|
// `9614807523_` (all numbers, one spot left for op)
|
||||||
|
// Note that this is _not_ a problem if the left spot is empty.
|
||||||
|
if self.free_spots == 1
|
||||||
|
&& self.board[10].is_none()
|
||||||
|
&& self
|
||||||
|
.board
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_ref())
|
||||||
|
.all(|x| !x.is_op())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `961487523/_` (forced division by zero)
|
||||||
|
if self.free_spots == 1
|
||||||
|
&& self.board[10].is_none()
|
||||||
|
&& self.board[9] == Some(Symb::Div)
|
||||||
|
&& self.board[0..8]
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_ref())
|
||||||
|
.all(|x| !x.is_op() && *x != Symb::Zero)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Board {
|
||||||
|
type Item = String;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.board
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.map(|x| x.to_string()).unwrap_or_default())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomType for Board {
|
||||||
|
fn build(mut builder: TypeBuilder<Self>) {
|
||||||
|
builder
|
||||||
|
.with_name("Board")
|
||||||
|
.is_iterable()
|
||||||
|
.with_fn("to_string", |s: &mut Self| format!("{}", s))
|
||||||
|
.with_fn("to_debug", |s: &mut Self| format!("{:?}", s))
|
||||||
|
.with_fn("size", |s: &mut Self| s.board.len() as i64)
|
||||||
|
.with_fn("len", |s: &mut Self| s.board.len() as i64)
|
||||||
|
.with_fn("is_full", |s: &mut Self| s.is_full())
|
||||||
|
.with_fn("free_spots", |s: &mut Self| s.free_spots)
|
||||||
|
.with_fn("play", |s: &mut Self, act: PlayerAction| {
|
||||||
|
s.play(act, "NONE".to_owned()) // Player doesn't matter
|
||||||
|
})
|
||||||
|
.with_fn("ith_free_slot", |s: &mut Self, idx: usize| {
|
||||||
|
s.ith_empty_slot(idx).map(|x| x as i64).unwrap_or(-1)
|
||||||
|
})
|
||||||
|
.with_fn("can_play", |s: &mut Self, act: PlayerAction| {
|
||||||
|
s.can_play(&act)
|
||||||
|
})
|
||||||
|
.with_fn("contains", |s: &mut Self, sym: &str| {
|
||||||
|
match Symb::from_str(sym) {
|
||||||
|
None => false,
|
||||||
|
Some(x) => s.contains(x),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_fn("contains", |s: &mut Self, sym: i64| {
|
||||||
|
let sym = sym.to_string();
|
||||||
|
match Symb::from_str(&sym) {
|
||||||
|
None => false,
|
||||||
|
Some(x) => s.contains(x),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_fn("evaluate", |s: &mut Self| -> Dynamic {
|
||||||
|
s.evaluate().map(|x| x.into()).unwrap_or(().into())
|
||||||
|
})
|
||||||
|
.with_fn("free_spots_idx", |s: &mut Self| -> Array {
|
||||||
|
s.board
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, x)| x.is_none())
|
||||||
|
.map(|(i, _)| i as i64)
|
||||||
|
.map(|x| x.into())
|
||||||
|
.collect::<Vec<Dynamic>>()
|
||||||
|
})
|
||||||
|
.with_indexer_get(
|
||||||
|
|s: &mut Self, idx: i64| -> Result<String, Box<EvalAltResult>> {
|
||||||
|
if idx as usize >= s.board.len() {
|
||||||
|
return Err(
|
||||||
|
EvalAltResult::ErrorIndexNotFound(idx.into(), Position::NONE).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = idx as usize;
|
||||||
|
return Ok(s.board[idx].map(|x| x.to_string()).unwrap_or_default());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_indexer_set(
|
||||||
|
|s: &mut Self, idx: i64, val: String| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let idx: usize = match idx.try_into() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
idx.into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if idx >= s.board.len() {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
(idx as i64).into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match Symb::from_str(&val) {
|
||||||
|
None => return Err(format!("Invalid symbol {val}").into()),
|
||||||
|
Some(x) => {
|
||||||
|
s.board[idx] = Some(x);
|
||||||
|
s.placed_by[idx] = Some("NONE".to_owned()); // Arbitrary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_indexer_set(
|
||||||
|
|s: &mut Self, idx: i64, _val: ()| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let idx: usize = match idx.try_into() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
idx.into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if idx >= s.board.len() {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
(idx as i64).into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
s.board[idx] = None;
|
||||||
|
s.placed_by[idx] = None;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_indexer_set(
|
||||||
|
|s: &mut Self, idx: i64, val: i64| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let idx: usize = match idx.try_into() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
idx.into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if idx >= s.board.len() {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
(idx as i64).into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match Symb::from_str(&val.to_string()) {
|
||||||
|
None => return Err(format!("Invalid symbol {val}").into()),
|
||||||
|
Some(x) => {
|
||||||
|
s.board[idx] = Some(x);
|
||||||
|
s.placed_by[idx] = Some("NULL".to_owned()); // Arbitrary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
rust/minimax/src/game/mod.rs
Normal file
9
rust/minimax/src/game/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod action;
|
||||||
|
mod board;
|
||||||
|
mod symb;
|
||||||
|
mod tree;
|
||||||
|
|
||||||
|
pub use action::PlayerAction;
|
||||||
|
pub use board::Board;
|
||||||
|
pub use symb::Symb;
|
||||||
|
pub use tree::TreeElement;
|
||||||
120
rust/minimax/src/game/symb.rs
Normal file
120
rust/minimax/src/game/symb.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
num::NonZeroU8,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy, Hash)]
|
||||||
|
|
||||||
|
pub enum Symb {
|
||||||
|
Number(NonZeroU8),
|
||||||
|
Zero,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Times,
|
||||||
|
Div,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Symb {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Number(x) => write!(f, "{x}")?,
|
||||||
|
Self::Zero => write!(f, "0")?,
|
||||||
|
Self::Plus => write!(f, "+")?,
|
||||||
|
Self::Minus => write!(f, "-")?,
|
||||||
|
Self::Div => write!(f, "÷")?,
|
||||||
|
Self::Times => write!(f, "×")?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Debug for Symb {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Symb {
|
||||||
|
/// Is this symbol a plain binary operator?
|
||||||
|
pub fn is_op(&self) -> bool {
|
||||||
|
matches!(self, Symb::Div | Symb::Plus | Symb::Times | Symb::Minus)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_minus(&self) -> bool {
|
||||||
|
self == &Self::Minus
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_random<R: Rng>(rng: &mut R) -> Self {
|
||||||
|
match rng.gen_range(0..=13) {
|
||||||
|
0 => Symb::Zero,
|
||||||
|
n @ 1..=9 => Symb::Number(NonZeroU8::new(n).unwrap()),
|
||||||
|
10 => Symb::Div,
|
||||||
|
11 => Symb::Minus,
|
||||||
|
12 => Symb::Plus,
|
||||||
|
13 => Symb::Times,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_random_op<R: Rng>(rng: &mut R) -> Self {
|
||||||
|
match rng.gen_range(0..=3) {
|
||||||
|
0 => Symb::Div,
|
||||||
|
1 => Symb::Minus,
|
||||||
|
2 => Symb::Plus,
|
||||||
|
3 => Symb::Times,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn get_char(&self) -> Option<char> {
|
||||||
|
match self {
|
||||||
|
Self::Plus => Some('+'),
|
||||||
|
Self::Minus => Some('-'),
|
||||||
|
Self::Times => Some('×'),
|
||||||
|
Self::Div => Some('÷'),
|
||||||
|
Self::Zero => Some('0'),
|
||||||
|
Self::Number(x) => match x.get() {
|
||||||
|
1 => Some('1'),
|
||||||
|
2 => Some('2'),
|
||||||
|
3 => Some('3'),
|
||||||
|
4 => Some('4'),
|
||||||
|
5 => Some('5'),
|
||||||
|
6 => Some('6'),
|
||||||
|
7 => Some('7'),
|
||||||
|
8 => Some('8'),
|
||||||
|
9 => Some('9'),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
if s.chars().count() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::from_char(s.chars().next()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_char(c: char) -> Option<Self> {
|
||||||
|
match c {
|
||||||
|
'1' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(1) })),
|
||||||
|
'2' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(2) })),
|
||||||
|
'3' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(3) })),
|
||||||
|
'4' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(4) })),
|
||||||
|
'5' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(5) })),
|
||||||
|
'6' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(6) })),
|
||||||
|
'7' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(7) })),
|
||||||
|
'8' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(8) })),
|
||||||
|
'9' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(9) })),
|
||||||
|
'0' => Some(Self::Zero),
|
||||||
|
'+' => Some(Self::Plus),
|
||||||
|
'-' => Some(Self::Minus),
|
||||||
|
'*' => Some(Self::Times),
|
||||||
|
'/' => Some(Self::Div),
|
||||||
|
'×' => Some(Self::Times),
|
||||||
|
'÷' => Some(Self::Div),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
rust/minimax/src/game/tree.rs
Normal file
143
rust/minimax/src/game/tree.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone)]
|
||||||
|
pub enum TreeElement {
|
||||||
|
Partial(String),
|
||||||
|
Number(f32),
|
||||||
|
Add {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Sub {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Mul {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Div {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Neg {
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TreeElement {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Partial(s) => write!(f, "{s}")?,
|
||||||
|
Self::Number(n) => write!(f, "{n}")?,
|
||||||
|
Self::Add { l, r } => write!(f, "({l}+{r})")?,
|
||||||
|
Self::Div { l, r } => write!(f, "({l}÷{r})")?,
|
||||||
|
Self::Mul { l, r } => write!(f, "({l}×{r})")?,
|
||||||
|
Self::Sub { l, r } => write!(f, "({l}-{r})")?,
|
||||||
|
Self::Neg { r } => write!(f, "(-{r})")?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for TreeElement {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl TreeElement {
|
||||||
|
pub fn left(&self) -> Option<&TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { l, .. }
|
||||||
|
| Self::Sub { l, .. }
|
||||||
|
| Self::Mul { l, .. }
|
||||||
|
| Self::Div { l, .. } => Some(&**l),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right(&self) -> Option<&TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { r, .. }
|
||||||
|
| Self::Neg { r, .. }
|
||||||
|
| Self::Sub { r, .. }
|
||||||
|
| Self::Mul { r, .. }
|
||||||
|
| Self::Div { r, .. } => Some(&**r),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left_mut(&mut self) -> Option<&mut TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { l, .. }
|
||||||
|
| Self::Sub { l, .. }
|
||||||
|
| Self::Mul { l, .. }
|
||||||
|
| Self::Div { l, .. } => Some(&mut **l),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right_mut(&mut self) -> Option<&mut TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { r, .. }
|
||||||
|
| Self::Neg { r, .. }
|
||||||
|
| Self::Sub { r, .. }
|
||||||
|
| Self::Mul { r, .. }
|
||||||
|
| Self::Div { r, .. } => Some(&mut **r),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&self) -> Option<f32> {
|
||||||
|
match self {
|
||||||
|
Self::Number(x) => Some(*x),
|
||||||
|
// Try to parse strings of a partial
|
||||||
|
Self::Partial(s) => s.parse().ok(),
|
||||||
|
Self::Add { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l + r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Mul { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l * r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Div { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
|
||||||
|
if r == Some(0.0) {
|
||||||
|
None
|
||||||
|
} else if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l / r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Sub { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l - r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Neg { r } => {
|
||||||
|
let r = r.evaluate();
|
||||||
|
r.map(|r| -r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
rust/minimax/src/lib.rs
Normal file
2
rust/minimax/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod agents;
|
||||||
|
pub mod game;
|
||||||
21
rust/rhai-codemirror/Cargo.toml
Normal file
21
rust/rhai-codemirror/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "rhai-codemirror"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rhai = { workspace = true, features = ["internals"] }
|
||||||
|
|
||||||
|
wasm-bindgen = { workspace = true }
|
||||||
|
js-sys = { workspace = true }
|
||||||
|
web-sys = { workspace = true, features = ["console"] }
|
||||||
|
console_error_panic_hook = { workspace = true }
|
||||||
|
wee_alloc = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde-wasm-bindgen = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
rhai = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
64
rust/rhai-codemirror/src/codemirror.rs
Normal file
64
rust/rhai-codemirror/src/codemirror.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use js_sys::{Array, JsString, RegExp};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen(module = "codemirror")]
|
||||||
|
extern "C" {
|
||||||
|
pub type StringStream;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn eol(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn sol(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn peek(this: &StringStream) -> JsString;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn next(this: &StringStream) -> JsString;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eat)]
|
||||||
|
pub fn eat_regexp(this: &StringStream, m: &RegExp) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eatWhile)]
|
||||||
|
pub fn eat_while_regexp(this: &StringStream, m: &RegExp) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eatSpace)]
|
||||||
|
pub fn eat_space(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = skipToEnd)]
|
||||||
|
pub fn skip_to_end(this: &StringStream);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = skipTo)]
|
||||||
|
pub fn skip_to(this: &StringStream, str: &str) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = match)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_str(this: &StringStream, pattern: &str, consume: bool, case_fold: bool) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = match)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_regexp(this: &StringStream, pattern: &RegExp, consume: bool) -> Array;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = backUp)]
|
||||||
|
pub fn back_up(this: &StringStream, n: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn column(this: &StringStream) -> u32;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn indentation(this: &StringStream) -> u32;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn current(this: &StringStream) -> String;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = lookAhead)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn look_ahead(this: &StringStream, n: u32) -> Option<String>;
|
||||||
|
}
|
||||||
20
rust/rhai-codemirror/src/lib.rs
Normal file
20
rust/rhai-codemirror/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod codemirror;
|
||||||
|
mod rhai_mode;
|
||||||
|
|
||||||
|
pub use rhai_mode::*;
|
||||||
|
|
||||||
|
// Use `wee_alloc` as the global allocator for smaller WASM size.
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub fn main_js() -> Result<(), JsValue> {
|
||||||
|
// This provides better error messages in debug mode.
|
||||||
|
// It's disabled in release mode so it doesn't bloat up the file size.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
353
rust/rhai-codemirror/src/rhai_mode.rs
Normal file
353
rust/rhai-codemirror/src/rhai_mode.rs
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
use crate::codemirror;
|
||||||
|
use js_sys::RegExp;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::console;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct RhaiMode {
|
||||||
|
indent_unit: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct State {
|
||||||
|
token_state: rhai::TokenizeState,
|
||||||
|
unclosed_bracket_count: i32,
|
||||||
|
line_indent: u32,
|
||||||
|
is_defining_identifier: bool,
|
||||||
|
/// Buffered character, if any. (For use by `StreamAdapter`.)
|
||||||
|
buf: Option<char>,
|
||||||
|
/// Interpolated string brace counting stack
|
||||||
|
interpolated_str_brace_stack: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ELECTRIC_INPUT: RegExp = RegExp::new("^\\s*[}\\])]$", "");
|
||||||
|
static LINE_COMMENT: JsValue = JsValue::from_str("//");
|
||||||
|
static CODEMIRROR_PASS: RefCell<JsValue> = RefCell::new(JsValue::null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn init_codemirror_pass(codemirror_pass: JsValue) {
|
||||||
|
CODEMIRROR_PASS.with(|v| v.replace(codemirror_pass));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl RhaiMode {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(indent_unit: u32) -> Self {
|
||||||
|
Self { indent_unit }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = startState)]
|
||||||
|
pub fn start_state(&self) -> State {
|
||||||
|
State {
|
||||||
|
token_state: rhai::TokenizeState {
|
||||||
|
include_comments: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
unclosed_bracket_count: 0,
|
||||||
|
line_indent: 0,
|
||||||
|
is_defining_identifier: false,
|
||||||
|
buf: None,
|
||||||
|
interpolated_str_brace_stack: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = copyState)]
|
||||||
|
pub fn copy_state(&self, state: &State) -> State {
|
||||||
|
state.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(
|
||||||
|
&self,
|
||||||
|
stream: codemirror::StringStream,
|
||||||
|
state: &mut State,
|
||||||
|
) -> Result<Option<String>, JsValue> {
|
||||||
|
token(stream, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indent(&self, state: &mut State, text_after: String) -> JsValue {
|
||||||
|
indent(self, state, text_after)
|
||||||
|
.map(JsValue::from)
|
||||||
|
.unwrap_or_else(|| CODEMIRROR_PASS.with(|v| v.borrow().clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter, js_name = electricInput)]
|
||||||
|
pub fn electric_input(&self) -> RegExp {
|
||||||
|
ELECTRIC_INPUT.with(|v| v.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter, js_name = lineComment)]
|
||||||
|
pub fn line_comment(&self) -> JsValue {
|
||||||
|
LINE_COMMENT.with(|v| v.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StreamAdapter {
|
||||||
|
/// Buffered character, if any.
|
||||||
|
buf: Option<char>,
|
||||||
|
stream: codemirror::StringStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rhai::InputStream for StreamAdapter {
|
||||||
|
fn unget(&mut self, ch: char) {
|
||||||
|
self.buf = Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next(&mut self) -> Option<char> {
|
||||||
|
if let Some(ch) = self.buf.take() {
|
||||||
|
return Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = self.stream.next();
|
||||||
|
if first.is_falsy() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
assert_eq!(first.length(), 1);
|
||||||
|
let first_code_unit = first.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) = std::char::decode_utf16(std::iter::once(first_code_unit)).next() {
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// The first value is likely an unpared surrogate, so we get one
|
||||||
|
// more UTF-16 unit to attempt to make a proper Unicode scalar.
|
||||||
|
let second = self.stream.next();
|
||||||
|
if second.is_falsy() {
|
||||||
|
return Some(std::char::REPLACEMENT_CHARACTER);
|
||||||
|
}
|
||||||
|
assert_eq!(second.length(), 1);
|
||||||
|
let second_code_unit = second.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) =
|
||||||
|
std::char::decode_utf16([first_code_unit, second_code_unit].iter().copied()).next()
|
||||||
|
{
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// Turns out to not be a proper surrogate pair, so back up one
|
||||||
|
// unit for it to be decoded separately.
|
||||||
|
self.stream.back_up(1);
|
||||||
|
Some(std::char::REPLACEMENT_CHARACTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_next(&mut self) -> Option<char> {
|
||||||
|
if let Some(ch) = self.buf {
|
||||||
|
return Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = self.stream.peek();
|
||||||
|
if first.is_falsy() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
assert_eq!(first.length(), 1);
|
||||||
|
let first_code_unit = first.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) = std::char::decode_utf16(std::iter::once(first_code_unit)).next() {
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// The first value is likely an unpared surrogate, so we get one more
|
||||||
|
// value to attempt to make a proper Unicode scalar value.
|
||||||
|
self.stream.next();
|
||||||
|
let second = self.stream.peek();
|
||||||
|
if second.is_falsy() {
|
||||||
|
return Some(std::char::REPLACEMENT_CHARACTER);
|
||||||
|
}
|
||||||
|
self.stream.back_up(1);
|
||||||
|
assert_eq!(second.length(), 1);
|
||||||
|
let second_code_unit = second.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) =
|
||||||
|
std::char::decode_utf16([first_code_unit, second_code_unit].iter().copied()).next()
|
||||||
|
{
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
Some(std::char::REPLACEMENT_CHARACTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token(stream: codemirror::StringStream, state: &mut State) -> Result<Option<String>, JsValue> {
|
||||||
|
if stream.sol() {
|
||||||
|
state.line_indent = stream.indentation();
|
||||||
|
state.unclosed_bracket_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stream_adapter = StreamAdapter {
|
||||||
|
stream,
|
||||||
|
buf: state.buf,
|
||||||
|
};
|
||||||
|
let (next_token, _) = rhai::get_next_token(
|
||||||
|
&mut stream_adapter,
|
||||||
|
&mut state.token_state,
|
||||||
|
&mut rhai::Position::default(),
|
||||||
|
);
|
||||||
|
state.buf = stream_adapter.buf;
|
||||||
|
match &next_token {
|
||||||
|
rhai::Token::LeftBrace
|
||||||
|
| rhai::Token::LeftBracket
|
||||||
|
| rhai::Token::LeftParen
|
||||||
|
| rhai::Token::MapStart => {
|
||||||
|
if state.unclosed_bracket_count < 0 {
|
||||||
|
state.unclosed_bracket_count = 0;
|
||||||
|
}
|
||||||
|
state.unclosed_bracket_count += 1;
|
||||||
|
}
|
||||||
|
rhai::Token::RightBrace | rhai::Token::RightBracket | rhai::Token::RightParen => {
|
||||||
|
state.unclosed_bracket_count -= 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
let res = match &next_token {
|
||||||
|
rhai::Token::IntegerConstant(_) => "number",
|
||||||
|
rhai::Token::FloatConstant(_) => "number",
|
||||||
|
rhai::Token::Identifier(_) => {
|
||||||
|
if state.is_defining_identifier {
|
||||||
|
"def"
|
||||||
|
} else {
|
||||||
|
"identifier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rhai::Token::CharConstant(_) => "string-2",
|
||||||
|
rhai::Token::StringConstant(_) => "string",
|
||||||
|
rhai::Token::InterpolatedString(_) => {
|
||||||
|
state.interpolated_str_brace_stack.push(0);
|
||||||
|
"string"
|
||||||
|
}
|
||||||
|
rhai::Token::LeftBrace => {
|
||||||
|
if let Some(brace_counting) = state.interpolated_str_brace_stack.last_mut() {
|
||||||
|
*brace_counting += 1;
|
||||||
|
}
|
||||||
|
"bracket"
|
||||||
|
}
|
||||||
|
rhai::Token::RightBrace => {
|
||||||
|
if let Some(brace_counting) = state.interpolated_str_brace_stack.last_mut() {
|
||||||
|
*brace_counting -= 1;
|
||||||
|
if *brace_counting == 0 {
|
||||||
|
state.interpolated_str_brace_stack.pop();
|
||||||
|
state.token_state.is_within_text_terminated_by = Some("`".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"bracket"
|
||||||
|
}
|
||||||
|
rhai::Token::LeftParen => "bracket",
|
||||||
|
rhai::Token::RightParen => "bracket",
|
||||||
|
rhai::Token::LeftBracket => "bracket",
|
||||||
|
rhai::Token::RightBracket => "bracket",
|
||||||
|
rhai::Token::QuestionBracket => "bracket",
|
||||||
|
rhai::Token::Unit => "bracket", // empty fn parens are parsed as this
|
||||||
|
rhai::Token::Plus => "operator",
|
||||||
|
rhai::Token::UnaryPlus => "operator",
|
||||||
|
rhai::Token::Minus => "operator",
|
||||||
|
rhai::Token::UnaryMinus => "operator",
|
||||||
|
rhai::Token::Multiply => "operator",
|
||||||
|
rhai::Token::Divide => "operator",
|
||||||
|
rhai::Token::Modulo => "operator",
|
||||||
|
rhai::Token::PowerOf => "operator",
|
||||||
|
rhai::Token::LeftShift => "operator",
|
||||||
|
rhai::Token::RightShift => "operator",
|
||||||
|
rhai::Token::SemiColon => "operator",
|
||||||
|
rhai::Token::Colon => "operator",
|
||||||
|
rhai::Token::DoubleColon => "operator",
|
||||||
|
rhai::Token::Comma => "operator",
|
||||||
|
rhai::Token::Period => "operator",
|
||||||
|
rhai::Token::ExclusiveRange => "operator",
|
||||||
|
rhai::Token::InclusiveRange => "operator",
|
||||||
|
rhai::Token::MapStart => "bracket",
|
||||||
|
rhai::Token::Equals => "operator",
|
||||||
|
rhai::Token::True => "builtin",
|
||||||
|
rhai::Token::False => "builtin",
|
||||||
|
rhai::Token::Let => "keyword",
|
||||||
|
rhai::Token::Const => "keyword",
|
||||||
|
rhai::Token::If => "keyword",
|
||||||
|
rhai::Token::Else => "keyword",
|
||||||
|
rhai::Token::While => "keyword",
|
||||||
|
rhai::Token::Loop => "keyword",
|
||||||
|
rhai::Token::For => "keyword",
|
||||||
|
rhai::Token::In => "keyword",
|
||||||
|
rhai::Token::NotIn => "keyword",
|
||||||
|
rhai::Token::LessThan => "operator",
|
||||||
|
rhai::Token::GreaterThan => "operator",
|
||||||
|
rhai::Token::LessThanEqualsTo => "operator",
|
||||||
|
rhai::Token::GreaterThanEqualsTo => "operator",
|
||||||
|
rhai::Token::EqualsTo => "operator",
|
||||||
|
rhai::Token::NotEqualsTo => "operator",
|
||||||
|
rhai::Token::Bang => "operator",
|
||||||
|
rhai::Token::Elvis => "operator",
|
||||||
|
rhai::Token::DoubleQuestion => "operator",
|
||||||
|
rhai::Token::Pipe => "operator",
|
||||||
|
rhai::Token::Or => "operator",
|
||||||
|
rhai::Token::XOr => "operator",
|
||||||
|
rhai::Token::Ampersand => "operator",
|
||||||
|
rhai::Token::And => "operator",
|
||||||
|
rhai::Token::Fn => "keyword",
|
||||||
|
rhai::Token::Continue => "keyword",
|
||||||
|
rhai::Token::Break => "keyword",
|
||||||
|
rhai::Token::Return => "keyword",
|
||||||
|
rhai::Token::Throw => "keyword",
|
||||||
|
rhai::Token::PlusAssign => "operator",
|
||||||
|
rhai::Token::MinusAssign => "operator",
|
||||||
|
rhai::Token::MultiplyAssign => "operator",
|
||||||
|
rhai::Token::DivideAssign => "operator",
|
||||||
|
rhai::Token::LeftShiftAssign => "operator",
|
||||||
|
rhai::Token::RightShiftAssign => "operator",
|
||||||
|
rhai::Token::AndAssign => "operator",
|
||||||
|
rhai::Token::OrAssign => "operator",
|
||||||
|
rhai::Token::XOrAssign => "operator",
|
||||||
|
rhai::Token::ModuloAssign => "operator",
|
||||||
|
rhai::Token::PowerOfAssign => "operator",
|
||||||
|
rhai::Token::Private => "keyword",
|
||||||
|
// Import/Export/As tokens not available in this Rhai version
|
||||||
|
rhai::Token::DoubleArrow => "operator",
|
||||||
|
rhai::Token::Underscore => "operator",
|
||||||
|
rhai::Token::Switch => "keyword",
|
||||||
|
rhai::Token::Do => "keyword",
|
||||||
|
rhai::Token::Until => "keyword",
|
||||||
|
rhai::Token::Try => "keyword",
|
||||||
|
rhai::Token::Catch => "keyword",
|
||||||
|
rhai::Token::Comment(_) => "comment",
|
||||||
|
rhai::Token::LexError(e) => {
|
||||||
|
console::log_1(&JsValue::from_str(&format!("LexError: {}", e)));
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
rhai::Token::Reserved(_) => "keyword",
|
||||||
|
// Custom token not available in this Rhai version
|
||||||
|
rhai::Token::EOF => return Ok(None),
|
||||||
|
token @ _ => {
|
||||||
|
console::log_1(&JsValue::from_str(&format!("Unhandled token {:?}", token)));
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &next_token {
|
||||||
|
rhai::Token::Fn | rhai::Token::Let | rhai::Token::Const | rhai::Token::For => {
|
||||||
|
state.is_defining_identifier = true;
|
||||||
|
}
|
||||||
|
rhai::Token::Comment(_) => {}
|
||||||
|
_ => {
|
||||||
|
state.is_defining_identifier = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(res.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indent(mode: &RhaiMode, state: &State, text_after: String) -> Option<u32> {
|
||||||
|
let should_dedent = || {
|
||||||
|
text_after
|
||||||
|
.trim_start()
|
||||||
|
.starts_with(['}', ']', ')'].as_ref())
|
||||||
|
};
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if state.unclosed_bracket_count > 0 {
|
||||||
|
if should_dedent() {
|
||||||
|
Some(state.line_indent)
|
||||||
|
} else {
|
||||||
|
Some(state.line_indent + mode.indent_unit)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if should_dedent() {
|
||||||
|
Some(state.line_indent.saturating_sub(mode.indent_unit))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
rust/runner/Cargo.toml
Normal file
28
rust/runner/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "runner"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
minimax = { workspace = true }
|
||||||
|
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
|
|
||||||
|
wasm-bindgen = { workspace = true }
|
||||||
|
js-sys = { workspace = true }
|
||||||
|
web-sys = { workspace = true }
|
||||||
|
console_error_panic_hook = { workspace = true }
|
||||||
|
wee_alloc = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde-wasm-bindgen = { workspace = true }
|
||||||
|
rhai = { workspace = true }
|
||||||
|
getrandom = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
rhai = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
getrandom = { workspace = true, features = ["js"] }
|
||||||
30
rust/runner/package.json
Normal file
30
rust/runner/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "script-runner-wasm",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Rust WASM script runner",
|
||||||
|
"main": "pkg/script_runner.js",
|
||||||
|
"types": "pkg/script_runner.d.ts",
|
||||||
|
"files": [
|
||||||
|
"pkg"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "wasm-pack build --target web --out-dir pkg",
|
||||||
|
"build:nodejs": "wasm-pack build --target nodejs --out-dir pkg-node",
|
||||||
|
"build:bundler": "wasm-pack build --target bundler --out-dir pkg-bundler",
|
||||||
|
"dev": "wasm-pack build --dev --target web --out-dir pkg"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "."
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"wasm",
|
||||||
|
"rust",
|
||||||
|
"script-runner"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"wasm-pack": "^0.12.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
rust/runner/src/ansi.rs
Normal file
12
rust/runner/src/ansi.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#![expect(clippy::allow_attributes)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
pub const RESET: &str = "\x1b[0m";
|
||||||
|
pub const RED: &str = "\x1b[31m";
|
||||||
|
pub const BLUE: &str = "\x1b[34m";
|
||||||
|
pub const MAGENTA: &str = "\x1b[35m";
|
||||||
|
|
||||||
|
pub const UP: &str = "\x1b[A";
|
||||||
|
pub const DOWN: &str = "\x1b[B";
|
||||||
|
pub const LEFT: &str = "\x1b[D";
|
||||||
|
pub const RIGHT: &str = "\x1b[C";
|
||||||
421
rust/runner/src/gamestate.rs
Normal file
421
rust/runner/src/gamestate.rs
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use minimax::{
|
||||||
|
agents::{Agent, RhaiAgent},
|
||||||
|
game::Board,
|
||||||
|
};
|
||||||
|
use rand::{rngs::StdRng, SeedableRng};
|
||||||
|
use rhai::ParseError;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::ansi;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum GameState {
|
||||||
|
/// Round 1, red is maximizing
|
||||||
|
Red,
|
||||||
|
|
||||||
|
/// Round 2, red is minimizing (and goes second)
|
||||||
|
Blue { red_score: f32 },
|
||||||
|
|
||||||
|
/// Game over, red won
|
||||||
|
RedWins { blue_score: f32 },
|
||||||
|
|
||||||
|
/// Game over, blue won
|
||||||
|
BlueWins { blue_score: f32 },
|
||||||
|
|
||||||
|
/// Game over, draw
|
||||||
|
DrawScore { score: f32 },
|
||||||
|
|
||||||
|
/// Invalid board, draw
|
||||||
|
DrawInvalid,
|
||||||
|
|
||||||
|
/// Error, end early
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct MinMaxGame {
|
||||||
|
red_agent: RhaiAgent<StdRng>,
|
||||||
|
blue_agent: RhaiAgent<StdRng>,
|
||||||
|
|
||||||
|
board: Board,
|
||||||
|
is_red_turn: bool,
|
||||||
|
is_first_print: bool,
|
||||||
|
state: GameState,
|
||||||
|
|
||||||
|
game_state_callback: Box<dyn Fn(&str) + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl MinMaxGame {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(
|
||||||
|
red_script: &str,
|
||||||
|
red_print_callback: js_sys::Function,
|
||||||
|
red_debug_callback: js_sys::Function,
|
||||||
|
|
||||||
|
blue_script: &str,
|
||||||
|
blue_print_callback: js_sys::Function,
|
||||||
|
blue_debug_callback: js_sys::Function,
|
||||||
|
|
||||||
|
game_state_callback: js_sys::Function,
|
||||||
|
) -> Result<MinMaxGame, String> {
|
||||||
|
Self::new_native(
|
||||||
|
red_script,
|
||||||
|
move |s| {
|
||||||
|
let _ = red_print_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = red_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
blue_script,
|
||||||
|
move |s| {
|
||||||
|
let _ = blue_print_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = blue_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = game_state_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|x| format!("Error at {}: {}", x.1, x.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_native(
|
||||||
|
red_script: &str,
|
||||||
|
red_print_callback: impl Fn(&str) + 'static,
|
||||||
|
red_debug_callback: impl Fn(&str) + 'static,
|
||||||
|
|
||||||
|
blue_script: &str,
|
||||||
|
blue_print_callback: impl Fn(&str) + 'static,
|
||||||
|
blue_debug_callback: impl Fn(&str) + 'static,
|
||||||
|
|
||||||
|
game_state_callback: impl Fn(&str) + 'static,
|
||||||
|
) -> Result<MinMaxGame, ParseError> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
let mut seed1 = [0u8; 32];
|
||||||
|
let mut seed2 = [0u8; 32];
|
||||||
|
getrandom::getrandom(&mut seed1).unwrap();
|
||||||
|
getrandom::getrandom(&mut seed2).unwrap();
|
||||||
|
|
||||||
|
Ok(MinMaxGame {
|
||||||
|
board: Board::new(),
|
||||||
|
is_first_print: true,
|
||||||
|
is_red_turn: true,
|
||||||
|
state: GameState::Red,
|
||||||
|
|
||||||
|
red_agent: RhaiAgent::new(
|
||||||
|
red_script,
|
||||||
|
StdRng::from_seed(seed1),
|
||||||
|
red_print_callback,
|
||||||
|
red_debug_callback,
|
||||||
|
)?,
|
||||||
|
|
||||||
|
blue_agent: RhaiAgent::new(
|
||||||
|
blue_script,
|
||||||
|
StdRng::from_seed(seed2),
|
||||||
|
blue_print_callback,
|
||||||
|
blue_debug_callback,
|
||||||
|
)?,
|
||||||
|
|
||||||
|
game_state_callback: Box::new(game_state_callback),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this game over for any reason?
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_done(&self) -> bool {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. }
|
||||||
|
| GameState::DrawInvalid
|
||||||
|
| GameState::Error
|
||||||
|
| GameState::BlueWins { .. }
|
||||||
|
| GameState::RedWins { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn red_won(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(false),
|
||||||
|
GameState::DrawInvalid { .. } => Some(false),
|
||||||
|
GameState::BlueWins { .. } => Some(false),
|
||||||
|
GameState::RedWins { .. } => Some(true),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn blue_won(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(false),
|
||||||
|
GameState::DrawInvalid { .. } => Some(false),
|
||||||
|
GameState::BlueWins { .. } => Some(true),
|
||||||
|
GameState::RedWins { .. } => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_draw_score(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(true),
|
||||||
|
GameState::DrawInvalid { .. } => Some(false),
|
||||||
|
GameState::BlueWins { .. } => Some(false),
|
||||||
|
GameState::RedWins { .. } => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_draw_invalid(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(false),
|
||||||
|
GameState::DrawInvalid { .. } => Some(true),
|
||||||
|
GameState::BlueWins { .. } => Some(false),
|
||||||
|
GameState::RedWins { .. } => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_error(&self) -> bool {
|
||||||
|
match self.state {
|
||||||
|
GameState::Error => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play one turn
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn step(&mut self) -> Result<(), String> {
|
||||||
|
if self.is_first_print {
|
||||||
|
self.print_board("", "");
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = match (self.state, self.is_red_turn) {
|
||||||
|
(GameState::Blue { .. }, false) => self.blue_agent.step_max(&self.board),
|
||||||
|
(GameState::Red, false) => self.blue_agent.step_min(&self.board),
|
||||||
|
(GameState::Blue { .. }, true) => self.red_agent.step_min(&self.board),
|
||||||
|
(GameState::Red, true) => self.red_agent.step_max(&self.board),
|
||||||
|
|
||||||
|
// Game is done, do nothing
|
||||||
|
(GameState::Error, _)
|
||||||
|
| (GameState::BlueWins { .. }, _)
|
||||||
|
| (GameState::RedWins { .. }, _)
|
||||||
|
| (GameState::DrawInvalid, _)
|
||||||
|
| (GameState::DrawScore { .. }, _) => return Ok(()),
|
||||||
|
}
|
||||||
|
.map_err(|err| format!("{err}"))?;
|
||||||
|
|
||||||
|
let player = self.is_red_turn.then_some("Red").unwrap_or("Blue");
|
||||||
|
let player_name = self
|
||||||
|
.is_red_turn
|
||||||
|
.then_some(self.red_agent.name())
|
||||||
|
.unwrap_or(self.blue_agent.name());
|
||||||
|
|
||||||
|
if !self.board.play(action, player) {
|
||||||
|
self.state = GameState::Error;
|
||||||
|
return Err(format!(
|
||||||
|
"{player} ({player_name}) made an invalid move {action}",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.print_board(
|
||||||
|
self.is_red_turn.then_some(ansi::RED).unwrap_or(ansi::BLUE),
|
||||||
|
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||||
|
);
|
||||||
|
(self.game_state_callback)("\n\r");
|
||||||
|
|
||||||
|
if !self.board.is_full() && !self.board.is_stuck() {
|
||||||
|
// This was not the last move
|
||||||
|
self.is_red_turn = !self.is_red_turn;
|
||||||
|
} else {
|
||||||
|
// This was the last move
|
||||||
|
|
||||||
|
// Close board
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}╙{}╜\n\r",
|
||||||
|
" ".repeat(6),
|
||||||
|
" ".repeat(self.board.size())
|
||||||
|
));
|
||||||
|
|
||||||
|
// Evaluate board and update state
|
||||||
|
match (self.state, self.board.evaluate()) {
|
||||||
|
// Start next round
|
||||||
|
(GameState::Red, Some(red_score)) => {
|
||||||
|
self.board = Board::new();
|
||||||
|
self.is_first_print = true;
|
||||||
|
self.is_red_turn = false;
|
||||||
|
self.state = GameState::Blue { red_score }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game over
|
||||||
|
(GameState::Blue { red_score }, Some(blue_score)) => {
|
||||||
|
self.state = match red_score.total_cmp(&blue_score) {
|
||||||
|
Ordering::Equal => GameState::DrawScore { score: red_score },
|
||||||
|
Ordering::Greater => GameState::RedWins { blue_score },
|
||||||
|
Ordering::Less => GameState::BlueWins { blue_score },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not evaluate board, tie by default
|
||||||
|
(GameState::Red, None) | (GameState::Blue { .. }, None) => {
|
||||||
|
self.state = GameState::DrawInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other code should make sure this never happens
|
||||||
|
(GameState::BlueWins { .. }, _)
|
||||||
|
| (GameState::RedWins { .. }, _)
|
||||||
|
| (GameState::DrawInvalid, _)
|
||||||
|
| (GameState::DrawScore { .. }, _)
|
||||||
|
| (GameState::Error, _) => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.board.is_stuck() {
|
||||||
|
self.state = GameState::DrawInvalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print depending on new state
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { score } => {
|
||||||
|
(self.game_state_callback)(&format!("Tie! Score: {score:.2}\n\r"));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::DrawInvalid => {
|
||||||
|
(self.game_state_callback)(&format!("Tie, invalid board!\n\r"));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::RedWins { blue_score, .. } => {
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Blue score:{} {blue_score:.2}\n\r",
|
||||||
|
ansi::BLUE,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Red wins!{}\n\r",
|
||||||
|
ansi::RED,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::BlueWins { blue_score, .. } => {
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Blue score:{} {blue_score:.2}\n\r",
|
||||||
|
ansi::BLUE,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Blue wins!{}\n\r",
|
||||||
|
ansi::BLUE,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::Blue { red_score } => {
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Red score:{} {red_score:.2}\n\r",
|
||||||
|
ansi::RED,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other code should make sure this never happens
|
||||||
|
GameState::Error | GameState::Red => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_board(&mut self, color: &str, player: &str) {
|
||||||
|
let board_label = format!("{}{:<6}{}", color, player, ansi::RESET);
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"\r{}{}{}{}",
|
||||||
|
board_label,
|
||||||
|
if self.is_first_print { '╓' } else { '║' },
|
||||||
|
self.board.prettyprint(),
|
||||||
|
if self.is_first_print { '╖' } else { '║' },
|
||||||
|
));
|
||||||
|
self.is_first_print = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: tests
|
||||||
|
//
|
||||||
|
// TODO:
|
||||||
|
// - infinite loop
|
||||||
|
// - random is different
|
||||||
|
// - incorrect return type
|
||||||
|
// - globals
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_random() {
|
||||||
|
const SCRIPT: &str = r#"
|
||||||
|
fn random_action(board) {
|
||||||
|
let symb = rand_symb();
|
||||||
|
let pos = rand_int(0, 10);
|
||||||
|
let action = Action(symb, pos);
|
||||||
|
|
||||||
|
while !board.can_play(action) {
|
||||||
|
let symb = rand_symb();
|
||||||
|
let pos = rand_int(0, 10);
|
||||||
|
action = Action(symb, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_min(board) {
|
||||||
|
random_action(board)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_max(board) {
|
||||||
|
random_action(board)
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut game =
|
||||||
|
MinMaxGame::new_native(&SCRIPT, |_| {}, |_| {}, &SCRIPT, |_| {}, |_| {}, |_| {}).unwrap();
|
||||||
|
|
||||||
|
let mut n = 0;
|
||||||
|
while !game.is_done() {
|
||||||
|
println!("{:?}", game.step());
|
||||||
|
println!("{:?}", game.board);
|
||||||
|
|
||||||
|
n += 1;
|
||||||
|
assert!(n < 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infinite_loop() {
|
||||||
|
const SCRIPT: &str = r#"
|
||||||
|
fn step_min(board) {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_max(board) {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut game =
|
||||||
|
MinMaxGame::new_native(&SCRIPT, |_| {}, |_| {}, &SCRIPT, |_| {}, |_| {}, |_| {}).unwrap();
|
||||||
|
|
||||||
|
while !game.is_done() {
|
||||||
|
println!("{:?}", game.step());
|
||||||
|
println!("{:?}", game.board);
|
||||||
|
}
|
||||||
|
}
|
||||||
288
rust/runner/src/gamestatehuman.rs
Normal file
288
rust/runner/src/gamestatehuman.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
use minimax::{
|
||||||
|
agents::{Agent, RhaiAgent},
|
||||||
|
game::Board,
|
||||||
|
};
|
||||||
|
use rand::{rngs::StdRng, SeedableRng};
|
||||||
|
use rhai::ParseError;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::{ansi, terminput::TermInput};
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct GameStateHuman {
|
||||||
|
/// Red player
|
||||||
|
human: TermInput,
|
||||||
|
|
||||||
|
// Blue player
|
||||||
|
agent: RhaiAgent<StdRng>,
|
||||||
|
|
||||||
|
board: Board,
|
||||||
|
is_red_turn: bool,
|
||||||
|
is_first_turn: bool,
|
||||||
|
is_error: bool,
|
||||||
|
red_score: Option<f32>,
|
||||||
|
|
||||||
|
game_state_callback: Box<dyn Fn(&str) + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl GameStateHuman {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(
|
||||||
|
max_script: &str,
|
||||||
|
max_print_callback: js_sys::Function,
|
||||||
|
max_debug_callback: js_sys::Function,
|
||||||
|
|
||||||
|
game_state_callback: js_sys::Function,
|
||||||
|
) -> Result<GameStateHuman, String> {
|
||||||
|
Self::new_native(
|
||||||
|
max_script,
|
||||||
|
move |s| {
|
||||||
|
let _ = max_print_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = max_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = game_state_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|x| format!("Error at {}: {}", x.1, x.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_native(
|
||||||
|
max_script: &str,
|
||||||
|
max_print_callback: impl Fn(&str) + 'static,
|
||||||
|
max_debug_callback: impl Fn(&str) + 'static,
|
||||||
|
|
||||||
|
game_state_callback: impl Fn(&str) + 'static,
|
||||||
|
) -> Result<Self, ParseError> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
let mut seed1 = [0u8; 32];
|
||||||
|
let mut seed2 = [0u8; 32];
|
||||||
|
getrandom::getrandom(&mut seed1).unwrap();
|
||||||
|
getrandom::getrandom(&mut seed2).unwrap();
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
board: Board::new(),
|
||||||
|
is_red_turn: true,
|
||||||
|
is_first_turn: true,
|
||||||
|
is_error: false,
|
||||||
|
red_score: None,
|
||||||
|
|
||||||
|
human: TermInput::new(ansi::RED.to_string()),
|
||||||
|
agent: RhaiAgent::new(
|
||||||
|
max_script,
|
||||||
|
StdRng::from_seed(seed1),
|
||||||
|
max_print_callback,
|
||||||
|
max_debug_callback,
|
||||||
|
)?,
|
||||||
|
|
||||||
|
game_state_callback: Box::new(game_state_callback),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_done(&self) -> bool {
|
||||||
|
(self.board.is_full() && self.red_score.is_some()) || self.is_error
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_error(&self) -> bool {
|
||||||
|
self.is_error
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn print_start(&mut self) -> Result<(), String> {
|
||||||
|
self.print_board("", "");
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
|
||||||
|
if !self.is_red_turn {
|
||||||
|
let action = {
|
||||||
|
if self.red_score.is_none() {
|
||||||
|
self.agent.step_min(&self.board)
|
||||||
|
} else {
|
||||||
|
self.agent.step_max(&self.board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|err| format!("{err}"))?;
|
||||||
|
|
||||||
|
if !self.board.play(action, "Blue") {
|
||||||
|
self.is_error = true;
|
||||||
|
return Err(format!(
|
||||||
|
"{} ({}) made an invalid move {}",
|
||||||
|
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||||
|
self.is_red_turn
|
||||||
|
.then_some("Human")
|
||||||
|
.unwrap_or(self.agent.name()),
|
||||||
|
action
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.print_board(
|
||||||
|
self.is_red_turn.then_some(ansi::RED).unwrap_or(ansi::BLUE),
|
||||||
|
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||||
|
);
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
self.is_red_turn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.print_ui();
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn print_board(&mut self, color: &str, player: &str) {
|
||||||
|
let board_label = format!("{}{:<6}{}", color, player, ansi::RESET);
|
||||||
|
|
||||||
|
// Print board
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"\r{}{}{}{}",
|
||||||
|
board_label,
|
||||||
|
if self.is_first_turn { '╓' } else { '║' },
|
||||||
|
self.board.prettyprint(),
|
||||||
|
if self.is_first_turn { '╖' } else { '║' },
|
||||||
|
));
|
||||||
|
self.is_first_turn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn print_ui(&mut self) {
|
||||||
|
(self.game_state_callback)(
|
||||||
|
&self
|
||||||
|
.human
|
||||||
|
.print_state(&self.board, self.red_score.is_some()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn take_input(&mut self, data: String) -> Result<(), String> {
|
||||||
|
self.human.process_input(&self.board, data);
|
||||||
|
self.print_ui();
|
||||||
|
|
||||||
|
if let Some(action) = self.human.pop_action() {
|
||||||
|
if !self.board.play(action, "Red") {
|
||||||
|
self.is_error = true;
|
||||||
|
return Err(format!(
|
||||||
|
"{} ({}) made an invalid move {}",
|
||||||
|
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||||
|
self.is_red_turn
|
||||||
|
.then_some("Human")
|
||||||
|
.unwrap_or(self.agent.name()),
|
||||||
|
action
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.is_red_turn = false;
|
||||||
|
|
||||||
|
if self.board.is_full() {
|
||||||
|
self.print_end()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.print_board(ansi::RED, "Red");
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
|
||||||
|
let action = {
|
||||||
|
if self.red_score.is_none() {
|
||||||
|
self.agent.step_min(&self.board)
|
||||||
|
} else {
|
||||||
|
self.agent.step_max(&self.board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map_err(|err| format!("{err}"))?;
|
||||||
|
|
||||||
|
if !self.board.play(action, "Blue") {
|
||||||
|
self.is_error = true;
|
||||||
|
return Err(format!(
|
||||||
|
"{} ({}) made an invalid move {}",
|
||||||
|
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||||
|
self.is_red_turn
|
||||||
|
.then_some("Human")
|
||||||
|
.unwrap_or(self.agent.name()),
|
||||||
|
action
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.is_red_turn = true;
|
||||||
|
|
||||||
|
if self.board.is_full() {
|
||||||
|
self.print_end()?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.print_board(ansi::BLUE, "Blue");
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
|
||||||
|
self.print_ui();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_end(&mut self) -> Result<(), String> {
|
||||||
|
let board_label = format!(
|
||||||
|
"{}{:<6}{}",
|
||||||
|
self.is_red_turn.then_some(ansi::BLUE).unwrap_or(ansi::RED),
|
||||||
|
self.is_red_turn.then_some("Blue").unwrap_or("Red"),
|
||||||
|
ansi::RESET
|
||||||
|
);
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!("\r{}║{}║", board_label, self.board.prettyprint()));
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"\r{}╙{}╜",
|
||||||
|
" ",
|
||||||
|
" ".repeat(self.board.size())
|
||||||
|
));
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
|
||||||
|
let score = self.board.evaluate().unwrap();
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"\r\n{}{} score:{} {:.2}\r\n",
|
||||||
|
self.red_score
|
||||||
|
.is_none()
|
||||||
|
.then_some(ansi::RED)
|
||||||
|
.unwrap_or(ansi::BLUE),
|
||||||
|
self.red_score.is_none().then_some("Red").unwrap_or("Blue"),
|
||||||
|
ansi::RESET,
|
||||||
|
score
|
||||||
|
));
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
|
||||||
|
match self.red_score {
|
||||||
|
// Start second round
|
||||||
|
None => {
|
||||||
|
let mut seed1 = [0u8; 32];
|
||||||
|
let mut seed2 = [0u8; 32];
|
||||||
|
getrandom::getrandom(&mut seed1).unwrap();
|
||||||
|
getrandom::getrandom(&mut seed2).unwrap();
|
||||||
|
|
||||||
|
self.board = Board::new();
|
||||||
|
self.is_red_turn = false;
|
||||||
|
self.is_first_turn = true;
|
||||||
|
self.is_error = false;
|
||||||
|
self.human = TermInput::new(ansi::RED.to_string());
|
||||||
|
self.red_score = Some(score);
|
||||||
|
|
||||||
|
self.print_start()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End game
|
||||||
|
Some(red_score) => {
|
||||||
|
if red_score == score {
|
||||||
|
(self.game_state_callback)(&format!("Tie! Score: {:.2}\r\n", score));
|
||||||
|
} else {
|
||||||
|
let red_wins = red_score > score;
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}{} wins!{}",
|
||||||
|
red_wins.then_some(ansi::RED).unwrap_or(ansi::BLUE),
|
||||||
|
red_wins.then_some("Red").unwrap_or("Blue"),
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
14
rust/runner/src/lib.rs
Normal file
14
rust/runner/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod ansi;
|
||||||
|
mod gamestate;
|
||||||
|
mod gamestatehuman;
|
||||||
|
mod terminput;
|
||||||
|
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn init_panic_hook() {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
}
|
||||||
181
rust/runner/src/terminput.rs
Normal file
181
rust/runner/src/terminput.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use minimax::game::{Board, PlayerAction, Symb};
|
||||||
|
|
||||||
|
use crate::ansi::{DOWN, LEFT, RESET, RIGHT, UP};
|
||||||
|
|
||||||
|
struct SymbolSelector {
|
||||||
|
symbols: Vec<char>,
|
||||||
|
cursor: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolSelector {
|
||||||
|
fn new(symbols: Vec<char>) -> Self {
|
||||||
|
Self { symbols, cursor: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current(&self) -> char {
|
||||||
|
self.symbols[self.cursor]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&mut self, board: &Board) {
|
||||||
|
while board.contains(Symb::from_char(self.current()).unwrap()) {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
self.cursor = self.symbols.len() - 1;
|
||||||
|
} else {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&mut self, board: &Board) {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
self.cursor = self.symbols.len() - 1;
|
||||||
|
} else {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while board.contains(Symb::from_char(self.current()).unwrap()) {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
self.cursor = self.symbols.len() - 1;
|
||||||
|
} else {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up(&mut self, board: &Board) {
|
||||||
|
if self.cursor == self.symbols.len() - 1 {
|
||||||
|
self.cursor = 0;
|
||||||
|
} else {
|
||||||
|
self.cursor += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while board.contains(Symb::from_char(self.current()).unwrap()) {
|
||||||
|
if self.cursor == self.symbols.len() - 1 {
|
||||||
|
self.cursor = 0;
|
||||||
|
} else {
|
||||||
|
self.cursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TermInput {
|
||||||
|
player_color: String,
|
||||||
|
cursor: usize,
|
||||||
|
symbol_selector: SymbolSelector,
|
||||||
|
|
||||||
|
/// Set to Some() when the player selects an action.
|
||||||
|
/// Should be cleared and applied immediately.
|
||||||
|
queued_action: Option<PlayerAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TermInput {
|
||||||
|
pub fn new(player_color: String) -> Self {
|
||||||
|
Self {
|
||||||
|
cursor: 0,
|
||||||
|
queued_action: None,
|
||||||
|
|
||||||
|
player_color,
|
||||||
|
symbol_selector: SymbolSelector::new(vec![
|
||||||
|
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '+', '-', '×', '÷',
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_action(&mut self) -> Option<PlayerAction> {
|
||||||
|
self.queued_action.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_state(&mut self, board: &Board, minimize: bool) -> String {
|
||||||
|
let cursor_max = board.size() - 1;
|
||||||
|
self.symbol_selector.check(board);
|
||||||
|
|
||||||
|
let board_label = format!(
|
||||||
|
"{}{:<6}{RESET}",
|
||||||
|
self.player_color,
|
||||||
|
if minimize { "Min" } else { "Max" },
|
||||||
|
);
|
||||||
|
|
||||||
|
return format!(
|
||||||
|
"\r{}╙{}{}{}{RESET}{}╜ {}",
|
||||||
|
board_label,
|
||||||
|
// Cursor
|
||||||
|
" ".repeat(self.cursor),
|
||||||
|
self.player_color,
|
||||||
|
if board.is_full() {
|
||||||
|
' '
|
||||||
|
} else {
|
||||||
|
self.symbol_selector.current()
|
||||||
|
},
|
||||||
|
// RESET
|
||||||
|
" ".repeat(cursor_max - self.cursor),
|
||||||
|
self.symbol_selector
|
||||||
|
.symbols
|
||||||
|
.iter()
|
||||||
|
.map(|x| {
|
||||||
|
if board.contains(Symb::from_char(*x).unwrap()) {
|
||||||
|
" ".to_string()
|
||||||
|
} else if *x == self.symbol_selector.current() {
|
||||||
|
format!("{}{x}{RESET}", self.player_color,)
|
||||||
|
} else {
|
||||||
|
format!("{x}",)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_input(&mut self, board: &Board, data: String) {
|
||||||
|
let cursor_max = board.size() - 1;
|
||||||
|
self.symbol_selector.check(board);
|
||||||
|
|
||||||
|
match &data[..] {
|
||||||
|
RIGHT => {
|
||||||
|
self.cursor = cursor_max.min(self.cursor + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
LEFT => {
|
||||||
|
if self.cursor != 0 {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UP => {
|
||||||
|
self.symbol_selector.up(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
DOWN => {
|
||||||
|
self.symbol_selector.down(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
" " | "\n" | "\r" => {
|
||||||
|
let symb = Symb::from_char(self.symbol_selector.current());
|
||||||
|
if let Some(symb) = symb {
|
||||||
|
let action = PlayerAction {
|
||||||
|
symb,
|
||||||
|
pos: self.cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
if board.can_play(&action) {
|
||||||
|
self.queued_action = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c => {
|
||||||
|
let symb = Symb::from_str(c);
|
||||||
|
if let Some(symb) = symb {
|
||||||
|
let action = PlayerAction {
|
||||||
|
symb,
|
||||||
|
pos: self.cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
if board.can_play(&action) {
|
||||||
|
self.queued_action = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rust/rustfmt.toml
Normal file
1
rust/rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hard_tabs = true
|
||||||
Reference in New Issue
Block a user