Compare commits
5 Commits
810772885e
...
684ae0ecf8
| Author | SHA1 | Date | |
|---|---|---|---|
| 684ae0ecf8 | |||
| 0db5b7a8f1 | |||
| bfbd9d35bc | |||
| 07aeda5e07 | |||
| 19f523d0ed |
169
agents/greed.rhai
Normal file
169
agents/greed.rhai
Normal file
@@ -0,0 +1,169 @@
|
||||
|
||||
|
||||
// Return a random valid action on the given board.
|
||||
// Used as a last resort.
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
/// Returns an array of (idx, f32) for each empty slot in the board.
|
||||
/// - idx is the index of this slot
|
||||
/// - f32 is the "influence of" this slot
|
||||
fn compute_influence(board) {
|
||||
// Fill all empty slots with fives and compute starting value
|
||||
let filled = board;
|
||||
for i in filled.free_spots_idx() {
|
||||
filled[i] = 5;
|
||||
}
|
||||
|
||||
// Compute the value of the filled board
|
||||
let base = filled.evaluate();
|
||||
|
||||
// Exit early if the board is invalid.
|
||||
// This is usually caused by zero-division.
|
||||
if (base == ()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Increase each slot's value by 1
|
||||
// and record the effect on the expression's total value.
|
||||
//
|
||||
// `influence` is an array of (slot_idx, value)
|
||||
let influence = [];
|
||||
for i in 0..board.size() {
|
||||
let slot = board[i];
|
||||
|
||||
// Ignore slots that are not empty
|
||||
if slot != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't assign directly to `filled`,
|
||||
// we want to keep it full of fives.
|
||||
// Assigning to `b` make a copy of the board.
|
||||
let b = filled;
|
||||
b[i] = 6;
|
||||
|
||||
influence.push([i, b.evaluate() - base]);
|
||||
}
|
||||
|
||||
|
||||
// Sort by increasing absolute score
|
||||
influence.sort(|a, b| {
|
||||
let a_abs = a[1].abs();
|
||||
let b_abs = b[1].abs();
|
||||
|
||||
// Returns...
|
||||
// 1 if positive (a_abs > b_abs),
|
||||
// -1 if negative,
|
||||
// 0 if equal
|
||||
return sign(a_abs - b_abs);
|
||||
});
|
||||
|
||||
return influence;
|
||||
}
|
||||
|
||||
fn place_number(board, minimize) {
|
||||
let numbers = [0,1,2,3,4,5,6,7,8,9];
|
||||
let available_numbers = numbers.retain(|x| board.contains(x));
|
||||
|
||||
let influence = compute_influence(board);
|
||||
|
||||
// Stupid edge cases, fall back to random
|
||||
if influence.len() == 0 || available_numbers.len() == 0 {
|
||||
return random_action(board);
|
||||
}
|
||||
|
||||
|
||||
// Get the most influential position
|
||||
let pos = influence[-1][0];
|
||||
let val = influence[-1][1];
|
||||
|
||||
// Pick the number we should use,
|
||||
// This is always either the largest
|
||||
// or the smallest number available to us.
|
||||
let symbol = 0;
|
||||
if minimize {
|
||||
if val > 0 {
|
||||
symbol = available_numbers[0];
|
||||
} else {
|
||||
symbol = available_numbers[-1];
|
||||
}
|
||||
} else {
|
||||
if val > 0 {
|
||||
symbol = available_numbers[-1];
|
||||
} else {
|
||||
symbol = available_numbers[0];
|
||||
}
|
||||
}
|
||||
|
||||
return Action(symbol, pos);
|
||||
}
|
||||
|
||||
fn place_op(board, minimize) {
|
||||
let ops = ["+", "-", "*", "/"];
|
||||
let available_ops = ops.retain(|x| board.contains(x));
|
||||
|
||||
// Place operations first,
|
||||
// they matter much more than numbers
|
||||
let give_up = 10;
|
||||
if !available_ops.is_empty() {
|
||||
let aa = available_ops.rand_shuffle();
|
||||
let pos = rand_int(0, 10);
|
||||
let action = Action(aa[0], pos);
|
||||
|
||||
while !board.can_play(action) {
|
||||
let pos = rand_int(0, 10);
|
||||
action = Action(aa[0], pos);
|
||||
|
||||
// In case there are no valid operator moves
|
||||
give_up -= 1;
|
||||
if give_up == 0 { break }
|
||||
}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
// Could not place an operation
|
||||
return ();
|
||||
}
|
||||
|
||||
|
||||
// Main step function (shared between min and max)
|
||||
fn greed_step(board, minimize) {
|
||||
|
||||
let action = place_op(board, minimize);
|
||||
if action == () {
|
||||
action = place_number(board, minimize);
|
||||
}
|
||||
|
||||
if board.can_play(action) {
|
||||
return action;
|
||||
}
|
||||
|
||||
// Prevent invalid moves, random fallback
|
||||
return random_action(board);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Minimizer step
|
||||
fn step_min(board) {
|
||||
greed_step(board, true)
|
||||
}
|
||||
|
||||
// Maximizer step
|
||||
fn step_max(board) {
|
||||
greed_step(board, false)
|
||||
}
|
||||
21
agents/random.rhai
Normal file
21
agents/random.rhai
Normal file
@@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
||||
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
|
||||
7
webui/.eslintrc.json
Normal file
7
webui/.eslintrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals"],
|
||||
"rules": {
|
||||
"@next/next/no-img-element": "off",
|
||||
"react-hooks/exhaustive-deps": "warn"
|
||||
}
|
||||
}
|
||||
4
webui/.prettierrc
Normal file
4
webui/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"tabWidth": 4
|
||||
}
|
||||
719
webui/bun.lock
Normal file
719
webui/bun.lock
Normal file
@@ -0,0 +1,719 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "rhai-playground-next",
|
||||
"dependencies": {
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"clsx": "^2.0.0",
|
||||
"codemirror": "^5.65.1",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next": "14.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/codemirror": "0.0.96",
|
||||
"@types/node": "^20.8.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-config-next": "14.0.0",
|
||||
"typescript": "^5.2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
|
||||
|
||||
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@next/env": ["@next/env@14.0.0", "", {}, "sha512-cIKhxkfVELB6hFjYsbtEeTus2mwrTC+JissfZYM0n+8Fv+g8ucUfOlm3VEDtwtwydZ0Nuauv3bl0qF82nnCAqA=="],
|
||||
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@14.0.0", "", { "dependencies": { "glob": "7.1.7" } }, "sha512-Ye37nNI09V3yt7pzuzSQtwlvuJ2CGzFszHXkcTHHZgNr7EhTMFLipn3VSJChy+e5+ahTdNApPphc3qCPUsn10A=="],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HQKi159jCz4SRsPesVCiNN6tPSAFUkOuSkpJsqYTIlbHLKr1mD6be/J0TvWV6fwJekj81bZV9V/Tgx3C2HO9lA=="],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4YyQLMSaCgX/kgC1jjF3s3xSoBnwHuDhnF6WA1DWNEYRsbOOPWjcYhv8TKhRe2ApdOam+VfQSffC4ZD+X4u1Cg=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-io7fMkJ28Glj7SH8yvnlD6naIhRDnDxeE55CmpQkj3+uaA2Hko6WGY2pT5SzpQLTnGGnviK85cy8EJ2qsETj/g=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nC2h0l1Jt8LEzyQeSs/BKpXAMe0mnHIMykYALWaeddTqCv5UEN8nGO3BG8JAqW/Y8iutqJsaMe2A9itS0d/r8w=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Wf+WjXibJQ7hHXOdNOmSMW5bxeJHVf46Pwb3eLSD2L76NrytQlif9NH7JpHuFlYKCQGfKfgSYYre5rIfmnSwQw=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WTZb2G7B+CTsdigcJVkRxfcAIQj7Lf0ipPNRJ3vlSadU8f0CFGv/ST+sJwF5eSwIe6dxKoX0DG6OljDBaad+rg=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-7R8/x6oQODmNpnWVW00rlWX90sIlwluJwcvMT6GXNIBOvEf01t3fBg0AGURNKdTJg2xNuP7TyLchCL7Lh2DTiw=="],
|
||||
|
||||
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.0.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-RLK1nELvhCnxaWPF07jGU4x3tjbyx2319q43loZELqF0+iJtKutZ+Lk8SVmf/KiJkYBc7Cragadz7hb3uQvz4g=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-g6hLf1SUko+hnnaywQQZzzb3BRecQsoKkF3o/C+F+dOA4w/noVAJngUVkfwF0+2/8FzNznM7ofM6TGZO9svn7w=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||
|
||||
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.14.1", "", {}, "sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/codemirror": ["@types/codemirror@0.0.96", "", { "dependencies": { "@types/tern": "*" } }, "sha512-GTswEV26Bl1byRxpD3sKd1rT2AISr0rK9ImlJgEzfvqhcVWeu4xQKFQI6UgSC95NT5swNG4st/oRMeGVZgPj9w=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA=="],
|
||||
|
||||
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
||||
|
||||
"@types/react": ["@types/react@18.3.26", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA=="],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
||||
|
||||
"@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "", {}, "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="],
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||
|
||||
"@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="],
|
||||
|
||||
"@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="],
|
||||
|
||||
"@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="],
|
||||
|
||||
"@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="],
|
||||
|
||||
"@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="],
|
||||
|
||||
"@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="],
|
||||
|
||||
"@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="],
|
||||
|
||||
"@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="],
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||
|
||||
"@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||
|
||||
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
||||
|
||||
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
|
||||
|
||||
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
|
||||
|
||||
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
|
||||
|
||||
"array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
|
||||
|
||||
"array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
|
||||
|
||||
"array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
|
||||
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"codemirror": ["codemirror@5.65.20", "", {}, "sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||
|
||||
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
|
||||
"es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
|
||||
|
||||
"eslint-config-next": ["eslint-config-next@14.0.0", "", { "dependencies": { "@next/eslint-plugin-next": "14.0.0", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-jtXeE+/pGQ3h9n11QyyuPN50kO13GO5XvjU5ZRq6W+XTpOMjyobWmK2s7aowy0FtzA49krJzYzEU9s1RMwoJ6g=="],
|
||||
|
||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
|
||||
|
||||
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
|
||||
|
||||
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
|
||||
|
||||
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
|
||||
|
||||
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
|
||||
|
||||
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@4.6.2", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||
|
||||
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||
|
||||
"glob": ["glob@7.1.7", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||
|
||||
"globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||
|
||||
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||
|
||||
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||
|
||||
"is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||
|
||||
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||
|
||||
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||
|
||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||
|
||||
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||
|
||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||
|
||||
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||
|
||||
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||
|
||||
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||
|
||||
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||
|
||||
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||
|
||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
||||
|
||||
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lucide-react": ["lucide-react@0.548.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"next": ["next@14.0.0", "", { "dependencies": { "@next/env": "14.0.0", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.31", "styled-jsx": "5.1.1", "watchpack": "2.4.0" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.0.0", "@next/swc-darwin-x64": "14.0.0", "@next/swc-linux-arm64-gnu": "14.0.0", "@next/swc-linux-arm64-musl": "14.0.0", "@next/swc-linux-x64-gnu": "14.0.0", "@next/swc-linux-x64-musl": "14.0.0", "@next/swc-win32-arm64-msvc": "14.0.0", "@next/swc-win32-ia32-msvc": "14.0.0", "@next/swc-win32-x64-msvc": "14.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-J0jHKBJpB9zd4+c153sair0sz44mbaCHxggs8ryVXSFBuBqJ8XdE9/ozoV85xGh2VnSjahwntBZZgsihL9QznA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||
|
||||
"object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
|
||||
|
||||
"object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
|
||||
|
||||
"object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="],
|
||||
|
||||
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||
|
||||
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
|
||||
|
||||
"react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||
|
||||
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
||||
|
||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||
|
||||
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
||||
|
||||
"string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
|
||||
|
||||
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||
|
||||
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||
|
||||
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||
|
||||
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"watchpack": ["watchpack@2.4.0", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
|
||||
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"eslint-plugin-react/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
}
|
||||
}
|
||||
5
webui/next-env.d.ts
vendored
Normal file
5
webui/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
27
webui/next.config.js
Normal file
27
webui/next.config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (config) => {
|
||||
// Handle WASM files
|
||||
config.experiments = {
|
||||
...config.experiments,
|
||||
asyncWebAssembly: true,
|
||||
};
|
||||
|
||||
// Add rule for .wasm files
|
||||
config.module.rules.push({
|
||||
test: /\.wasm$/,
|
||||
type: 'asset/resource',
|
||||
});
|
||||
|
||||
// Allow importing from parent directories (for rust workspace)
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
fs: false,
|
||||
path: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
31
webui/package.json
Normal file
31
webui/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "rhai-playground-next",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"clsx": "^2.0.0",
|
||||
"codemirror": "^5.65.1",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next": "14.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/codemirror": "0.0.96",
|
||||
"@types/node": "^20.8.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"eslint": "^8.51.0",
|
||||
"eslint-config-next": "14.0.0",
|
||||
"typescript": "^5.2.0"
|
||||
},
|
||||
"packageManager": "bun@1.0.0"
|
||||
}
|
||||
24
webui/src/app/layout.tsx
Normal file
24
webui/src/app/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import "@/styles/globals.css";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Minimax",
|
||||
description: "",
|
||||
};
|
||||
|
||||
export const viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
77
webui/src/app/page.tsx
Normal file
77
webui/src/app/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { loadAllWasm } from "@/utils/wasmLoader";
|
||||
|
||||
const Playground = dynamic(() => import("@/components/Playground"), {
|
||||
ssr: false,
|
||||
loading: () => (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: "8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
Loading WASM...
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
export default function Home() {
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [isWasmLoaded, setIsWasmLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
|
||||
// Load all WASM modules
|
||||
loadAllWasm()
|
||||
.then(() => {
|
||||
setIsWasmLoaded(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load WASM modules:', error);
|
||||
// Still allow the app to load, but WASM features may not work
|
||||
setIsWasmLoaded(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!isClient || !isWasmLoaded) {
|
||||
return (
|
||||
<div
|
||||
id="loading"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: "8px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
Loading WASM...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<Playground />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
153
webui/src/components/Editor.tsx
Normal file
153
webui/src/components/Editor.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from "react";
|
||||
import styles from "@/styles/Editor.module.css";
|
||||
import { loadRhaiWasm, initRhaiMode } from "@/utils/wasmLoader";
|
||||
|
||||
// Dynamic import for CodeMirror to avoid SSR issues
|
||||
let CodeMirror: any = null;
|
||||
let isCodeMirrorReady = false;
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
import("codemirror")
|
||||
.then(async (cm) => {
|
||||
CodeMirror = cm.default;
|
||||
await import("codemirror/addon/edit/matchbrackets");
|
||||
await import("codemirror/addon/edit/closebrackets");
|
||||
//await import("codemirror/addon/selection/active-line");
|
||||
await import("codemirror/addon/comment/comment");
|
||||
// @ts-ignore - CodeMirror addon type issues
|
||||
await import("codemirror/addon/fold/brace-fold");
|
||||
// @ts-ignore - CodeMirror addon type issues
|
||||
await import("codemirror/addon/fold/foldgutter");
|
||||
// @ts-ignore - CodeMirror addon type issues
|
||||
await import("codemirror/addon/search/match-highlighter");
|
||||
require("codemirror/lib/codemirror.css");
|
||||
require("codemirror/theme/material-darker.css");
|
||||
require("codemirror/addon/fold/foldgutter.css");
|
||||
|
||||
await loadRhaiWasm();
|
||||
initRhaiMode(CodeMirror);
|
||||
console.log("✅ WASM-based Rhai mode initialized successfully");
|
||||
|
||||
isCodeMirrorReady = true;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load CodeMirror:", error);
|
||||
});
|
||||
}
|
||||
|
||||
interface EditorProps {
|
||||
initialValue?: string;
|
||||
onChange?: (editor: any, changes: any) => void;
|
||||
onReady?: (editor: any) => void;
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "minimax-editor-content";
|
||||
|
||||
export const Editor = forwardRef<any, EditorProps>(function Editor(
|
||||
{ initialValue = "", onChange, onReady, fontSize = 14 },
|
||||
ref
|
||||
) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
|
||||
const getInitialContent = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
return saved || initialValue;
|
||||
}
|
||||
return initialValue;
|
||||
};
|
||||
|
||||
const [content, setContent] = useState(getInitialContent);
|
||||
|
||||
useImperativeHandle(ref, () => editorRef.current);
|
||||
|
||||
// Initialize editor only once
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isCodeMirrorReady ||
|
||||
!CodeMirror ||
|
||||
!textareaRef.current ||
|
||||
editorRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
const editor = CodeMirror.fromTextArea(textareaRef.current, {
|
||||
lineNumbers: true,
|
||||
mode: "rhai",
|
||||
theme: "material-darker",
|
||||
indentUnit: 4,
|
||||
matchBrackets: true,
|
||||
foldGutter: {
|
||||
rangeFinder: CodeMirror.fold.brace,
|
||||
},
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
styleActiveLine: true,
|
||||
highlightSelectionMatches: {
|
||||
minChars: 3,
|
||||
showToken: true,
|
||||
annotateScrollbar: true,
|
||||
},
|
||||
rulers: [],
|
||||
autoCloseBrackets: {
|
||||
pairs: `()[]{}''""`,
|
||||
closeBefore: `)]}'":;,`,
|
||||
triples: "",
|
||||
explode: "()[]{}",
|
||||
},
|
||||
});
|
||||
|
||||
editor.setValue(content);
|
||||
|
||||
editor.on("change", (instance: any, changes: any) => {
|
||||
const newContent = instance.getValue();
|
||||
setContent(newContent);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(STORAGE_KEY, newContent);
|
||||
}
|
||||
|
||||
onChange?.(instance, changes);
|
||||
});
|
||||
|
||||
editorRef.current = editor;
|
||||
onReady?.(editor);
|
||||
|
||||
return () => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.toTextArea();
|
||||
editorRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []); // DO NOT FILL ARRAY
|
||||
|
||||
// Update font size when it changes
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
const wrapper = editorRef.current.getWrapperElement();
|
||||
if (wrapper) {
|
||||
wrapper.style.fontSize = `${fontSize}px`;
|
||||
editorRef.current.refresh();
|
||||
}
|
||||
}
|
||||
}, [fontSize]);
|
||||
|
||||
return (
|
||||
<div className={styles.editorContainer}>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
defaultValue={content}
|
||||
placeholder="Code goes here"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
552
webui/src/components/Playground.tsx
Normal file
552
webui/src/components/Playground.tsx
Normal file
@@ -0,0 +1,552 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/Button";
|
||||
import { Dropdown } from "@/components/ui/Dropdown";
|
||||
import { Slider } from "@/components/ui/Slider";
|
||||
import { SidePanel } from "@/components/ui/SidePanel";
|
||||
import { AgentSelector } from "@/components/ui/AgentSelector";
|
||||
import { Editor } from "@/components/Editor";
|
||||
import { Terminal, TerminalRef } from "@/components/Terminal";
|
||||
import {
|
||||
sendDataToScript,
|
||||
startScript,
|
||||
startScriptBulk,
|
||||
stopScript,
|
||||
} from "@/lib/runner";
|
||||
import styles from "@/styles/Playground.module.css";
|
||||
|
||||
const initialCode = `fn random_action(board) {
|
||||
let symb = rand_symb();
|
||||
let pos = rand_int(0, 10);
|
||||
let action = Action(symb, pos);
|
||||
|
||||
// If this action is invalid, randomly select a new one.
|
||||
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) {
|
||||
return random_action(board);
|
||||
}
|
||||
|
||||
fn step_max(board) {
|
||||
return random_action(board);
|
||||
}`;
|
||||
|
||||
const AGENTS = {
|
||||
// special-cased below
|
||||
Self: undefined,
|
||||
|
||||
Random: `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) {
|
||||
return random_action(board);
|
||||
}
|
||||
|
||||
fn step_max(board) {
|
||||
return random_action(board);
|
||||
}`,
|
||||
};
|
||||
|
||||
export default function Playground() {
|
||||
const [isScriptRunning, setIsScriptRunning] = useState(false);
|
||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||
const [fontSize, setFontSize] = useState(14);
|
||||
const [bulkRounds, setBulkRounds] = useState(1000);
|
||||
const [selectedAgent, setSelectedAgent] = useState("Random");
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const resultRef = useRef<HTMLTextAreaElement>(null);
|
||||
const terminalRef = useRef<TerminalRef>(null);
|
||||
|
||||
const runDisabled = isScriptRunning || !isEditorReady;
|
||||
const stopDisabled = !isScriptRunning;
|
||||
|
||||
const runHuman = useCallback(async () => {
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value = "";
|
||||
}
|
||||
|
||||
if (runDisabled || !editorRef.current) return;
|
||||
|
||||
setIsScriptRunning(true);
|
||||
|
||||
try {
|
||||
terminalRef.current?.clear();
|
||||
terminalRef.current?.focus();
|
||||
|
||||
await startScript(
|
||||
editorRef.current.getValue(),
|
||||
(line: string) => {
|
||||
if (resultRef.current) {
|
||||
let v = resultRef.current.value + line + "\n";
|
||||
if (v.length > 10000) {
|
||||
v = v.substring(v.length - 10000);
|
||||
}
|
||||
resultRef.current.value = v;
|
||||
resultRef.current.scrollTop =
|
||||
resultRef.current.scrollHeight -
|
||||
resultRef.current.clientHeight;
|
||||
}
|
||||
},
|
||||
|
||||
(line: string) => {
|
||||
terminalRef.current?.write(line);
|
||||
}
|
||||
);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||
}
|
||||
terminalRef.current?.write(
|
||||
"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" +
|
||||
String(ex).replace("\n", "\n\r") +
|
||||
"\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
setIsScriptRunning(false);
|
||||
}, [runDisabled]);
|
||||
|
||||
const runBulk = useCallback(async () => {
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value = "";
|
||||
}
|
||||
|
||||
if (runDisabled) return;
|
||||
|
||||
setIsScriptRunning(true);
|
||||
|
||||
try {
|
||||
terminalRef.current?.clear();
|
||||
terminalRef.current?.focus();
|
||||
|
||||
const agentCode = AGENTS[selectedAgent as keyof typeof AGENTS];
|
||||
const blueScript =
|
||||
agentCode || (editorRef.current?.getValue() ?? "");
|
||||
const redScript = editorRef.current?.getValue() ?? "";
|
||||
const opponentName = agentCode ? selectedAgent : "script";
|
||||
|
||||
await startScriptBulk(
|
||||
redScript,
|
||||
blueScript,
|
||||
opponentName,
|
||||
(line: string) => {
|
||||
if (resultRef.current) {
|
||||
let v = resultRef.current.value + line + "\n";
|
||||
if (v.length > 10000) {
|
||||
v = v.substring(v.length - 10000);
|
||||
}
|
||||
resultRef.current.value = v;
|
||||
resultRef.current.scrollTop =
|
||||
resultRef.current.scrollHeight -
|
||||
resultRef.current.clientHeight;
|
||||
}
|
||||
},
|
||||
|
||||
(line: string) => {
|
||||
terminalRef.current?.write(line);
|
||||
},
|
||||
bulkRounds
|
||||
);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||
}
|
||||
terminalRef.current?.write(
|
||||
"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" +
|
||||
String(ex).replace("\n", "\n\r") +
|
||||
"\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
setIsScriptRunning(false);
|
||||
}, [runDisabled, bulkRounds, selectedAgent]);
|
||||
|
||||
const stopScriptHandler = useCallback(() => {
|
||||
stopScript();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.playgroundRoot}>
|
||||
<header className={styles.header}>
|
||||
<div className={styles.headerField}>
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button
|
||||
variant="success"
|
||||
iconLeft="play"
|
||||
onClick={runHuman}
|
||||
loading={isScriptRunning}
|
||||
disabled={runDisabled}
|
||||
>
|
||||
Run
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="success"
|
||||
iconLeft="play"
|
||||
onClick={runBulk}
|
||||
loading={isScriptRunning}
|
||||
disabled={runDisabled}
|
||||
>
|
||||
Bulk Run
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="danger"
|
||||
iconLeft="stop"
|
||||
onClick={stopScriptHandler}
|
||||
disabled={stopDisabled}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<Dropdown
|
||||
trigger="Config"
|
||||
align="right"
|
||||
customContent={
|
||||
<div className={styles.configPanel}>
|
||||
<Slider
|
||||
label="Font Size"
|
||||
value={fontSize}
|
||||
min={10}
|
||||
max={24}
|
||||
step={1}
|
||||
onChange={setFontSize}
|
||||
unit="px"
|
||||
/>
|
||||
<Slider
|
||||
label="Bulk Rounds"
|
||||
value={bulkRounds}
|
||||
min={100}
|
||||
max={10000}
|
||||
step={100}
|
||||
onChange={setBulkRounds}
|
||||
unit=""
|
||||
/>
|
||||
<div className={styles.configField}>
|
||||
<label>Bulk opponent</label>
|
||||
<AgentSelector
|
||||
agents={Object.keys(AGENTS)}
|
||||
selectedAgent={selectedAgent}
|
||||
onSelect={setSelectedAgent}
|
||||
placeholder="Select an agent..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
iconLeft="help-circle"
|
||||
onClick={() => setIsHelpOpen(true)}
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className={styles.mainContent}>
|
||||
<div className={styles.leftPanel}>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
initialValue={initialCode}
|
||||
onChange={() => {}}
|
||||
onReady={() => setIsEditorReady(true)}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rightPanel}>
|
||||
<div className={styles.terminalPanel}>
|
||||
<div className={styles.panelHeader}>Terminal</div>
|
||||
<div className={styles.terminalContainer}>
|
||||
<Terminal
|
||||
ref={terminalRef}
|
||||
onData={sendDataToScript}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.outputPanel}>
|
||||
<div className={styles.panelHeader}>Output</div>
|
||||
<textarea
|
||||
ref={resultRef}
|
||||
className={styles.result}
|
||||
readOnly
|
||||
autoComplete="off"
|
||||
placeholder="Use print() to produce output"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SidePanel isOpen={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
|
||||
<h2>Game Rules</h2>
|
||||
|
||||
<p>
|
||||
This game is played in two rounds, on an empty eleven-space
|
||||
board. The first round is played on {"Red's"} board, the
|
||||
second is played on {"Blue's"}.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
On {"Red's"} board, {"Red's"} goal is to maximize the value
|
||||
of the expression.
|
||||
{" Blue's"} goal is to minimize it. Players take turns
|
||||
placing the fourteen symbols <code>0123456789+-×÷</code>
|
||||
on the board, with the maximizing player taking the first
|
||||
move.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
A {"board's"} syntax must always be valid, and the following
|
||||
rules are enforced:
|
||||
</p>
|
||||
|
||||
<ol>
|
||||
<li>Each symbol may only be used once</li>
|
||||
<li>
|
||||
The binary operators <code>+-×÷</code> may not be next
|
||||
to one another, and may not be at the end slots.
|
||||
</li>
|
||||
<li>
|
||||
The unary operator <code>-</code> (negative) must have a
|
||||
number as an argument. Therefore, it cannot be left of
|
||||
an operator (like <code>-×</code>), and it may not be in
|
||||
the rightmost slot.
|
||||
</li>
|
||||
<li>
|
||||
Unary <code>+</code> may not be used.
|
||||
</li>
|
||||
<li>
|
||||
{" "}
|
||||
<code>0</code> may not follow <code>÷</code>. This
|
||||
prevents most cases of zero-division, but{" "}
|
||||
{"isn't perfect"}.<code>÷-0</code> will result in an
|
||||
invalid board (causing a draw), and <code>÷0_+</code> is
|
||||
forbidden despite being valid syntax once the empty slot
|
||||
is filled. This is done to simplyify game logic, and
|
||||
might be improved later.
|
||||
</li>
|
||||
<li>Division by zero results in a draw.</li>
|
||||
<li>
|
||||
An incomplete board with no valid moves results in a
|
||||
draw.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>How to Play</h2>
|
||||
<ol>
|
||||
<li>
|
||||
Click <strong>Run</strong> to start a single game. Play
|
||||
against your agent in the terminal. Use your arrow keys
|
||||
(up, down, left, right) to select a symbol. Use enter or
|
||||
space to make a move.
|
||||
</li>
|
||||
<li>
|
||||
Click <strong>Bulk Run</strong> to collect statistics
|
||||
from a many games.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<code>step_min()</code> is called once per turn with the{" "}
|
||||
{"board's"} current state. This function must return an{" "}
|
||||
<code>Action</code> that aims to minimize the total
|
||||
value of the board.
|
||||
</li>
|
||||
<li>
|
||||
<code>step_max()</code> is just like{" "}
|
||||
<code>step_min</code>, but should aim to maximize the
|
||||
value of the board.{" "}
|
||||
</li>
|
||||
<li>
|
||||
Agent code may not be edited between games. Start a new
|
||||
game to use new code.
|
||||
</li>
|
||||
<li>
|
||||
If your agent takes more than 5 seconds to compute a
|
||||
move, the script will exit with an error.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Rhai basics</h2>
|
||||
<p>
|
||||
Agents are written in <a href="https://rhai.rs">Rhai</a>, a
|
||||
wonderful embedded scripting language powered by Rust. Basic
|
||||
language features are outlined below.
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
All statements must be followed by a <code>;</code>
|
||||
</li>
|
||||
<li>
|
||||
Use <code>return</code> to return a value from a
|
||||
function.
|
||||
</li>
|
||||
<li>
|
||||
<code>print(anything)</code> - Prints to the output
|
||||
panel. Prefer this over <code>debug</code>.
|
||||
</li>
|
||||
<li>
|
||||
<code>debug(anything)</code> - Prints to the output
|
||||
panel. Includes extra debug info.
|
||||
</li>
|
||||
<li>
|
||||
<code>()</code> is the {'"none"'} type, returned by some
|
||||
methods above.
|
||||
</li>
|
||||
<li>
|
||||
<code>for i in 0..5 {"{}"}</code> will iterate five
|
||||
times, with <code>i = 0, 1, 2, 3, 4</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>for i in 0..=5 {"{}"}</code> will iterate six
|
||||
times, with <code>i = 0, 1, 2, 3, 4, 5</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>let a = [];</code> initializes an empty array.
|
||||
</li>
|
||||
<li>
|
||||
<code>a.push(value)</code> adds a value to the end of an
|
||||
array
|
||||
</li>
|
||||
<li>
|
||||
<code>a.pop()</code> removes a value from the end of an
|
||||
array and returns it
|
||||
</li>
|
||||
<li>
|
||||
<code>a[0]</code> returns the first item of an array
|
||||
</li>
|
||||
<li>
|
||||
<code>a[1]</code> returns the second item of an array
|
||||
</li>
|
||||
<li>
|
||||
Refer to{" "}
|
||||
<a href="https://rhai.rs/book/language/values-and-types.html">
|
||||
the Rhai book
|
||||
</a>{" "}
|
||||
for more details.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>Notable Functions</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<code>Action(symbol, position)</code> - Creates a new
|
||||
action that places <code>symbol</code> at{" "}
|
||||
<code>position</code>. Valid symbols are{" "}
|
||||
<code>01234567890+-/*</code>. Both <code>0</code> and{" "}
|
||||
<code>{'"0"'}</code> are valid symbols.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.can_play(action)</code> - Checks if an
|
||||
action is valid. Returns a boolean.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.size()</code> - Return the total number of
|
||||
spots on this board.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.free_spots()</code> - Count the number of
|
||||
free spots on the board.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.play(action)</code> - Apply the given action
|
||||
on this board. This mutates the <code>board</code>, but
|
||||
does NOT make the move in the game. The only way to
|
||||
commit to an action is to return it from{" "}
|
||||
<code>step_min</code> or <code>step_max</code>. This
|
||||
method lets you compute potential values of a board when
|
||||
used with <code>board.evaluate()</code>.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.ith_free_slot(idx)</code> - Returns the
|
||||
index of the <code>n</code>th free slot on this board.
|
||||
Returns <code>-1</code> if no such slot exists.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.contains(symbol)</code> - Checks if this
|
||||
board contains the given symbol. Returns a boolean.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.evaluate()</code> - Return the value of a
|
||||
board if it can be computed. Returns <code>()</code>{" "}
|
||||
otherwise.
|
||||
</li>
|
||||
<li>
|
||||
<code>board.free_spots_idx(action)</code> - Checks if an
|
||||
action is valid. Returns a boolean.
|
||||
</li>
|
||||
<li>
|
||||
<code>for i in board {"{ ... }"}</code> - Iterate over
|
||||
all slots on this board. Items are returned as strings,
|
||||
empty slots are the empty string (<code>{'""'}</code>)
|
||||
</li>
|
||||
<li>
|
||||
<code>is_op(symbol)</code> - Returns <code>true</code>{" "}
|
||||
if <code>symbol</code> is one of <code>+-*/</code>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<code>rand_symb()</code> - Returns a random symbol
|
||||
(number or operation)
|
||||
</li>
|
||||
<li>
|
||||
<code>rand_op()</code> - Returns a random operator
|
||||
symbol (one of <code>+-*/</code>)
|
||||
</li>
|
||||
<li>
|
||||
<code>rand_action()</code> - Returns a random{" "}
|
||||
<code>Action</code>
|
||||
</li>
|
||||
<li>
|
||||
<code>rand_int(min, max)</code> - Returns a random
|
||||
integer between min and max, including both endpoints.
|
||||
</li>
|
||||
<li>
|
||||
<code>rand_bool(probability)</code> - Return{" "}
|
||||
<code>true</code> with the given probability. Otherwise
|
||||
return <code>false</code>.
|
||||
</li>
|
||||
<li>
|
||||
<code>rand_shuffle(array)</code> - Shuffle the given
|
||||
array
|
||||
</li>
|
||||
<li>
|
||||
<code>for p in permutations(array, 5) {"{}"}</code> -
|
||||
Iterate over all permutations of 5 elements of the given
|
||||
array.
|
||||
</li>
|
||||
</ul>
|
||||
</SidePanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
135
webui/src/components/Terminal.tsx
Normal file
135
webui/src/components/Terminal.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useImperativeHandle,
|
||||
forwardRef,
|
||||
RefObject,
|
||||
} from "react";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
|
||||
export type TerminalRef = {
|
||||
write: (data: string) => void;
|
||||
clear: () => void;
|
||||
focus: () => void;
|
||||
};
|
||||
|
||||
export const Terminal = forwardRef<
|
||||
TerminalRef,
|
||||
{
|
||||
onData: (data: String) => void;
|
||||
fontSize?: number;
|
||||
}
|
||||
>(function Terminal(props, ref) {
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
const xtermRef = useRef<any>(null);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
write: (data: string) => {
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.write(data);
|
||||
}
|
||||
},
|
||||
clear: () => {
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.clear();
|
||||
}
|
||||
},
|
||||
focus: () => {
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.focus();
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// Set to false when this component is unmounted.
|
||||
//
|
||||
// Here's what this flag prevents:
|
||||
// - <Terminal> component mounts
|
||||
// - `useEffect` runs, `mounted = true`
|
||||
// - `init_term()` function begins work
|
||||
// - before init_term() finishes, the user navigates away and <Terminal> to unmounts
|
||||
// - `useEffect` cleans up, `mounted = false`
|
||||
// - `init_term()` ccompletes, and we attempt to set `xtermRef.current`, causing issues
|
||||
let mounted = true;
|
||||
|
||||
init_term(terminalRef, props.onData, () => mounted, props.fontSize)
|
||||
.then((term) => {
|
||||
if (!mounted) return;
|
||||
xtermRef.current = term;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to initialize terminal:", err);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
if (xtermRef.current) {
|
||||
xtermRef.current.dispose();
|
||||
xtermRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [props.onData, props.fontSize]);
|
||||
|
||||
// Update font size when it changes
|
||||
useEffect(() => {
|
||||
if (xtermRef.current && props.fontSize !== undefined) {
|
||||
xtermRef.current.options.fontSize = props.fontSize;
|
||||
}
|
||||
}, [props.fontSize]);
|
||||
|
||||
return <div ref={terminalRef} style={{ height: "100%", width: "100%" }} />;
|
||||
});
|
||||
|
||||
async function init_term(
|
||||
ref: RefObject<HTMLDivElement>,
|
||||
|
||||
// Called when the terminal receives data
|
||||
onData: (data: String) => void,
|
||||
isMounted: () => boolean,
|
||||
fontSize?: number
|
||||
) {
|
||||
if (!ref.current) return;
|
||||
|
||||
const { Terminal } = await import("@xterm/xterm");
|
||||
if (!isMounted()) return;
|
||||
|
||||
const term = new Terminal({
|
||||
//"fontFamily": "Fantasque",
|
||||
rows: 24,
|
||||
fontSize: fontSize ?? 18,
|
||||
tabStopWidth: 4,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "block",
|
||||
cursorInactiveStyle: "none",
|
||||
theme: {
|
||||
background: "#1D1F21",
|
||||
foreground: "#F8F8F8",
|
||||
cursor: "#F8F8F2",
|
||||
black: "#282828",
|
||||
blue: "#0087AF",
|
||||
brightBlack: "#555555",
|
||||
brightBlue: "#87DFFF",
|
||||
brightCyan: "#28D1E7",
|
||||
brightGreen: "#A8FF60",
|
||||
brightMagenta: "#985EFF",
|
||||
brightRed: "#FFAA00",
|
||||
brightWhite: "#D0D0D0",
|
||||
brightYellow: "#F1FF52",
|
||||
cyan: "#87DFEB",
|
||||
green: "#B4EC85",
|
||||
magenta: "#BD99FF",
|
||||
red: "#ff2f00ff",
|
||||
white: "#F8F8F8",
|
||||
yellow: "#FFFFB6",
|
||||
},
|
||||
});
|
||||
|
||||
term.open(ref.current);
|
||||
term.onData(onData);
|
||||
|
||||
console.log("Terminal ready");
|
||||
return term;
|
||||
}
|
||||
191
webui/src/components/ui/AgentSelector.tsx
Normal file
191
webui/src/components/ui/AgentSelector.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { ChevronDown, Search } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import styles from "@/styles/AgentSelector.module.css";
|
||||
|
||||
interface AgentSelectorProps {
|
||||
agents: string[];
|
||||
selectedAgent: string;
|
||||
onSelect: (agent: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export function AgentSelector({
|
||||
agents,
|
||||
selectedAgent,
|
||||
onSelect,
|
||||
placeholder = "Select an agent..."
|
||||
}: AgentSelectorProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const filteredAgents = agents.filter(agent =>
|
||||
agent.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
setSearchTerm("");
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && searchRef.current) {
|
||||
searchRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
setHighlightedIndex(0);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (!isOpen) {
|
||||
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
updateDropdownPosition();
|
||||
setIsOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
setHighlightedIndex(prev =>
|
||||
prev < filteredAgents.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
setHighlightedIndex(prev =>
|
||||
prev > 0 ? prev - 1 : filteredAgents.length - 1
|
||||
);
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
if (filteredAgents[highlightedIndex]) {
|
||||
onSelect(filteredAgents[highlightedIndex]);
|
||||
setIsOpen(false);
|
||||
setSearchTerm("");
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
setSearchTerm("");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (agent: string) => {
|
||||
onSelect(agent);
|
||||
setIsOpen(false);
|
||||
setSearchTerm("");
|
||||
};
|
||||
|
||||
const updateDropdownPosition = () => {
|
||||
if (triggerRef.current) {
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
setDropdownPosition({
|
||||
top: rect.bottom + 2,
|
||||
left: rect.left,
|
||||
width: rect.width
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
updateDropdownPosition();
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.agentSelector} ref={dropdownRef}>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
className={clsx(styles.trigger, isOpen && styles.triggerOpen)}
|
||||
type="button"
|
||||
onClick={handleOpen}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
<span className={styles.triggerText}>
|
||||
{selectedAgent || placeholder}
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={clsx(styles.chevron, isOpen && styles.chevronOpen)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className={styles.dropdown}
|
||||
style={{
|
||||
top: dropdownPosition.top,
|
||||
left: dropdownPosition.left,
|
||||
width: Math.max(dropdownPosition.width, 200)
|
||||
}}
|
||||
>
|
||||
<div className={styles.searchContainer}>
|
||||
<Search size={16} className={styles.searchIcon} />
|
||||
<input
|
||||
ref={searchRef}
|
||||
type="text"
|
||||
className={styles.searchInput}
|
||||
placeholder="Search agents..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ul className={styles.agentList} role="listbox">
|
||||
{filteredAgents.length === 0 ? (
|
||||
<li className={styles.noResults}>No agents found</li>
|
||||
) : (
|
||||
filteredAgents.map((agent, index) => (
|
||||
<li
|
||||
key={agent}
|
||||
className={clsx(
|
||||
styles.agentOption,
|
||||
agent === selectedAgent && styles.selected,
|
||||
index === highlightedIndex && styles.highlighted
|
||||
)}
|
||||
onClick={() => handleSelect(agent)}
|
||||
role="option"
|
||||
aria-selected={agent === selectedAgent}
|
||||
>
|
||||
{agent}
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
webui/src/components/ui/Button.tsx
Normal file
73
webui/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import { Play, Square, Loader2 } from "lucide-react";
|
||||
import styles from "@/styles/Button.module.css";
|
||||
|
||||
const iconMap = {
|
||||
play: Play,
|
||||
stop: Square,
|
||||
loading: Loader2,
|
||||
};
|
||||
|
||||
function getIcon(iconName: string) {
|
||||
return iconMap[iconName as keyof typeof iconMap];
|
||||
}
|
||||
|
||||
interface ButtonProps
|
||||
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
|
||||
variant?:
|
||||
| "primary"
|
||||
| "success"
|
||||
| "danger"
|
||||
| "warning"
|
||||
| "info"
|
||||
| "light"
|
||||
| "dark";
|
||||
iconLeft?: string;
|
||||
iconRight?: string;
|
||||
loading?: boolean;
|
||||
children: ReactNode;
|
||||
tooltip?: string;
|
||||
type?: "submit" | "reset" | "button";
|
||||
}
|
||||
|
||||
export function Button({
|
||||
variant = "primary",
|
||||
iconLeft,
|
||||
iconRight,
|
||||
loading = false,
|
||||
children,
|
||||
className,
|
||||
disabled,
|
||||
tooltip,
|
||||
type = "button",
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type={type}
|
||||
className={clsx(
|
||||
styles.button,
|
||||
styles[`is-${variant}`],
|
||||
loading && styles.isLoading,
|
||||
className,
|
||||
)}
|
||||
disabled={disabled || loading}
|
||||
title={tooltip}
|
||||
{...props}
|
||||
>
|
||||
{iconLeft && !loading && (() => {
|
||||
const IconComponent = getIcon(iconLeft);
|
||||
return IconComponent ? <IconComponent size={16} /> : null;
|
||||
})()}
|
||||
{loading && <Loader2 size={16} className={styles.spin} />}
|
||||
<span>{children}</span>
|
||||
{iconRight && !loading && (() => {
|
||||
const IconComponent = getIcon(iconRight);
|
||||
return IconComponent ? <IconComponent size={16} /> : null;
|
||||
})()}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
115
webui/src/components/ui/Dropdown.tsx
Normal file
115
webui/src/components/ui/Dropdown.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect, ReactNode } from "react";
|
||||
import clsx from "clsx";
|
||||
import { HelpCircle, MoreHorizontal, ChevronDown } from "lucide-react";
|
||||
import styles from "@/styles/Dropdown.module.css";
|
||||
|
||||
const iconMap = {
|
||||
"help-circle": HelpCircle,
|
||||
"dots-horizontal": MoreHorizontal,
|
||||
"menu-down": ChevronDown,
|
||||
};
|
||||
|
||||
function getIcon(iconName: string) {
|
||||
return iconMap[iconName as keyof typeof iconMap];
|
||||
}
|
||||
|
||||
interface DropdownItem {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
trigger?: string;
|
||||
triggerIcon?: string;
|
||||
disabled?: boolean;
|
||||
items?: DropdownItem[];
|
||||
customContent?: ReactNode;
|
||||
align?: "left" | "right";
|
||||
}
|
||||
|
||||
export function Dropdown({
|
||||
trigger,
|
||||
triggerIcon,
|
||||
disabled = false,
|
||||
items = [],
|
||||
customContent,
|
||||
align = "left",
|
||||
}: DropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.dropdown, isOpen && styles.isActive)}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<button
|
||||
className={styles.dropdownTrigger}
|
||||
type="button"
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{triggerIcon && (() => {
|
||||
const IconComponent = getIcon(triggerIcon);
|
||||
return IconComponent ? <IconComponent size={16} /> : null;
|
||||
})()}
|
||||
{trigger && <span>{trigger}</span>}
|
||||
{!triggerIcon && !trigger && <MoreHorizontal size={16} />}
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.dropdownMenu,
|
||||
align === "right" && styles.alignRight,
|
||||
)}
|
||||
>
|
||||
<div className={styles.dropdownContent}>
|
||||
{customContent ? (
|
||||
<div className={styles.dropdownItem}>
|
||||
{customContent}
|
||||
</div>
|
||||
) : (
|
||||
items.map((item, index) => (
|
||||
<a
|
||||
key={index}
|
||||
href="#"
|
||||
className={styles.dropdownItem}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
item.onClick();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.text}
|
||||
</a>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
webui/src/components/ui/SidePanel.tsx
Normal file
57
webui/src/components/ui/SidePanel.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import styles from "@/styles/SidePanel.module.css";
|
||||
|
||||
interface SidePanelProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function SidePanel({ isOpen, onClose, children }: SidePanelProps) {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setIsVisible(true);
|
||||
// Small delay to trigger animation
|
||||
requestAnimationFrame(() => {
|
||||
setIsAnimating(true);
|
||||
});
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
// Wait for animation to complete before hiding
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(false);
|
||||
}, 300); // Match animation duration
|
||||
document.body.style.overflow = "";
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`${styles.overlay} ${isAnimating ? styles.overlayVisible : ""}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className={`${styles.sidePanel} ${isAnimating ? styles.sidePanelOpen : ""}`}>
|
||||
<button className={styles.closeButton} onClick={onClose}>
|
||||
×
|
||||
</button>
|
||||
<div className={styles.content}>{children}</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
49
webui/src/components/ui/Slider.tsx
Normal file
49
webui/src/components/ui/Slider.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { ChangeEvent } from "react";
|
||||
import styles from "@/styles/Slider.module.css";
|
||||
|
||||
interface SliderProps {
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
onChange: (value: number) => void;
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export function Slider({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
onChange,
|
||||
unit = "",
|
||||
}: SliderProps) {
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(Number(e.target.value));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderLabel}>
|
||||
<span>{label}</span>
|
||||
<span className={styles.sliderValue}>
|
||||
{value}
|
||||
{unit}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className={styles.slider}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
webui/src/lib/runner.ts
Normal file
106
webui/src/lib/runner.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
let worker: Worker | null = null;
|
||||
|
||||
export function sendDataToScript(data: String) {
|
||||
if (worker) {
|
||||
worker.postMessage({ type: "data", data });
|
||||
}
|
||||
}
|
||||
|
||||
export async function startScript(
|
||||
script: string,
|
||||
appendOutput: (line: string) => void,
|
||||
appendTerminal: (line: string) => void
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
}
|
||||
|
||||
worker = new Worker(new URL("./worker_human.ts", import.meta.url));
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
const { type, line, error } = event.data;
|
||||
|
||||
if (type === "output") {
|
||||
appendOutput(line);
|
||||
} else if (type === "terminal") {
|
||||
appendTerminal(line);
|
||||
} else if (type === "complete") {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
resolve();
|
||||
} else if (type === "error") {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
reject(new Error(error));
|
||||
} else if (type === "stopped") {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
reject(error);
|
||||
};
|
||||
|
||||
worker.postMessage({ type: "run", script });
|
||||
});
|
||||
}
|
||||
|
||||
export async function startScriptBulk(
|
||||
redScript: string,
|
||||
blueScript: string,
|
||||
opponentName: string,
|
||||
appendOutput: (line: string) => void,
|
||||
appendTerminal: (line: string) => void,
|
||||
rounds: number = 1000
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (worker) {
|
||||
worker.terminate();
|
||||
}
|
||||
|
||||
worker = new Worker(new URL("./worker_bulk.ts", import.meta.url));
|
||||
|
||||
worker.onmessage = (event) => {
|
||||
const { type, line, error } = event.data;
|
||||
|
||||
if (type === "output") {
|
||||
appendOutput(line);
|
||||
} else if (type === "terminal") {
|
||||
appendTerminal(line);
|
||||
} else if (type === "complete") {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
resolve();
|
||||
} else if (type === "error") {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
reject(new Error(error));
|
||||
} else if (type === "stopped") {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
worker?.terminate();
|
||||
worker = null;
|
||||
reject(error);
|
||||
};
|
||||
|
||||
worker.postMessage({ type: "run", redScript, blueScript, opponentName, rounds });
|
||||
});
|
||||
}
|
||||
|
||||
export function stopScript(): void {
|
||||
if (worker) {
|
||||
worker.postMessage({ type: "stop" });
|
||||
}
|
||||
}
|
||||
119
webui/src/lib/worker_bulk.ts
Normal file
119
webui/src/lib/worker_bulk.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import init, { MinMaxGame } from "../wasm/runner";
|
||||
|
||||
let wasmReady = false;
|
||||
let wasmInitPromise: Promise<void> | null = null;
|
||||
let currentGame: MinMaxGame | null = null;
|
||||
|
||||
async function initWasm(): Promise<void> {
|
||||
if (wasmReady) return;
|
||||
|
||||
if (wasmInitPromise) {
|
||||
return wasmInitPromise;
|
||||
}
|
||||
|
||||
wasmInitPromise = (async () => {
|
||||
await init();
|
||||
wasmReady = true;
|
||||
})();
|
||||
|
||||
return wasmInitPromise;
|
||||
}
|
||||
|
||||
self.onmessage = async (event) => {
|
||||
const { type, ...event_data } = event.data;
|
||||
|
||||
if (type === "init") {
|
||||
try {
|
||||
await initWasm();
|
||||
self.postMessage({ type: "ready" });
|
||||
} catch (error) {
|
||||
self.postMessage({ type: "error", error: String(error) });
|
||||
}
|
||||
} else if (type === "run") {
|
||||
try {
|
||||
await initWasm();
|
||||
|
||||
self.postMessage({
|
||||
type: "output",
|
||||
line: "Output is disabled during bulk runs.",
|
||||
});
|
||||
|
||||
const appendTerminal = (line: string) => {
|
||||
self.postMessage({ type: "terminal", line });
|
||||
};
|
||||
|
||||
const n_rounds = event_data.rounds || 1000;
|
||||
const start = performance.now();
|
||||
let red_wins = 0;
|
||||
let blue_wins = 0;
|
||||
let draw_score = 0;
|
||||
let draw_invalid = 0;
|
||||
|
||||
for (var i = 0; i < n_rounds; i++) {
|
||||
appendTerminal(`\n\r`);
|
||||
appendTerminal(`============\n\r`);
|
||||
appendTerminal(`= Round ${i + 1}\n\r`);
|
||||
appendTerminal(`============\n\n\r`);
|
||||
|
||||
currentGame = new MinMaxGame(
|
||||
event_data.redScript,
|
||||
() => {},
|
||||
() => {},
|
||||
|
||||
event_data.blueScript,
|
||||
() => {},
|
||||
() => {},
|
||||
|
||||
appendTerminal
|
||||
);
|
||||
|
||||
while (currentGame && !currentGame.is_done()) {
|
||||
currentGame.step();
|
||||
}
|
||||
|
||||
appendTerminal("\r\n");
|
||||
|
||||
if (currentGame.is_error()) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentGame.red_won() === true) {
|
||||
red_wins += 1;
|
||||
} else if (currentGame.blue_won() === true) {
|
||||
blue_wins += 1;
|
||||
} else if (currentGame.is_draw_invalid() === true) {
|
||||
draw_invalid += 1;
|
||||
} else if (currentGame.is_draw_score() === true) {
|
||||
draw_score += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = Math.round((performance.now() - start) / 100) / 10;
|
||||
const r_winrate = Math.round((red_wins / n_rounds) * 1000) / 10;
|
||||
const b_winrate = Math.round((blue_wins / n_rounds) * 1000) / 10;
|
||||
|
||||
const opponentName = event_data.opponentName || "Unknown";
|
||||
|
||||
appendTerminal("\r\n");
|
||||
appendTerminal(`Ran ${n_rounds} rounds in ${elapsed}s\r\n`);
|
||||
appendTerminal(
|
||||
`Red won: ${red_wins} (${r_winrate}%) (script)\r\n`
|
||||
);
|
||||
appendTerminal(
|
||||
`Blue won: ${blue_wins} (${b_winrate}%) (${opponentName})\r\n`
|
||||
);
|
||||
appendTerminal("\r\n");
|
||||
appendTerminal(`Draws: ${draw_score}\r\n`);
|
||||
appendTerminal(`Invalid: ${draw_invalid}\r\n`);
|
||||
|
||||
if (currentGame) {
|
||||
self.postMessage({ type: "complete" });
|
||||
}
|
||||
} catch (error) {
|
||||
self.postMessage({ type: "error", error: String(error) });
|
||||
}
|
||||
} else if (type === "stop") {
|
||||
currentGame = null;
|
||||
self.postMessage({ type: "stopped" });
|
||||
}
|
||||
};
|
||||
71
webui/src/lib/worker_human.ts
Normal file
71
webui/src/lib/worker_human.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import init, { GameState, GameStateHuman } from "../wasm/runner";
|
||||
|
||||
let wasmReady = false;
|
||||
let wasmInitPromise: Promise<void> | null = null;
|
||||
let currentGame: GameStateHuman | null = null;
|
||||
|
||||
async function initWasm(): Promise<void> {
|
||||
if (wasmReady) return;
|
||||
|
||||
if (wasmInitPromise) {
|
||||
return wasmInitPromise;
|
||||
}
|
||||
|
||||
wasmInitPromise = (async () => {
|
||||
await init();
|
||||
wasmReady = true;
|
||||
})();
|
||||
|
||||
return wasmInitPromise;
|
||||
}
|
||||
|
||||
self.onmessage = async (event) => {
|
||||
const { type, ...event_data } = event.data;
|
||||
|
||||
if (type === "data") {
|
||||
if (currentGame !== null) {
|
||||
currentGame.take_input(event_data.data);
|
||||
|
||||
if (currentGame.is_error()) {
|
||||
currentGame = null;
|
||||
self.postMessage({ type: "complete" });
|
||||
} else if (currentGame.is_done()) {
|
||||
currentGame = null;
|
||||
self.postMessage({ type: "complete" });
|
||||
}
|
||||
}
|
||||
} else if (type === "init") {
|
||||
try {
|
||||
await initWasm();
|
||||
self.postMessage({ type: "ready" });
|
||||
} catch (error) {
|
||||
self.postMessage({ type: "error", error: String(error) });
|
||||
}
|
||||
} else if (type === "run") {
|
||||
try {
|
||||
await initWasm();
|
||||
|
||||
const appendOutput = (line: string) => {
|
||||
self.postMessage({ type: "output", line });
|
||||
};
|
||||
|
||||
const appendTerminal = (line: string) => {
|
||||
self.postMessage({ type: "terminal", line });
|
||||
};
|
||||
|
||||
currentGame = new GameStateHuman(
|
||||
event_data.script,
|
||||
appendOutput,
|
||||
appendOutput,
|
||||
appendTerminal
|
||||
);
|
||||
|
||||
currentGame.print_start();
|
||||
} catch (error) {
|
||||
self.postMessage({ type: "error", error: String(error) });
|
||||
}
|
||||
} else if (type === "stop") {
|
||||
currentGame = null;
|
||||
self.postMessage({ type: "stopped" });
|
||||
}
|
||||
};
|
||||
143
webui/src/styles/AgentSelector.module.css
Normal file
143
webui/src/styles/AgentSelector.module.css
Normal file
@@ -0,0 +1,143 @@
|
||||
.agentSelector {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
width: 100%;
|
||||
padding: 6px 8px;
|
||||
background: #3c3c3c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.trigger:hover {
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.trigger:focus,
|
||||
.triggerOpen {
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 0 3px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.triggerText {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.chevronOpen {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
background: #2d2d30;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
margin-top: 2px;
|
||||
max-height: 300px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #555;
|
||||
}
|
||||
|
||||
.searchIcon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
padding: 6px 8px 6px 32px;
|
||||
background: #3c3c3c;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.searchInput:focus {
|
||||
border-color: #007acc;
|
||||
box-shadow: 0 0 3px rgba(0, 122, 204, 0.3);
|
||||
}
|
||||
|
||||
.searchInput::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.agentList {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agentOption {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #e0e0e0;
|
||||
transition: background-color 0.15s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.agentOption:hover,
|
||||
.highlighted {
|
||||
background: #404040;
|
||||
}
|
||||
|
||||
.agentOption:active {
|
||||
background: #4a4a4a;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background: #007acc;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.selected:hover,
|
||||
.selected.highlighted {
|
||||
background: #0088dd;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
padding: 12px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
font-size: 13px;
|
||||
}
|
||||
131
webui/src/styles/Button.module.css
Normal file
131
webui/src/styles/Button.module.css
Normal file
@@ -0,0 +1,131 @@
|
||||
.button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
background: #3a3a3a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.button:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.isLoading {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Button types */
|
||||
.is-primary {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.is-primary:hover:not(:disabled) {
|
||||
background: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
.is-success {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border-color: #28a745;
|
||||
}
|
||||
|
||||
.is-success:hover:not(:disabled) {
|
||||
background: #1e7e34;
|
||||
border-color: #1e7e34;
|
||||
}
|
||||
|
||||
.is-danger {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
.is-danger:hover:not(:disabled) {
|
||||
background: #c82333;
|
||||
border-color: #c82333;
|
||||
}
|
||||
|
||||
.is-warning {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.is-warning:hover:not(:disabled) {
|
||||
background: #e0a800;
|
||||
border-color: #e0a800;
|
||||
}
|
||||
|
||||
.is-info {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
border-color: #17a2b8;
|
||||
}
|
||||
|
||||
.is-info:hover:not(:disabled) {
|
||||
background: #138496;
|
||||
border-color: #138496;
|
||||
}
|
||||
|
||||
.is-light {
|
||||
background: #f8f9fa;
|
||||
color: #212529;
|
||||
border-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.is-light:hover:not(:disabled) {
|
||||
background: #e2e6ea;
|
||||
border-color: #e2e6ea;
|
||||
}
|
||||
|
||||
.is-dark {
|
||||
background: #343a40;
|
||||
color: white;
|
||||
border-color: #343a40;
|
||||
}
|
||||
|
||||
.is-dark:hover:not(:disabled) {
|
||||
background: #23272b;
|
||||
border-color: #23272b;
|
||||
}
|
||||
100
webui/src/styles/Dropdown.module.css
Normal file
100
webui/src/styles/Dropdown.module.css
Normal file
@@ -0,0 +1,100 @@
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdownTrigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.dropdownTrigger:hover:not(:disabled) {
|
||||
background: #3a3a3a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.dropdownTrigger:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.isActive .dropdownTrigger {
|
||||
background: #3a3a3a;
|
||||
border-color: #4fc3f7;
|
||||
box-shadow: 0 0 0 2px rgba(79, 195, 247, 0.25);
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
min-width: 160px;
|
||||
max-width: 90vw;
|
||||
margin-top: 4px;
|
||||
background: #2d2d2d;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||
animation: dropdownFadeIn 0.15s ease-out;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.dropdownMenu.alignRight {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@keyframes dropdownFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownContent {
|
||||
padding: 4px 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdownItem {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: #e0e0e0;
|
||||
text-align: inherit;
|
||||
text-decoration: none;
|
||||
white-space: normal;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.dropdownItem:hover,
|
||||
.dropdownItem:focus {
|
||||
background-color: #3a3a3a;
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
38
webui/src/styles/Editor.module.css
Normal file
38
webui/src/styles/Editor.module.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.editorContainer {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.editorContainer textarea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
padding: 8px;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Enhanced CodeMirror styles from playground */
|
||||
.editorContainer :global(.CodeMirror) {
|
||||
border: none;
|
||||
height: 100% !important;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.95em;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
.editorContainer :global(.rhai-error) {
|
||||
text-decoration: underline wavy red;
|
||||
}
|
||||
|
||||
.editorContainer :global(.cm-matchhighlight) {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.editorContainer :global(.CodeMirror-selection-highlight-scrollbar) {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
157
webui/src/styles/Playground.module.css
Normal file
157
webui/src/styles/Playground.module.css
Normal file
@@ -0,0 +1,157 @@
|
||||
.playgroundRoot {
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid #333;
|
||||
background: #252526;
|
||||
}
|
||||
|
||||
.headerField {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.buttonGroup {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mainContent {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.leftPanel {
|
||||
width: 50%;
|
||||
border-right: 1px solid #333;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.rightPanel {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.terminalPanel {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.outputPanel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
background: #2d2d30;
|
||||
border-bottom: 1px solid #333;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #cccccc;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.terminalContainer {
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
background: #1D1F21;
|
||||
}
|
||||
|
||||
.result {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
resize: none;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.configPanel {
|
||||
padding: 16px;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.configField {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.configField label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #cccccc;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
|
||||
.helpPanel {
|
||||
padding: 16px;
|
||||
width: 300px;
|
||||
max-width: 90vw;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.helpPanel h1 {
|
||||
font-size: 18px;
|
||||
margin: 0 0 8px 0;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.helpPanel p {
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.5;
|
||||
color: #cccccc;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.helpPanel a {
|
||||
color: #4fc3f7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.helpPanel a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
border-top: 1px solid #444;
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
166
webui/src/styles/SidePanel.module.css
Normal file
166
webui/src/styles/SidePanel.module.css
Normal file
@@ -0,0 +1,166 @@
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0);
|
||||
z-index: 1000;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.overlayVisible {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.sidePanel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
background: #1e1e1e;
|
||||
z-index: 1001;
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidePanelOpen {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
font-size: 32px;
|
||||
cursor: pointer;
|
||||
padding: 4px 12px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
border-radius: 4px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
color: #ffffff;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px 32px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 28px;
|
||||
margin: 0 0 24px 0;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
font-size: 20px;
|
||||
margin: 32px 0 16px 0;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content h3 {
|
||||
font-size: 16px;
|
||||
margin: 24px 0 12px 0;
|
||||
color: #e0e0e0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.content p {
|
||||
line-height: 1.6;
|
||||
margin: 0 0 16px 0;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.content ul,
|
||||
.content ol {
|
||||
margin: 0 0 16px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.content li {
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8px;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.content code {
|
||||
background: #2d2d30;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
font-size: 13px;
|
||||
color: #4fc3f7;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
background: #2d2d30;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.content pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content a {
|
||||
color: #4fc3f7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content kbd {
|
||||
background: #2d2d30;
|
||||
border: 1px solid #444;
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
color: #e0e0e0;
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidePanel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
57
webui/src/styles/Slider.module.css
Normal file
57
webui/src/styles/Slider.module.css
Normal file
@@ -0,0 +1,57 @@
|
||||
.sliderContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sliderLabel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.sliderValue {
|
||||
font-weight: 600;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #3a3a3a;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #4fc3f7;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb:hover {
|
||||
background: #6dd1ff;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #4fc3f7;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb:hover {
|
||||
background: #6dd1ff;
|
||||
}
|
||||
119
webui/src/styles/globals.css
Normal file
119
webui/src/styles/globals.css
Normal file
@@ -0,0 +1,119 @@
|
||||
/* Global styles based on the original site */
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden !important;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
background: #1a1a1a;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* CodeMirror base styles */
|
||||
.CodeMirror {
|
||||
border: 1px solid #444;
|
||||
height: 100% !important;
|
||||
box-sizing: border-box;
|
||||
font-size: 0.95em;
|
||||
line-height: initial;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.CodeMirror .rhai-error {
|
||||
text-decoration: underline wavy red;
|
||||
}
|
||||
|
||||
.CodeMirror .cm-matchhighlight {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.CodeMirror .CodeMirror-selection-highlight-scrollbar {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Basic button styles */
|
||||
button {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #555;
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #3a3a3a;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Input styles */
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
padding: 8px;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
select {
|
||||
background: #2d2d2d;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||
}
|
||||
|
||||
/* Form elements */
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
/* Kbd styling */
|
||||
kbd {
|
||||
background: #333;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
color: #e0e0e0;
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
margin: 0 2px;
|
||||
padding: 1px 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
10
webui/src/types/codemirror.d.ts
vendored
Normal file
10
webui/src/types/codemirror.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// Type declarations for CodeMirror modules
|
||||
declare module "codemirror/mode/javascript/javascript";
|
||||
declare module "codemirror/addon/edit/matchbrackets";
|
||||
declare module "codemirror/addon/edit/closebrackets";
|
||||
declare module "codemirror/addon/selection/active-line";
|
||||
declare module "codemirror/addon/fold/foldcode";
|
||||
declare module "codemirror/addon/fold/foldgutter";
|
||||
declare module "codemirror/addon/fold/brace-fold";
|
||||
declare module "codemirror/lib/codemirror.css";
|
||||
declare module "codemirror/addon/fold/foldgutter.css";
|
||||
91
webui/src/utils/wasmLoader.ts
Normal file
91
webui/src/utils/wasmLoader.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// WASM loader for Rhai CodeMirror mode
|
||||
import init, {
|
||||
RhaiMode,
|
||||
init_codemirror_pass,
|
||||
} from "@/wasm/rhai-codemirror/rhai_codemirror.js";
|
||||
|
||||
let wasmInitialized = false;
|
||||
let wasmModule: any = null;
|
||||
let wasmLoadPromise: Promise<any> | null = null;
|
||||
|
||||
export const loadRhaiWasm = async () => {
|
||||
if (wasmInitialized) {
|
||||
return wasmModule;
|
||||
}
|
||||
if (wasmLoadPromise) {
|
||||
return wasmLoadPromise;
|
||||
}
|
||||
|
||||
wasmLoadPromise = (async () => {
|
||||
try {
|
||||
// Initialize the WASM module
|
||||
wasmModule = await init();
|
||||
wasmInitialized = true;
|
||||
|
||||
return wasmModule;
|
||||
} catch (error) {
|
||||
console.error("Failed to load Rhai WASM module:", error);
|
||||
wasmLoadPromise = null; // Reset on error
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
return wasmLoadPromise;
|
||||
};
|
||||
|
||||
export const initRhaiMode = (CodeMirror: any) => {
|
||||
if (!wasmInitialized || !wasmModule) {
|
||||
throw new Error("WASM module not loaded. Call loadRhaiWasm() first.");
|
||||
}
|
||||
|
||||
// Initialize CodeMirror Pass for the WASM module
|
||||
init_codemirror_pass(CodeMirror.Pass);
|
||||
|
||||
// Define the Rhai mode using the WASM-based RhaiMode
|
||||
CodeMirror.defineMode("rhai", (config: any) => {
|
||||
return new RhaiMode(config.indentUnit || 4);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Function to preload all WASM modules used by the application
|
||||
export const loadAllWasm = async (): Promise<void> => {
|
||||
try {
|
||||
|
||||
await loadRhaiWasm();
|
||||
|
||||
// Load Script Runner WASM by creating and immediately terminating a worker
|
||||
const worker = new Worker(
|
||||
new URL("../lib/worker_bulk.ts", import.meta.url)
|
||||
);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
worker.terminate();
|
||||
reject(new Error("Script runner WASM load timeout"));
|
||||
}, 10000);
|
||||
|
||||
worker.postMessage({ type: "init" });
|
||||
worker.onmessage = (event) => {
|
||||
if (event.data.type === "ready") {
|
||||
clearTimeout(timeout);
|
||||
worker.terminate();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
worker.onerror = (error) => {
|
||||
clearTimeout(timeout);
|
||||
worker.terminate();
|
||||
reject(error);
|
||||
};
|
||||
});
|
||||
|
||||
console.log("All WASM modules loaded successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to load WASM modules:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { RhaiMode };
|
||||
33
webui/tsconfig.json
Normal file
33
webui/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "es6"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"src/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user