Compare commits

...

5 Commits

Author SHA1 Message Date
684ae0ecf8 Agent library 2025-11-03 16:46:33 -08:00
0db5b7a8f1 Select bulk opponent 2025-11-03 16:42:03 -08:00
bfbd9d35bc Persist code 2025-11-03 16:42:03 -08:00
07aeda5e07 Add initial webui 2025-11-03 16:42:03 -08:00
19f523d0ed Add Rust: minimax, runner, and codelens highlighter 2025-11-03 16:41:58 -08:00
56 changed files with 7170 additions and 0 deletions

169
agents/greed.rhai Normal file
View 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
View 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
View 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
View 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
View 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
View 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 }

View 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>;
}

View 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());
}
}
}

View 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);
}
}

View 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(());
},
);
}
}

View 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;

View 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,
}
}
}

View 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
View File

@@ -0,0 +1,2 @@
pub mod agents;
pub mod game;

View 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"] }

View 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>;
}

View 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(())
}

View 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
View 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
View 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
View 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";

View 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);
}
}

View 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
View 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();
}

View 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
View File

@@ -0,0 +1 @@
hard_tabs = true

7
webui/.eslintrc.json Normal file
View 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
View File

@@ -0,0 +1,4 @@
{
"useTabs": true,
"tabWidth": 4
}

719
webui/bun.lock Normal file
View 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
View 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
View 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
View 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
View 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
View 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>
);
}

View 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>
);
});

View 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>
);
}

View 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;
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
</>
);
}

View 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
View 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" });
}
}

View 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" });
}
};

View 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" });
}
};

View 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;
}

View 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;
}

View 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;
}

View 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);
}

View 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;
}

View 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%;
}
}

View 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;
}

View 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
View 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";

View 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
View 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"]
}