Compare commits
9 Commits
85ce50293c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 48a45b5447 | |||
| 5bd6331ad9 | |||
| eb084e1f07 | |||
| 30649488bb | |||
| 684ae0ecf8 | |||
| 0db5b7a8f1 | |||
| bfbd9d35bc | |||
| 07aeda5e07 | |||
| 19f523d0ed |
8
.env_dist
Normal file
8
.env_dist
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Script saving configuration
|
||||||
|
ENABLE_SAVE=true
|
||||||
|
SAVE_SECRET=save
|
||||||
|
SAVE_DIRECTORY=./data/scripts
|
||||||
|
MAX_FILENAME_LENGTH=32
|
||||||
|
|
||||||
|
# Next.js environment
|
||||||
|
NODE_ENV=production
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -4,3 +4,5 @@ node_modules
|
|||||||
target
|
target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
webui/src/wasm
|
webui/src/wasm
|
||||||
|
webui/data
|
||||||
|
.env
|
||||||
|
|||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM rust:1.91 AS rust-builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
RUN cargo install wasm-pack
|
||||||
|
|
||||||
|
COPY rust/ ./rust/
|
||||||
|
WORKDIR /app/rust/rhai-codemirror
|
||||||
|
RUN wasm-pack build --target web --out-dir "../../webui/src/wasm/rhai-codemirror"
|
||||||
|
|
||||||
|
WORKDIR /app/rust/runner
|
||||||
|
RUN wasm-pack build --target web --out-dir "../../webui/src/wasm/runner"
|
||||||
|
|
||||||
|
FROM node:24-alpine AS app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN npm install -g bun
|
||||||
|
COPY webui/package.json webui/bun.lock* ./webui/
|
||||||
|
|
||||||
|
WORKDIR /app/webui
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
COPY webui/ ./
|
||||||
|
COPY --from=rust-builder /app/webui/src/wasm/ ./src/wasm/
|
||||||
|
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
|
RUN mkdir -p ../data/scripts
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["bun", "start"]
|
||||||
161
agents/greed-v1.rhai
Normal file
161
agents/greed-v1.rhai
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
// 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);
|
||||||
|
|
||||||
|
// We could not place an op, so place a number
|
||||||
|
if action == () {
|
||||||
|
action = place_number(board, minimize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent invalid moves, random fallback
|
||||||
|
if board.can_play(action) { return action; }
|
||||||
|
return random_action(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Minimizer step
|
||||||
|
fn step_min(board) {
|
||||||
|
greed_step(board, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximizer step
|
||||||
|
fn step_max(board) {
|
||||||
|
greed_step(board, false)
|
||||||
|
}
|
||||||
254
agents/greed-v2.rhai
Normal file
254
agents/greed-v2.rhai
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
// SECRET
|
||||||
|
|
||||||
|
// 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 op_value(board) {
|
||||||
|
print(board);
|
||||||
|
let actions = [];
|
||||||
|
for o in ["+", "-", "*", "/"] {
|
||||||
|
if board.contains(o) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in 0..=10 {
|
||||||
|
let action = Action(o, p);
|
||||||
|
if board.can_play(action) {
|
||||||
|
actions.push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No other operators can be placed, return value of fives
|
||||||
|
if actions.is_empty() {
|
||||||
|
let filled = board;
|
||||||
|
for i in filled.free_spots_idx() {
|
||||||
|
filled[i] = 5;
|
||||||
|
}
|
||||||
|
let v = filled.evaluate();
|
||||||
|
if v == () {
|
||||||
|
return ();
|
||||||
|
} else {
|
||||||
|
return [v, v];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let max = ();
|
||||||
|
let min = ();
|
||||||
|
for a in actions {
|
||||||
|
let tmp = board;
|
||||||
|
tmp.play(a);
|
||||||
|
|
||||||
|
let vals = op_value(tmp);
|
||||||
|
if vals != () {
|
||||||
|
for v in vals {
|
||||||
|
if max == () || min == () {
|
||||||
|
max = v;
|
||||||
|
min = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if v > max {
|
||||||
|
max = v;
|
||||||
|
} else if v < min {
|
||||||
|
min = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if min == () || max == () {
|
||||||
|
return ();
|
||||||
|
}
|
||||||
|
|
||||||
|
return [min, max];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn place_op(board, minimize) {
|
||||||
|
let ops = ["+", "-", "*", "/"];
|
||||||
|
let available_ops = ops.retain(|x| board.contains(x));
|
||||||
|
|
||||||
|
// Performance optimization if board is empty.
|
||||||
|
// This is the move we would pick, hard-coded.
|
||||||
|
if available_ops.len() == 4 {
|
||||||
|
if minimize {
|
||||||
|
let act = Action("+", 3);
|
||||||
|
if board.can_play(act) {return act}
|
||||||
|
} else {
|
||||||
|
let act = Action("/", 9);
|
||||||
|
if board.can_play(act) {return act}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All possible operator actions
|
||||||
|
let actions = [];
|
||||||
|
for o in ["+", "-", "*", "/"] {
|
||||||
|
for p in 0..=10 {
|
||||||
|
let action = Action(o, p);
|
||||||
|
if board.can_play(action) {
|
||||||
|
let tmp = board;
|
||||||
|
tmp.play(action);
|
||||||
|
let v = op_value(tmp);
|
||||||
|
if v != () {
|
||||||
|
actions.push([action, v]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if actions.is_empty() {
|
||||||
|
return ();
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = ();
|
||||||
|
if minimize {
|
||||||
|
// Sort by increasing minimum score
|
||||||
|
actions.sort(|a, b| sign(a[1][0] - b[1][0]));
|
||||||
|
action = actions[0][0];
|
||||||
|
} else {
|
||||||
|
// Sort by increasing maximum score
|
||||||
|
actions.sort(|a, b| sign(a[1][1] - b[1][1]));
|
||||||
|
action = actions[-1][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(action);
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Main step function (shared between min and max)
|
||||||
|
fn greed_step(board, minimize) {
|
||||||
|
let action = place_op(board, minimize);
|
||||||
|
if action == () {
|
||||||
|
action = place_number(board, minimize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if board.can_play(action) {
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent invalid moves, random fallback
|
||||||
|
return random_action(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Minimizer step
|
||||||
|
fn step_min(board) {
|
||||||
|
greed_step(board, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maximizer step
|
||||||
|
fn step_max(board) {
|
||||||
|
greed_step(board, false)
|
||||||
|
}
|
||||||
21
agents/random.rhai
Normal file
21
agents/random.rhai
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
fn random_action(board) {
|
||||||
|
let symb = rand_symb();
|
||||||
|
let pos = rand_int(0, 10);
|
||||||
|
let action = Action(symb, pos);
|
||||||
|
|
||||||
|
while !board.can_play(action) {
|
||||||
|
let symb = rand_symb();
|
||||||
|
let pos = rand_int(0, 10);
|
||||||
|
action = Action(symb, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_min(board) {
|
||||||
|
random_action(board)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_max(board) {
|
||||||
|
random_action(board)
|
||||||
|
}
|
||||||
12
build.sh
Normal file
12
build.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
set -e
|
||||||
|
|
||||||
|
cd rust/rhai-codemirror
|
||||||
|
wasm-pack build --target web --out-dir "../../webui/src/wasm/rhai-codemirror";
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
cd rust/runner
|
||||||
|
wasm-pack build --target web --out-dir "../../webui/src/wasm/runner";
|
||||||
|
cd ../..
|
||||||
|
|
||||||
|
cd webui
|
||||||
|
bun install
|
||||||
10
docker-compose.yml
Normal file
10
docker-compose.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
services:
|
||||||
|
webui:
|
||||||
|
image: minimax
|
||||||
|
ports:
|
||||||
|
- "4000:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
restart: unless-stopped
|
||||||
615
rust/Cargo.lock
generated
Normal file
615
rust/Cargo.lock
generated
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ahash"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"const-random",
|
||||||
|
"once_cell",
|
||||||
|
"version_check",
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.100"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.19.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console_error_panic_hook"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crunchy"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "either"
|
||||||
|
version = "1.15.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instant"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memory_units"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimax"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"itertools",
|
||||||
|
"parking_lot",
|
||||||
|
"rand",
|
||||||
|
"rhai",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.41"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhai"
|
||||||
|
version = "1.23.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "527390cc333a8d2cd8237890e15c36518c26f8b54c903d86fc59f42f08d25594"
|
||||||
|
dependencies = [
|
||||||
|
"ahash",
|
||||||
|
"bitflags",
|
||||||
|
"getrandom",
|
||||||
|
"instant",
|
||||||
|
"num-traits",
|
||||||
|
"once_cell",
|
||||||
|
"rhai_codegen",
|
||||||
|
"smallvec",
|
||||||
|
"smartstring",
|
||||||
|
"thin-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhai-codemirror"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"js-sys",
|
||||||
|
"rhai",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
"wee_alloc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhai_codegen"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4322a2a4e8cf30771dd9f27f7f37ca9ac8fe812dddd811096a98483080dabe6"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "runner"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"console_error_panic_hook",
|
||||||
|
"getrandom",
|
||||||
|
"itertools",
|
||||||
|
"js-sys",
|
||||||
|
"minimax",
|
||||||
|
"rand",
|
||||||
|
"rhai",
|
||||||
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"web-sys",
|
||||||
|
"wee_alloc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-wasm-bindgen"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3b4c031cd0d9014307d82b8abf653c0290fbdaeb4c02d00c63cf52f728628bf"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.145"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smartstring"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"static_assertions",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.108"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thin-vec"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-keccak"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.105"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wee_alloc"
|
||||||
|
version = "0.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 0.1.10",
|
||||||
|
"libc",
|
||||||
|
"memory_units",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi"
|
||||||
|
version = "0.3.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-i686-pc-windows-gnu",
|
||||||
|
"winapi-x86_64-pc-windows-gnu",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-i686-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
41
rust/Cargo.toml
Normal file
41
rust/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
opt-level = 's'
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["runner", "minimax", "rhai-codemirror"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.dependencies]
|
||||||
|
minimax = { path = "./minimax" }
|
||||||
|
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
rand = { version = "0.8.5", features = ["alloc", "small_rng"] }
|
||||||
|
anyhow = "1.0.80"
|
||||||
|
itertools = "0.12.1"
|
||||||
|
rhai = { version = "1.23.4", default-features = false, features = [
|
||||||
|
"no_time",
|
||||||
|
"no_module",
|
||||||
|
"no_custom_syntax",
|
||||||
|
"only_i64",
|
||||||
|
"f32_float",
|
||||||
|
] }
|
||||||
|
|
||||||
|
|
||||||
|
# js ffi
|
||||||
|
getrandom = "0.2"
|
||||||
|
wasm-bindgen = { version = "0.2", features = ["serde-serialize"] }
|
||||||
|
js-sys = "0.3"
|
||||||
|
web-sys = { version = "0.3", features = [
|
||||||
|
"console",
|
||||||
|
"Document",
|
||||||
|
"Element",
|
||||||
|
"HtmlElement",
|
||||||
|
"Window",
|
||||||
|
"Performance",
|
||||||
|
] }
|
||||||
|
console_error_panic_hook = "0.1"
|
||||||
|
wee_alloc = "0.4"
|
||||||
|
serde-wasm-bindgen = "0.4"
|
||||||
|
parking_lot = "0.12.5"
|
||||||
17
rust/minimax/Cargo.toml
Normal file
17
rust/minimax/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
[package]
|
||||||
|
name = "minimax"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
|
||||||
|
rhai = { workspace = true }
|
||||||
|
parking_lot = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
rhai = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
web-sys = { workspace = true }
|
||||||
|
wasm-bindgen = { workspace = true }
|
||||||
18
rust/minimax/src/agents/mod.rs
Normal file
18
rust/minimax/src/agents/mod.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
mod rhai;
|
||||||
|
|
||||||
|
pub use rhai::RhaiAgent;
|
||||||
|
|
||||||
|
use crate::game::{Board, PlayerAction};
|
||||||
|
|
||||||
|
pub trait Agent {
|
||||||
|
type ErrorType;
|
||||||
|
|
||||||
|
/// This agent's name
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
/// Try to minimize the value of a board.
|
||||||
|
fn step_min(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType>;
|
||||||
|
|
||||||
|
/// Try to maximize the value of a board.
|
||||||
|
fn step_max(&mut self, board: &Board) -> Result<PlayerAction, Self::ErrorType>;
|
||||||
|
}
|
||||||
304
rust/minimax/src/agents/rhai.rs
Normal file
304
rust/minimax/src/agents/rhai.rs
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
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,
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
rng,
|
||||||
|
engine,
|
||||||
|
script,
|
||||||
|
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 Scope::new(),
|
||||||
|
&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 Scope::new(),
|
||||||
|
&self.script,
|
||||||
|
"step_max",
|
||||||
|
(board.clone(),),
|
||||||
|
);
|
||||||
|
|
||||||
|
match res {
|
||||||
|
Ok(x) => Ok(x),
|
||||||
|
Err(err) => Err(*err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: rhai helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
mod helpers {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use itertools::Permutations;
|
||||||
|
use rhai::{CustomType, TypeBuilder};
|
||||||
|
|
||||||
|
/// A Rhai iterator that produces all permutations of length `n`
|
||||||
|
/// of the elements in an array
|
||||||
|
pub struct RhaiPer<T: Clone, I: Iterator<Item = T>> {
|
||||||
|
inner: Arc<Permutations<I>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone, I: Clone + Iterator<Item = T>> RhaiPer<T, I> {
|
||||||
|
pub fn new(inner: Permutations<I>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(inner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone, I: Clone + Iterator<Item = T>> IntoIterator for RhaiPer<T, I> {
|
||||||
|
type Item = Vec<T>;
|
||||||
|
type IntoIter = Permutations<I>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
(*self.inner).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone, I: Iterator<Item = T>> Clone for RhaiPer<T, I> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: self.inner.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone + 'static, I: Clone + Iterator<Item = T> + 'static> CustomType for RhaiPer<T, I> {
|
||||||
|
fn build(mut builder: TypeBuilder<Self>) {
|
||||||
|
builder
|
||||||
|
.with_name("Permutations")
|
||||||
|
.is_iterable()
|
||||||
|
.with_fn("to_string", |_s: &mut Self| "Permutation {}".to_owned())
|
||||||
|
.with_fn("to_debug", |_s: &mut Self| "Permutation {}".to_owned());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
rust/minimax/src/game/action.rs
Normal file
70
rust/minimax/src/game/action.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use super::{Board, Symb};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct PlayerAction {
|
||||||
|
pub symb: Symb,
|
||||||
|
pub pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PlayerAction {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{} at {}", self.symb, self.pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerAction {
|
||||||
|
pub fn new_random<R: Rng>(rng: &mut R, board: &Board) -> Self {
|
||||||
|
let n = board.size();
|
||||||
|
let pos = rng.gen_range(0..n);
|
||||||
|
let symb = Symb::new_random(rng);
|
||||||
|
PlayerAction { symb, pos }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomType for PlayerAction {
|
||||||
|
fn build(mut builder: TypeBuilder<Self>) {
|
||||||
|
builder
|
||||||
|
.with_name("Action")
|
||||||
|
.with_fn(
|
||||||
|
"Action",
|
||||||
|
|symb: &str, pos: i64| -> Result<Self, Box<EvalAltResult>> {
|
||||||
|
let symb = match Symb::from_str(symb) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Err(format!("Invalid symbol {symb:?}").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
symb,
|
||||||
|
pos: pos as usize,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_fn(
|
||||||
|
"Action",
|
||||||
|
|symb: i64, pos: i64| -> Result<Self, Box<EvalAltResult>> {
|
||||||
|
let symb = symb.to_string();
|
||||||
|
let symb = match Symb::from_str(&symb) {
|
||||||
|
Some(x) => x,
|
||||||
|
None => return Err(format!("Invalid symbol {symb:?}").into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
symb,
|
||||||
|
pos: pos as usize,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_fn("to_string", |s: &mut Self| -> String {
|
||||||
|
format!("Action {{{} at {}}}", s.symb, s.pos)
|
||||||
|
})
|
||||||
|
.with_fn("to_debug", |s: &mut Self| -> String {
|
||||||
|
format!("Action {{{} at {}}}", s.symb, s.pos)
|
||||||
|
})
|
||||||
|
.with_get("symb", |s: &mut Self| s.symb.to_string())
|
||||||
|
.with_get("pos", |s: &mut Self| s.pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
632
rust/minimax/src/game/board.rs
Normal file
632
rust/minimax/src/game/board.rs
Normal file
@@ -0,0 +1,632 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use rhai::Array;
|
||||||
|
use rhai::CustomType;
|
||||||
|
use rhai::Dynamic;
|
||||||
|
use rhai::EvalAltResult;
|
||||||
|
use rhai::Position;
|
||||||
|
use rhai::TypeBuilder;
|
||||||
|
use std::fmt::{Debug, Display, Write};
|
||||||
|
|
||||||
|
use super::{PlayerAction, Symb, TreeElement};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum InterTreeElement {
|
||||||
|
Unprocessed(Token),
|
||||||
|
Processed(TreeElement),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InterTreeElement {
|
||||||
|
fn to_value(&self) -> Option<TreeElement> {
|
||||||
|
Some(match self {
|
||||||
|
InterTreeElement::Processed(x) => x.clone(),
|
||||||
|
InterTreeElement::Unprocessed(Token::Value(s)) => {
|
||||||
|
if let Some(s) = s.strip_prefix('-') {
|
||||||
|
TreeElement::Neg {
|
||||||
|
r: {
|
||||||
|
if s.contains('_') {
|
||||||
|
Box::new(TreeElement::Partial(s.to_string()))
|
||||||
|
} else {
|
||||||
|
Box::new(TreeElement::Number(match s.parse() {
|
||||||
|
Ok(x) => x,
|
||||||
|
_ => return None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if s.contains('_') {
|
||||||
|
TreeElement::Partial(s.to_string())
|
||||||
|
} else {
|
||||||
|
TreeElement::Number(match s.parse() {
|
||||||
|
Ok(x) => x,
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
enum Token {
|
||||||
|
Value(String),
|
||||||
|
OpAdd,
|
||||||
|
OpSub,
|
||||||
|
OpMult,
|
||||||
|
OpDiv,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Board {
|
||||||
|
board: [Option<Symb>; 11],
|
||||||
|
placed_by: [Option<String>; 11],
|
||||||
|
|
||||||
|
/// Number of Nones in `board`
|
||||||
|
free_spots: usize,
|
||||||
|
|
||||||
|
/// Index of the last board index that was changed
|
||||||
|
last_placed: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Board {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for c in self.board {
|
||||||
|
write!(f, "{}", c.map(|s| s.get_char().unwrap()).unwrap_or('_'))?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for Board {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl Board {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
free_spots: 11,
|
||||||
|
board: Default::default(),
|
||||||
|
placed_by: Default::default(),
|
||||||
|
last_placed: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_board(&self) -> &[Option<Symb>; 11] {
|
||||||
|
&self.board
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_board_mut(&mut self) -> &mut [Option<Symb>; 11] {
|
||||||
|
&mut self.board
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the index of the ith empty slot
|
||||||
|
pub fn ith_empty_slot(&self, mut idx: usize) -> Option<usize> {
|
||||||
|
for (i, c) in self.board.iter().enumerate() {
|
||||||
|
if c.is_none() {
|
||||||
|
if idx == 0 {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
idx -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if idx == 0 {
|
||||||
|
Some(self.board.len() - 1)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_full(&self) -> bool {
|
||||||
|
self.free_spots == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prettyprint(&self) -> String {
|
||||||
|
const RESET: &str = "\x1b[0m";
|
||||||
|
const MAGENTA: &str = "\x1b[35m";
|
||||||
|
|
||||||
|
let mut s = String::new();
|
||||||
|
// Print board
|
||||||
|
for (i, (symb, _)) in self.board.iter().zip(self.placed_by.iter()).enumerate() {
|
||||||
|
match symb {
|
||||||
|
Some(symb) => write!(
|
||||||
|
s,
|
||||||
|
"{}{}{RESET}",
|
||||||
|
// If index matches last placed, draw symbol in red.
|
||||||
|
// If last_placed is None, this check will always fail
|
||||||
|
// since self.board.len is always greater than i.
|
||||||
|
if self.last_placed.unwrap_or(self.board.len()) == i {
|
||||||
|
MAGENTA
|
||||||
|
} else {
|
||||||
|
RESET
|
||||||
|
},
|
||||||
|
symb,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
|
||||||
|
None => write!(s, "_").unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> usize {
|
||||||
|
self.board.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_last_placed(&self) -> Option<usize> {
|
||||||
|
self.last_placed
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, s: Symb) -> bool {
|
||||||
|
self.board.iter().contains(&Some(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is the given action valid?
|
||||||
|
pub fn can_play(&self, action: &PlayerAction) -> bool {
|
||||||
|
match &self.board[action.pos] {
|
||||||
|
Some(_) => return false,
|
||||||
|
None => {
|
||||||
|
// Check for duplicate symbols
|
||||||
|
if self.contains(action.symb) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check syntax
|
||||||
|
match action.symb {
|
||||||
|
Symb::Minus => {
|
||||||
|
if action.pos == self.board.len() - 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = &self.board[action.pos + 1];
|
||||||
|
if r.is_some_and(|s| s.is_op() && !s.is_minus()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Symb::Zero => {
|
||||||
|
if action.pos != 0 {
|
||||||
|
let l = &self.board[action.pos - 1];
|
||||||
|
if l == &Some(Symb::Div) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Symb::Div | Symb::Plus | Symb::Times => {
|
||||||
|
if action.pos == 0 || action.pos == self.board.len() - 1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = &self.board[action.pos - 1];
|
||||||
|
let r = &self.board[action.pos + 1];
|
||||||
|
|
||||||
|
if action.symb == Symb::Div && r == &Some(Symb::Zero) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.is_some_and(|s| s.is_op())
|
||||||
|
|| r.is_some_and(|s| s.is_op() && !s.is_minus())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Place the marked symbol at the given position.
|
||||||
|
/// Returns true for valid moves and false otherwise.
|
||||||
|
pub fn play(&mut self, action: PlayerAction, player: impl Into<String>) -> bool {
|
||||||
|
if !self.can_play(&action) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.board[action.pos] = Some(action.symb);
|
||||||
|
self.placed_by[action.pos] = Some(player.into());
|
||||||
|
self.free_spots -= 1;
|
||||||
|
self.last_placed = Some(action.pos);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize(&self) -> Vec<Token> {
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut is_neg = true; // if true, - is negative. if false, subtract.
|
||||||
|
let mut current_num = String::new();
|
||||||
|
|
||||||
|
for s in self.board.iter() {
|
||||||
|
match s {
|
||||||
|
Some(Symb::Div) => {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpDiv);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
Some(Symb::Minus) => {
|
||||||
|
if is_neg {
|
||||||
|
current_num = format!("-{}", current_num);
|
||||||
|
} else {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpSub);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Symb::Plus) => {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpAdd);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
Some(Symb::Times) => {
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
current_num.clear();
|
||||||
|
}
|
||||||
|
tokens.push(Token::OpMult);
|
||||||
|
is_neg = true;
|
||||||
|
}
|
||||||
|
Some(Symb::Zero) => {
|
||||||
|
current_num.push('0');
|
||||||
|
is_neg = false;
|
||||||
|
}
|
||||||
|
Some(Symb::Number(x)) => {
|
||||||
|
current_num.push_str(&x.to_string());
|
||||||
|
is_neg = false;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
current_num.push('_');
|
||||||
|
is_neg = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current_num.is_empty() {
|
||||||
|
tokens.push(Token::Value(current_num.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_tree(&self) -> Option<TreeElement> {
|
||||||
|
let tokens = self.tokenize();
|
||||||
|
|
||||||
|
let mut tree: Vec<_> = tokens
|
||||||
|
.iter()
|
||||||
|
.map(|x| InterTreeElement::Unprocessed(x.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut priority_level = 0;
|
||||||
|
let mut did_something;
|
||||||
|
while tree.len() > 1 {
|
||||||
|
did_something = false;
|
||||||
|
for i in 0..tree.len() {
|
||||||
|
if match priority_level {
|
||||||
|
0 => matches!(
|
||||||
|
tree[i],
|
||||||
|
InterTreeElement::Unprocessed(Token::OpMult)
|
||||||
|
| InterTreeElement::Unprocessed(Token::OpDiv)
|
||||||
|
),
|
||||||
|
1 => matches!(
|
||||||
|
tree[i],
|
||||||
|
InterTreeElement::Unprocessed(Token::OpAdd)
|
||||||
|
| InterTreeElement::Unprocessed(Token::OpSub)
|
||||||
|
),
|
||||||
|
_ => false,
|
||||||
|
} {
|
||||||
|
did_something = true;
|
||||||
|
|
||||||
|
if i == 0 || i + 1 >= tree.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let l = tree[i - 1].to_value()?;
|
||||||
|
let r = tree[i + 1].to_value()?;
|
||||||
|
|
||||||
|
let v = match tree[i] {
|
||||||
|
InterTreeElement::Unprocessed(Token::OpAdd) => TreeElement::Add {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
InterTreeElement::Unprocessed(Token::OpDiv) => TreeElement::Div {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
InterTreeElement::Unprocessed(Token::OpMult) => TreeElement::Mul {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
InterTreeElement::Unprocessed(Token::OpSub) => TreeElement::Sub {
|
||||||
|
l: Box::new(l),
|
||||||
|
r: Box::new(r),
|
||||||
|
},
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
tree.remove(i - 1);
|
||||||
|
tree.remove(i - 1);
|
||||||
|
tree[i - 1] = InterTreeElement::Processed(v);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !did_something {
|
||||||
|
priority_level += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(match tree.into_iter().next().unwrap() {
|
||||||
|
InterTreeElement::Processed(x) => x,
|
||||||
|
x => x.to_value()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&self) -> Option<f32> {
|
||||||
|
self.to_tree()?.evaluate()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_board(board: [Option<Symb>; 11]) -> Self {
|
||||||
|
let free_spots = board.iter().filter(|x| x.is_none()).count();
|
||||||
|
Self {
|
||||||
|
board,
|
||||||
|
placed_by: Default::default(),
|
||||||
|
free_spots,
|
||||||
|
last_placed: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a board from a string
|
||||||
|
pub fn from_string(s: &str) -> Option<Self> {
|
||||||
|
if s.len() != 11 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = s
|
||||||
|
.chars()
|
||||||
|
.filter_map(|c| {
|
||||||
|
if c == '_' {
|
||||||
|
Some(None)
|
||||||
|
} else {
|
||||||
|
Symb::from_char(c).map(Some)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if x.len() != 11 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut free_spots = 11;
|
||||||
|
let mut board = [None; 11];
|
||||||
|
for i in 0..x.len() {
|
||||||
|
board[i] = x[i];
|
||||||
|
if x[i].is_some() {
|
||||||
|
free_spots -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Self {
|
||||||
|
board,
|
||||||
|
placed_by: Default::default(),
|
||||||
|
free_spots,
|
||||||
|
last_placed: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If true, this board is not done and has no valid moves
|
||||||
|
pub fn is_stuck(&self) -> bool {
|
||||||
|
if self.is_full() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This can only happen in a few cases,
|
||||||
|
// enumerated below.
|
||||||
|
|
||||||
|
// `9614807523_` (all numbers, one spot left for op)
|
||||||
|
// Note that this is _not_ a problem if the left spot is empty.
|
||||||
|
if self.free_spots == 1
|
||||||
|
&& self.board[10].is_none()
|
||||||
|
&& self
|
||||||
|
.board
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_ref())
|
||||||
|
.all(|x| !x.is_op())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `961487523/_` (forced division by zero)
|
||||||
|
if self.free_spots == 1
|
||||||
|
&& self.board[10].is_none()
|
||||||
|
&& self.board[9] == Some(Symb::Div)
|
||||||
|
&& self.board[0..8]
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| x.as_ref())
|
||||||
|
.all(|x| !x.is_op() && *x != Symb::Zero)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for Board {
|
||||||
|
type Item = String;
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.board
|
||||||
|
.iter()
|
||||||
|
.map(|x| x.map(|x| x.to_string()).unwrap_or_default())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomType for Board {
|
||||||
|
fn build(mut builder: TypeBuilder<Self>) {
|
||||||
|
builder
|
||||||
|
.with_name("Board")
|
||||||
|
.is_iterable()
|
||||||
|
.with_fn("to_string", |s: &mut Self| format!("{}", s))
|
||||||
|
.with_fn("to_debug", |s: &mut Self| format!("{:?}", s))
|
||||||
|
.with_fn("size", |s: &mut Self| s.board.len() as i64)
|
||||||
|
.with_fn("len", |s: &mut Self| s.board.len() as i64)
|
||||||
|
.with_fn("is_full", |s: &mut Self| s.is_full())
|
||||||
|
.with_fn("free_spots", |s: &mut Self| s.free_spots)
|
||||||
|
.with_fn("play", |s: &mut Self, act: PlayerAction| {
|
||||||
|
s.play(act, "NONE".to_owned()) // Player doesn't matter
|
||||||
|
})
|
||||||
|
.with_fn("ith_free_slot", |s: &mut Self, idx: usize| {
|
||||||
|
s.ith_empty_slot(idx).map(|x| x as i64).unwrap_or(-1)
|
||||||
|
})
|
||||||
|
.with_fn("can_play", |s: &mut Self, act: PlayerAction| {
|
||||||
|
s.can_play(&act)
|
||||||
|
})
|
||||||
|
.with_fn("contains", |s: &mut Self, sym: &str| {
|
||||||
|
match Symb::from_str(sym) {
|
||||||
|
None => false,
|
||||||
|
Some(x) => s.contains(x),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_fn("contains", |s: &mut Self, sym: i64| {
|
||||||
|
let sym = sym.to_string();
|
||||||
|
match Symb::from_str(&sym) {
|
||||||
|
None => false,
|
||||||
|
Some(x) => s.contains(x),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_fn("evaluate", |s: &mut Self| -> Dynamic {
|
||||||
|
s.evaluate().map(|x| x.into()).unwrap_or(().into())
|
||||||
|
})
|
||||||
|
.with_fn("free_spots_idx", |s: &mut Self| -> Array {
|
||||||
|
s.board
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, x)| x.is_none())
|
||||||
|
.map(|(i, _)| i as i64)
|
||||||
|
.map(|x| x.into())
|
||||||
|
.collect::<Vec<Dynamic>>()
|
||||||
|
})
|
||||||
|
.with_indexer_get(
|
||||||
|
|s: &mut Self, idx: i64| -> Result<String, Box<EvalAltResult>> {
|
||||||
|
if idx as usize >= s.board.len() {
|
||||||
|
return Err(
|
||||||
|
EvalAltResult::ErrorIndexNotFound(idx.into(), Position::NONE).into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = idx as usize;
|
||||||
|
return Ok(s.board[idx].map(|x| x.to_string()).unwrap_or_default());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_indexer_set(
|
||||||
|
|s: &mut Self, idx: i64, val: String| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let idx: usize = match idx.try_into() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
idx.into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if idx >= s.board.len() {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
(idx as i64).into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match Symb::from_str(&val) {
|
||||||
|
None => return Err(format!("Invalid symbol {val}").into()),
|
||||||
|
Some(x) => {
|
||||||
|
s.board[idx] = Some(x);
|
||||||
|
s.placed_by[idx] = Some("NONE".to_owned()); // Arbitrary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_indexer_set(
|
||||||
|
|s: &mut Self, idx: i64, _val: ()| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let idx: usize = match idx.try_into() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
idx.into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if idx >= s.board.len() {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
(idx as i64).into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
s.board[idx] = None;
|
||||||
|
s.placed_by[idx] = None;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_indexer_set(
|
||||||
|
|s: &mut Self, idx: i64, val: i64| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
let idx: usize = match idx.try_into() {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
idx.into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if idx >= s.board.len() {
|
||||||
|
return Err(EvalAltResult::ErrorIndexNotFound(
|
||||||
|
(idx as i64).into(),
|
||||||
|
Position::NONE,
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
match Symb::from_str(&val.to_string()) {
|
||||||
|
None => return Err(format!("Invalid symbol {val}").into()),
|
||||||
|
Some(x) => {
|
||||||
|
s.board[idx] = Some(x);
|
||||||
|
s.placed_by[idx] = Some("NULL".to_owned()); // Arbitrary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
rust/minimax/src/game/mod.rs
Normal file
9
rust/minimax/src/game/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod action;
|
||||||
|
mod board;
|
||||||
|
mod symb;
|
||||||
|
mod tree;
|
||||||
|
|
||||||
|
pub use action::PlayerAction;
|
||||||
|
pub use board::Board;
|
||||||
|
pub use symb::Symb;
|
||||||
|
pub use tree::TreeElement;
|
||||||
120
rust/minimax/src/game/symb.rs
Normal file
120
rust/minimax/src/game/symb.rs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
use rand::Rng;
|
||||||
|
use std::{
|
||||||
|
fmt::{Debug, Display},
|
||||||
|
num::NonZeroU8,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy, Hash)]
|
||||||
|
|
||||||
|
pub enum Symb {
|
||||||
|
Number(NonZeroU8),
|
||||||
|
Zero,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Times,
|
||||||
|
Div,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Symb {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Number(x) => write!(f, "{x}")?,
|
||||||
|
Self::Zero => write!(f, "0")?,
|
||||||
|
Self::Plus => write!(f, "+")?,
|
||||||
|
Self::Minus => write!(f, "-")?,
|
||||||
|
Self::Div => write!(f, "÷")?,
|
||||||
|
Self::Times => write!(f, "×")?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Debug for Symb {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Symb {
|
||||||
|
/// Is this symbol a plain binary operator?
|
||||||
|
pub fn is_op(&self) -> bool {
|
||||||
|
matches!(self, Symb::Div | Symb::Plus | Symb::Times | Symb::Minus)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_minus(&self) -> bool {
|
||||||
|
self == &Self::Minus
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_random<R: Rng>(rng: &mut R) -> Self {
|
||||||
|
match rng.gen_range(0..=13) {
|
||||||
|
0 => Symb::Zero,
|
||||||
|
n @ 1..=9 => Symb::Number(NonZeroU8::new(n).unwrap()),
|
||||||
|
10 => Symb::Div,
|
||||||
|
11 => Symb::Minus,
|
||||||
|
12 => Symb::Plus,
|
||||||
|
13 => Symb::Times,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_random_op<R: Rng>(rng: &mut R) -> Self {
|
||||||
|
match rng.gen_range(0..=3) {
|
||||||
|
0 => Symb::Div,
|
||||||
|
1 => Symb::Minus,
|
||||||
|
2 => Symb::Plus,
|
||||||
|
3 => Symb::Times,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn get_char(&self) -> Option<char> {
|
||||||
|
match self {
|
||||||
|
Self::Plus => Some('+'),
|
||||||
|
Self::Minus => Some('-'),
|
||||||
|
Self::Times => Some('×'),
|
||||||
|
Self::Div => Some('÷'),
|
||||||
|
Self::Zero => Some('0'),
|
||||||
|
Self::Number(x) => match x.get() {
|
||||||
|
1 => Some('1'),
|
||||||
|
2 => Some('2'),
|
||||||
|
3 => Some('3'),
|
||||||
|
4 => Some('4'),
|
||||||
|
5 => Some('5'),
|
||||||
|
6 => Some('6'),
|
||||||
|
7 => Some('7'),
|
||||||
|
8 => Some('8'),
|
||||||
|
9 => Some('9'),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Option<Self> {
|
||||||
|
if s.chars().count() != 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::from_char(s.chars().next()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn from_char(c: char) -> Option<Self> {
|
||||||
|
match c {
|
||||||
|
'1' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(1) })),
|
||||||
|
'2' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(2) })),
|
||||||
|
'3' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(3) })),
|
||||||
|
'4' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(4) })),
|
||||||
|
'5' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(5) })),
|
||||||
|
'6' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(6) })),
|
||||||
|
'7' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(7) })),
|
||||||
|
'8' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(8) })),
|
||||||
|
'9' => Some(Self::Number(unsafe { NonZeroU8::new_unchecked(9) })),
|
||||||
|
'0' => Some(Self::Zero),
|
||||||
|
'+' => Some(Self::Plus),
|
||||||
|
'-' => Some(Self::Minus),
|
||||||
|
'*' => Some(Self::Times),
|
||||||
|
'/' => Some(Self::Div),
|
||||||
|
'×' => Some(Self::Times),
|
||||||
|
'÷' => Some(Self::Div),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
rust/minimax/src/game/tree.rs
Normal file
143
rust/minimax/src/game/tree.rs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone)]
|
||||||
|
pub enum TreeElement {
|
||||||
|
Partial(String),
|
||||||
|
Number(f32),
|
||||||
|
Add {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Sub {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Mul {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Div {
|
||||||
|
l: Box<TreeElement>,
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
Neg {
|
||||||
|
r: Box<TreeElement>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for TreeElement {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Partial(s) => write!(f, "{s}")?,
|
||||||
|
Self::Number(n) => write!(f, "{n}")?,
|
||||||
|
Self::Add { l, r } => write!(f, "({l}+{r})")?,
|
||||||
|
Self::Div { l, r } => write!(f, "({l}÷{r})")?,
|
||||||
|
Self::Mul { l, r } => write!(f, "({l}×{r})")?,
|
||||||
|
Self::Sub { l, r } => write!(f, "({l}-{r})")?,
|
||||||
|
Self::Neg { r } => write!(f, "(-{r})")?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for TreeElement {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
Display::fmt(&self, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl TreeElement {
|
||||||
|
pub fn left(&self) -> Option<&TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { l, .. }
|
||||||
|
| Self::Sub { l, .. }
|
||||||
|
| Self::Mul { l, .. }
|
||||||
|
| Self::Div { l, .. } => Some(&**l),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right(&self) -> Option<&TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { r, .. }
|
||||||
|
| Self::Neg { r, .. }
|
||||||
|
| Self::Sub { r, .. }
|
||||||
|
| Self::Mul { r, .. }
|
||||||
|
| Self::Div { r, .. } => Some(&**r),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn left_mut(&mut self) -> Option<&mut TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { l, .. }
|
||||||
|
| Self::Sub { l, .. }
|
||||||
|
| Self::Mul { l, .. }
|
||||||
|
| Self::Div { l, .. } => Some(&mut **l),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn right_mut(&mut self) -> Option<&mut TreeElement> {
|
||||||
|
match self {
|
||||||
|
Self::Add { r, .. }
|
||||||
|
| Self::Neg { r, .. }
|
||||||
|
| Self::Sub { r, .. }
|
||||||
|
| Self::Mul { r, .. }
|
||||||
|
| Self::Div { r, .. } => Some(&mut **r),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&self) -> Option<f32> {
|
||||||
|
match self {
|
||||||
|
Self::Number(x) => Some(*x),
|
||||||
|
// Try to parse strings of a partial
|
||||||
|
Self::Partial(s) => s.parse().ok(),
|
||||||
|
Self::Add { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l + r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Mul { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l * r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Div { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
|
||||||
|
if r == Some(0.0) {
|
||||||
|
None
|
||||||
|
} else if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l / r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Sub { l, r } => {
|
||||||
|
let l = l.evaluate();
|
||||||
|
let r = r.evaluate();
|
||||||
|
if let (Some(l), Some(r)) = (l, r) {
|
||||||
|
Some(l - r)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Neg { r } => {
|
||||||
|
let r = r.evaluate();
|
||||||
|
r.map(|r| -r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
rust/minimax/src/lib.rs
Normal file
2
rust/minimax/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod agents;
|
||||||
|
pub mod game;
|
||||||
21
rust/rhai-codemirror/Cargo.toml
Normal file
21
rust/rhai-codemirror/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[package]
|
||||||
|
name = "rhai-codemirror"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rhai = { workspace = true, features = ["internals"] }
|
||||||
|
|
||||||
|
wasm-bindgen = { workspace = true }
|
||||||
|
js-sys = { workspace = true }
|
||||||
|
web-sys = { workspace = true, features = ["console"] }
|
||||||
|
console_error_panic_hook = { workspace = true }
|
||||||
|
wee_alloc = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde-wasm-bindgen = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
rhai = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
64
rust/rhai-codemirror/src/codemirror.rs
Normal file
64
rust/rhai-codemirror/src/codemirror.rs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
use js_sys::{Array, JsString, RegExp};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[wasm_bindgen(module = "codemirror")]
|
||||||
|
extern "C" {
|
||||||
|
pub type StringStream;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn eol(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn sol(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn peek(this: &StringStream) -> JsString;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
pub fn next(this: &StringStream) -> JsString;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eat)]
|
||||||
|
pub fn eat_regexp(this: &StringStream, m: &RegExp) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eatWhile)]
|
||||||
|
pub fn eat_while_regexp(this: &StringStream, m: &RegExp) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = eatSpace)]
|
||||||
|
pub fn eat_space(this: &StringStream) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = skipToEnd)]
|
||||||
|
pub fn skip_to_end(this: &StringStream);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = skipTo)]
|
||||||
|
pub fn skip_to(this: &StringStream, str: &str) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = match)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_str(this: &StringStream, pattern: &str, consume: bool, case_fold: bool) -> bool;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = match)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn match_regexp(this: &StringStream, pattern: &RegExp, consume: bool) -> Array;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = backUp)]
|
||||||
|
pub fn back_up(this: &StringStream, n: u32);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn column(this: &StringStream) -> u32;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn indentation(this: &StringStream) -> u32;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn current(this: &StringStream) -> String;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, js_name = lookAhead)]
|
||||||
|
#[must_use]
|
||||||
|
pub fn look_ahead(this: &StringStream, n: u32) -> Option<String>;
|
||||||
|
}
|
||||||
20
rust/rhai-codemirror/src/lib.rs
Normal file
20
rust/rhai-codemirror/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod codemirror;
|
||||||
|
mod rhai_mode;
|
||||||
|
|
||||||
|
pub use rhai_mode::*;
|
||||||
|
|
||||||
|
// Use `wee_alloc` as the global allocator for smaller WASM size.
|
||||||
|
#[global_allocator]
|
||||||
|
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub fn main_js() -> Result<(), JsValue> {
|
||||||
|
// This provides better error messages in debug mode.
|
||||||
|
// It's disabled in release mode so it doesn't bloat up the file size.
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
353
rust/rhai-codemirror/src/rhai_mode.rs
Normal file
353
rust/rhai-codemirror/src/rhai_mode.rs
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
use crate::codemirror;
|
||||||
|
use js_sys::RegExp;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use web_sys::console;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct RhaiMode {
|
||||||
|
indent_unit: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct State {
|
||||||
|
token_state: rhai::TokenizeState,
|
||||||
|
unclosed_bracket_count: i32,
|
||||||
|
line_indent: u32,
|
||||||
|
is_defining_identifier: bool,
|
||||||
|
/// Buffered character, if any. (For use by `StreamAdapter`.)
|
||||||
|
buf: Option<char>,
|
||||||
|
/// Interpolated string brace counting stack
|
||||||
|
interpolated_str_brace_stack: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
static ELECTRIC_INPUT: RegExp = RegExp::new("^\\s*[}\\])]$", "");
|
||||||
|
static LINE_COMMENT: JsValue = JsValue::from_str("//");
|
||||||
|
static CODEMIRROR_PASS: RefCell<JsValue> = RefCell::new(JsValue::null());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn init_codemirror_pass(codemirror_pass: JsValue) {
|
||||||
|
CODEMIRROR_PASS.with(|v| v.replace(codemirror_pass));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl RhaiMode {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(indent_unit: u32) -> Self {
|
||||||
|
Self { indent_unit }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = startState)]
|
||||||
|
pub fn start_state(&self) -> State {
|
||||||
|
State {
|
||||||
|
token_state: rhai::TokenizeState {
|
||||||
|
include_comments: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
unclosed_bracket_count: 0,
|
||||||
|
line_indent: 0,
|
||||||
|
is_defining_identifier: false,
|
||||||
|
buf: None,
|
||||||
|
interpolated_str_brace_stack: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = copyState)]
|
||||||
|
pub fn copy_state(&self, state: &State) -> State {
|
||||||
|
state.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn token(
|
||||||
|
&self,
|
||||||
|
stream: codemirror::StringStream,
|
||||||
|
state: &mut State,
|
||||||
|
) -> Result<Option<String>, JsValue> {
|
||||||
|
token(stream, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indent(&self, state: &mut State, text_after: String) -> JsValue {
|
||||||
|
indent(self, state, text_after)
|
||||||
|
.map(JsValue::from)
|
||||||
|
.unwrap_or_else(|| CODEMIRROR_PASS.with(|v| v.borrow().clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter, js_name = electricInput)]
|
||||||
|
pub fn electric_input(&self) -> RegExp {
|
||||||
|
ELECTRIC_INPUT.with(|v| v.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter, js_name = lineComment)]
|
||||||
|
pub fn line_comment(&self) -> JsValue {
|
||||||
|
LINE_COMMENT.with(|v| v.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StreamAdapter {
|
||||||
|
/// Buffered character, if any.
|
||||||
|
buf: Option<char>,
|
||||||
|
stream: codemirror::StringStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rhai::InputStream for StreamAdapter {
|
||||||
|
fn unget(&mut self, ch: char) {
|
||||||
|
self.buf = Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_next(&mut self) -> Option<char> {
|
||||||
|
if let Some(ch) = self.buf.take() {
|
||||||
|
return Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = self.stream.next();
|
||||||
|
if first.is_falsy() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
assert_eq!(first.length(), 1);
|
||||||
|
let first_code_unit = first.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) = std::char::decode_utf16(std::iter::once(first_code_unit)).next() {
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// The first value is likely an unpared surrogate, so we get one
|
||||||
|
// more UTF-16 unit to attempt to make a proper Unicode scalar.
|
||||||
|
let second = self.stream.next();
|
||||||
|
if second.is_falsy() {
|
||||||
|
return Some(std::char::REPLACEMENT_CHARACTER);
|
||||||
|
}
|
||||||
|
assert_eq!(second.length(), 1);
|
||||||
|
let second_code_unit = second.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) =
|
||||||
|
std::char::decode_utf16([first_code_unit, second_code_unit].iter().copied()).next()
|
||||||
|
{
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// Turns out to not be a proper surrogate pair, so back up one
|
||||||
|
// unit for it to be decoded separately.
|
||||||
|
self.stream.back_up(1);
|
||||||
|
Some(std::char::REPLACEMENT_CHARACTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek_next(&mut self) -> Option<char> {
|
||||||
|
if let Some(ch) = self.buf {
|
||||||
|
return Some(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = self.stream.peek();
|
||||||
|
if first.is_falsy() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
assert_eq!(first.length(), 1);
|
||||||
|
let first_code_unit = first.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) = std::char::decode_utf16(std::iter::once(first_code_unit)).next() {
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
// The first value is likely an unpared surrogate, so we get one more
|
||||||
|
// value to attempt to make a proper Unicode scalar value.
|
||||||
|
self.stream.next();
|
||||||
|
let second = self.stream.peek();
|
||||||
|
if second.is_falsy() {
|
||||||
|
return Some(std::char::REPLACEMENT_CHARACTER);
|
||||||
|
}
|
||||||
|
self.stream.back_up(1);
|
||||||
|
assert_eq!(second.length(), 1);
|
||||||
|
let second_code_unit = second.char_code_at(0) as u16;
|
||||||
|
if let Some(Ok(c)) =
|
||||||
|
std::char::decode_utf16([first_code_unit, second_code_unit].iter().copied()).next()
|
||||||
|
{
|
||||||
|
Some(c)
|
||||||
|
} else {
|
||||||
|
Some(std::char::REPLACEMENT_CHARACTER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token(stream: codemirror::StringStream, state: &mut State) -> Result<Option<String>, JsValue> {
|
||||||
|
if stream.sol() {
|
||||||
|
state.line_indent = stream.indentation();
|
||||||
|
state.unclosed_bracket_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stream_adapter = StreamAdapter {
|
||||||
|
stream,
|
||||||
|
buf: state.buf,
|
||||||
|
};
|
||||||
|
let (next_token, _) = rhai::get_next_token(
|
||||||
|
&mut stream_adapter,
|
||||||
|
&mut state.token_state,
|
||||||
|
&mut rhai::Position::default(),
|
||||||
|
);
|
||||||
|
state.buf = stream_adapter.buf;
|
||||||
|
match &next_token {
|
||||||
|
rhai::Token::LeftBrace
|
||||||
|
| rhai::Token::LeftBracket
|
||||||
|
| rhai::Token::LeftParen
|
||||||
|
| rhai::Token::MapStart => {
|
||||||
|
if state.unclosed_bracket_count < 0 {
|
||||||
|
state.unclosed_bracket_count = 0;
|
||||||
|
}
|
||||||
|
state.unclosed_bracket_count += 1;
|
||||||
|
}
|
||||||
|
rhai::Token::RightBrace | rhai::Token::RightBracket | rhai::Token::RightParen => {
|
||||||
|
state.unclosed_bracket_count -= 1;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
let res = match &next_token {
|
||||||
|
rhai::Token::IntegerConstant(_) => "number",
|
||||||
|
rhai::Token::FloatConstant(_) => "number",
|
||||||
|
rhai::Token::Identifier(_) => {
|
||||||
|
if state.is_defining_identifier {
|
||||||
|
"def"
|
||||||
|
} else {
|
||||||
|
"identifier"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rhai::Token::CharConstant(_) => "string-2",
|
||||||
|
rhai::Token::StringConstant(_) => "string",
|
||||||
|
rhai::Token::InterpolatedString(_) => {
|
||||||
|
state.interpolated_str_brace_stack.push(0);
|
||||||
|
"string"
|
||||||
|
}
|
||||||
|
rhai::Token::LeftBrace => {
|
||||||
|
if let Some(brace_counting) = state.interpolated_str_brace_stack.last_mut() {
|
||||||
|
*brace_counting += 1;
|
||||||
|
}
|
||||||
|
"bracket"
|
||||||
|
}
|
||||||
|
rhai::Token::RightBrace => {
|
||||||
|
if let Some(brace_counting) = state.interpolated_str_brace_stack.last_mut() {
|
||||||
|
*brace_counting -= 1;
|
||||||
|
if *brace_counting == 0 {
|
||||||
|
state.interpolated_str_brace_stack.pop();
|
||||||
|
state.token_state.is_within_text_terminated_by = Some("`".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"bracket"
|
||||||
|
}
|
||||||
|
rhai::Token::LeftParen => "bracket",
|
||||||
|
rhai::Token::RightParen => "bracket",
|
||||||
|
rhai::Token::LeftBracket => "bracket",
|
||||||
|
rhai::Token::RightBracket => "bracket",
|
||||||
|
rhai::Token::QuestionBracket => "bracket",
|
||||||
|
rhai::Token::Unit => "bracket", // empty fn parens are parsed as this
|
||||||
|
rhai::Token::Plus => "operator",
|
||||||
|
rhai::Token::UnaryPlus => "operator",
|
||||||
|
rhai::Token::Minus => "operator",
|
||||||
|
rhai::Token::UnaryMinus => "operator",
|
||||||
|
rhai::Token::Multiply => "operator",
|
||||||
|
rhai::Token::Divide => "operator",
|
||||||
|
rhai::Token::Modulo => "operator",
|
||||||
|
rhai::Token::PowerOf => "operator",
|
||||||
|
rhai::Token::LeftShift => "operator",
|
||||||
|
rhai::Token::RightShift => "operator",
|
||||||
|
rhai::Token::SemiColon => "operator",
|
||||||
|
rhai::Token::Colon => "operator",
|
||||||
|
rhai::Token::DoubleColon => "operator",
|
||||||
|
rhai::Token::Comma => "operator",
|
||||||
|
rhai::Token::Period => "operator",
|
||||||
|
rhai::Token::ExclusiveRange => "operator",
|
||||||
|
rhai::Token::InclusiveRange => "operator",
|
||||||
|
rhai::Token::MapStart => "bracket",
|
||||||
|
rhai::Token::Equals => "operator",
|
||||||
|
rhai::Token::True => "builtin",
|
||||||
|
rhai::Token::False => "builtin",
|
||||||
|
rhai::Token::Let => "keyword",
|
||||||
|
rhai::Token::Const => "keyword",
|
||||||
|
rhai::Token::If => "keyword",
|
||||||
|
rhai::Token::Else => "keyword",
|
||||||
|
rhai::Token::While => "keyword",
|
||||||
|
rhai::Token::Loop => "keyword",
|
||||||
|
rhai::Token::For => "keyword",
|
||||||
|
rhai::Token::In => "keyword",
|
||||||
|
rhai::Token::NotIn => "keyword",
|
||||||
|
rhai::Token::LessThan => "operator",
|
||||||
|
rhai::Token::GreaterThan => "operator",
|
||||||
|
rhai::Token::LessThanEqualsTo => "operator",
|
||||||
|
rhai::Token::GreaterThanEqualsTo => "operator",
|
||||||
|
rhai::Token::EqualsTo => "operator",
|
||||||
|
rhai::Token::NotEqualsTo => "operator",
|
||||||
|
rhai::Token::Bang => "operator",
|
||||||
|
rhai::Token::Elvis => "operator",
|
||||||
|
rhai::Token::DoubleQuestion => "operator",
|
||||||
|
rhai::Token::Pipe => "operator",
|
||||||
|
rhai::Token::Or => "operator",
|
||||||
|
rhai::Token::XOr => "operator",
|
||||||
|
rhai::Token::Ampersand => "operator",
|
||||||
|
rhai::Token::And => "operator",
|
||||||
|
rhai::Token::Fn => "keyword",
|
||||||
|
rhai::Token::Continue => "keyword",
|
||||||
|
rhai::Token::Break => "keyword",
|
||||||
|
rhai::Token::Return => "keyword",
|
||||||
|
rhai::Token::Throw => "keyword",
|
||||||
|
rhai::Token::PlusAssign => "operator",
|
||||||
|
rhai::Token::MinusAssign => "operator",
|
||||||
|
rhai::Token::MultiplyAssign => "operator",
|
||||||
|
rhai::Token::DivideAssign => "operator",
|
||||||
|
rhai::Token::LeftShiftAssign => "operator",
|
||||||
|
rhai::Token::RightShiftAssign => "operator",
|
||||||
|
rhai::Token::AndAssign => "operator",
|
||||||
|
rhai::Token::OrAssign => "operator",
|
||||||
|
rhai::Token::XOrAssign => "operator",
|
||||||
|
rhai::Token::ModuloAssign => "operator",
|
||||||
|
rhai::Token::PowerOfAssign => "operator",
|
||||||
|
rhai::Token::Private => "keyword",
|
||||||
|
// Import/Export/As tokens not available in this Rhai version
|
||||||
|
rhai::Token::DoubleArrow => "operator",
|
||||||
|
rhai::Token::Underscore => "operator",
|
||||||
|
rhai::Token::Switch => "keyword",
|
||||||
|
rhai::Token::Do => "keyword",
|
||||||
|
rhai::Token::Until => "keyword",
|
||||||
|
rhai::Token::Try => "keyword",
|
||||||
|
rhai::Token::Catch => "keyword",
|
||||||
|
rhai::Token::Comment(_) => "comment",
|
||||||
|
rhai::Token::LexError(e) => {
|
||||||
|
console::log_1(&JsValue::from_str(&format!("LexError: {}", e)));
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
rhai::Token::Reserved(_) => "keyword",
|
||||||
|
// Custom token not available in this Rhai version
|
||||||
|
rhai::Token::EOF => return Ok(None),
|
||||||
|
token @ _ => {
|
||||||
|
console::log_1(&JsValue::from_str(&format!("Unhandled token {:?}", token)));
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match &next_token {
|
||||||
|
rhai::Token::Fn | rhai::Token::Let | rhai::Token::Const | rhai::Token::For => {
|
||||||
|
state.is_defining_identifier = true;
|
||||||
|
}
|
||||||
|
rhai::Token::Comment(_) => {}
|
||||||
|
_ => {
|
||||||
|
state.is_defining_identifier = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Some(res.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn indent(mode: &RhaiMode, state: &State, text_after: String) -> Option<u32> {
|
||||||
|
let should_dedent = || {
|
||||||
|
text_after
|
||||||
|
.trim_start()
|
||||||
|
.starts_with(['}', ']', ')'].as_ref())
|
||||||
|
};
|
||||||
|
#[allow(clippy::collapsible_if)]
|
||||||
|
if state.unclosed_bracket_count > 0 {
|
||||||
|
if should_dedent() {
|
||||||
|
Some(state.line_indent)
|
||||||
|
} else {
|
||||||
|
Some(state.line_indent + mode.indent_unit)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if should_dedent() {
|
||||||
|
Some(state.line_indent.saturating_sub(mode.indent_unit))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
28
rust/runner/Cargo.toml
Normal file
28
rust/runner/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "runner"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
minimax = { workspace = true }
|
||||||
|
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
rand = { workspace = true }
|
||||||
|
itertools = { workspace = true }
|
||||||
|
|
||||||
|
wasm-bindgen = { workspace = true }
|
||||||
|
js-sys = { workspace = true }
|
||||||
|
web-sys = { workspace = true }
|
||||||
|
console_error_panic_hook = { workspace = true }
|
||||||
|
wee_alloc = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde-wasm-bindgen = { workspace = true }
|
||||||
|
rhai = { workspace = true }
|
||||||
|
getrandom = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
rhai = { workspace = true, features = ["wasm-bindgen"] }
|
||||||
|
getrandom = { workspace = true, features = ["js"] }
|
||||||
30
rust/runner/package.json
Normal file
30
rust/runner/package.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "script-runner-wasm",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Rust WASM script runner",
|
||||||
|
"main": "pkg/script_runner.js",
|
||||||
|
"types": "pkg/script_runner.d.ts",
|
||||||
|
"files": [
|
||||||
|
"pkg"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "wasm-pack build --target web --out-dir pkg",
|
||||||
|
"build:nodejs": "wasm-pack build --target nodejs --out-dir pkg-node",
|
||||||
|
"build:bundler": "wasm-pack build --target bundler --out-dir pkg-bundler",
|
||||||
|
"dev": "wasm-pack build --dev --target web --out-dir pkg"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "."
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"wasm",
|
||||||
|
"rust",
|
||||||
|
"script-runner"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"wasm-pack": "^0.12.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
rust/runner/src/ansi.rs
Normal file
12
rust/runner/src/ansi.rs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#![expect(clippy::allow_attributes)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
pub const RESET: &str = "\x1b[0m";
|
||||||
|
pub const RED: &str = "\x1b[31m";
|
||||||
|
pub const BLUE: &str = "\x1b[34m";
|
||||||
|
pub const MAGENTA: &str = "\x1b[35m";
|
||||||
|
|
||||||
|
pub const UP: &str = "\x1b[A";
|
||||||
|
pub const DOWN: &str = "\x1b[B";
|
||||||
|
pub const LEFT: &str = "\x1b[D";
|
||||||
|
pub const RIGHT: &str = "\x1b[C";
|
||||||
14
rust/runner/src/lib.rs
Normal file
14
rust/runner/src/lib.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
mod ansi;
|
||||||
|
mod minmaxgame;
|
||||||
|
mod minmaxgamehuman;
|
||||||
|
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();
|
||||||
|
}
|
||||||
421
rust/runner/src/minmaxgame.rs
Normal file
421
rust/runner/src/minmaxgame.rs
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
use minimax::{
|
||||||
|
agents::{Agent, RhaiAgent},
|
||||||
|
game::Board,
|
||||||
|
};
|
||||||
|
use rand::{rngs::StdRng, SeedableRng};
|
||||||
|
use rhai::ParseError;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::ansi;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum GameState {
|
||||||
|
/// Round 1, red is maximizing
|
||||||
|
Red,
|
||||||
|
|
||||||
|
/// Round 2, red is minimizing (and goes second)
|
||||||
|
Blue { red_score: f32 },
|
||||||
|
|
||||||
|
/// Game over, red won
|
||||||
|
RedWins { blue_score: f32 },
|
||||||
|
|
||||||
|
/// Game over, blue won
|
||||||
|
BlueWins { blue_score: f32 },
|
||||||
|
|
||||||
|
/// Game over, draw
|
||||||
|
DrawScore { score: f32 },
|
||||||
|
|
||||||
|
/// Invalid board, draw
|
||||||
|
DrawInvalid,
|
||||||
|
|
||||||
|
/// Error, end early
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct MinMaxGame {
|
||||||
|
red_agent: RhaiAgent<StdRng>,
|
||||||
|
blue_agent: RhaiAgent<StdRng>,
|
||||||
|
|
||||||
|
board: Board,
|
||||||
|
is_red_turn: bool,
|
||||||
|
is_first_print: bool,
|
||||||
|
state: GameState,
|
||||||
|
|
||||||
|
game_state_callback: Box<dyn Fn(&str) + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
impl MinMaxGame {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(
|
||||||
|
red_script: &str,
|
||||||
|
red_print_callback: js_sys::Function,
|
||||||
|
red_debug_callback: js_sys::Function,
|
||||||
|
|
||||||
|
blue_script: &str,
|
||||||
|
blue_print_callback: js_sys::Function,
|
||||||
|
blue_debug_callback: js_sys::Function,
|
||||||
|
|
||||||
|
game_state_callback: js_sys::Function,
|
||||||
|
) -> Result<MinMaxGame, String> {
|
||||||
|
Self::new_native(
|
||||||
|
red_script,
|
||||||
|
move |s| {
|
||||||
|
let _ = red_print_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = red_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
blue_script,
|
||||||
|
move |s| {
|
||||||
|
let _ = blue_print_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = blue_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
move |s| {
|
||||||
|
let _ = game_state_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|x| format!("Error at {}: {}", x.1, x.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_native(
|
||||||
|
red_script: &str,
|
||||||
|
red_print_callback: impl Fn(&str) + 'static,
|
||||||
|
red_debug_callback: impl Fn(&str) + 'static,
|
||||||
|
|
||||||
|
blue_script: &str,
|
||||||
|
blue_print_callback: impl Fn(&str) + 'static,
|
||||||
|
blue_debug_callback: impl Fn(&str) + 'static,
|
||||||
|
|
||||||
|
game_state_callback: impl Fn(&str) + 'static,
|
||||||
|
) -> Result<MinMaxGame, ParseError> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
let mut seed1 = [0u8; 32];
|
||||||
|
let mut seed2 = [0u8; 32];
|
||||||
|
getrandom::getrandom(&mut seed1).unwrap();
|
||||||
|
getrandom::getrandom(&mut seed2).unwrap();
|
||||||
|
|
||||||
|
Ok(MinMaxGame {
|
||||||
|
board: Board::new(),
|
||||||
|
is_first_print: true,
|
||||||
|
is_red_turn: true,
|
||||||
|
state: GameState::Red,
|
||||||
|
|
||||||
|
red_agent: RhaiAgent::new(
|
||||||
|
red_script,
|
||||||
|
StdRng::from_seed(seed1),
|
||||||
|
red_print_callback,
|
||||||
|
red_debug_callback,
|
||||||
|
)?,
|
||||||
|
|
||||||
|
blue_agent: RhaiAgent::new(
|
||||||
|
blue_script,
|
||||||
|
StdRng::from_seed(seed2),
|
||||||
|
blue_print_callback,
|
||||||
|
blue_debug_callback,
|
||||||
|
)?,
|
||||||
|
|
||||||
|
game_state_callback: Box::new(game_state_callback),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is this game over for any reason?
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_done(&self) -> bool {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. }
|
||||||
|
| GameState::DrawInvalid
|
||||||
|
| GameState::Error
|
||||||
|
| GameState::BlueWins { .. }
|
||||||
|
| GameState::RedWins { .. } => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn red_won(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(false),
|
||||||
|
GameState::DrawInvalid { .. } => Some(false),
|
||||||
|
GameState::BlueWins { .. } => Some(false),
|
||||||
|
GameState::RedWins { .. } => Some(true),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn blue_won(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(false),
|
||||||
|
GameState::DrawInvalid { .. } => Some(false),
|
||||||
|
GameState::BlueWins { .. } => Some(true),
|
||||||
|
GameState::RedWins { .. } => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_draw_score(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(true),
|
||||||
|
GameState::DrawInvalid { .. } => Some(false),
|
||||||
|
GameState::BlueWins { .. } => Some(false),
|
||||||
|
GameState::RedWins { .. } => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_draw_invalid(&self) -> Option<bool> {
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { .. } => Some(false),
|
||||||
|
GameState::DrawInvalid { .. } => Some(true),
|
||||||
|
GameState::BlueWins { .. } => Some(false),
|
||||||
|
GameState::RedWins { .. } => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn is_error(&self) -> bool {
|
||||||
|
match self.state {
|
||||||
|
GameState::Error => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play one turn
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn step(&mut self) -> Result<(), String> {
|
||||||
|
if self.is_first_print {
|
||||||
|
self.print_board("", "");
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = match (self.state, self.is_red_turn) {
|
||||||
|
(GameState::Blue { .. }, false) => self.blue_agent.step_max(&self.board),
|
||||||
|
(GameState::Red, false) => self.blue_agent.step_min(&self.board),
|
||||||
|
(GameState::Blue { .. }, true) => self.red_agent.step_min(&self.board),
|
||||||
|
(GameState::Red, true) => self.red_agent.step_max(&self.board),
|
||||||
|
|
||||||
|
// Game is done, do nothing
|
||||||
|
(GameState::Error, _)
|
||||||
|
| (GameState::BlueWins { .. }, _)
|
||||||
|
| (GameState::RedWins { .. }, _)
|
||||||
|
| (GameState::DrawInvalid, _)
|
||||||
|
| (GameState::DrawScore { .. }, _) => return Ok(()),
|
||||||
|
}
|
||||||
|
.map_err(|err| format!("{err}"))?;
|
||||||
|
|
||||||
|
let player = self.is_red_turn.then_some("Red").unwrap_or("Blue");
|
||||||
|
let player_name = self
|
||||||
|
.is_red_turn
|
||||||
|
.then_some(self.red_agent.name())
|
||||||
|
.unwrap_or(self.blue_agent.name());
|
||||||
|
|
||||||
|
if !self.board.play(action, player) {
|
||||||
|
self.state = GameState::Error;
|
||||||
|
return Err(format!(
|
||||||
|
"{player} ({player_name}) made an invalid move {action}",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.print_board(
|
||||||
|
self.is_red_turn.then_some(ansi::RED).unwrap_or(ansi::BLUE),
|
||||||
|
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||||
|
);
|
||||||
|
(self.game_state_callback)("\n\r");
|
||||||
|
|
||||||
|
if !self.board.is_full() && !self.board.is_stuck() {
|
||||||
|
// This was not the last move
|
||||||
|
self.is_red_turn = !self.is_red_turn;
|
||||||
|
} else {
|
||||||
|
// This was the last move
|
||||||
|
|
||||||
|
// Close board
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}╙{}╜\n\r",
|
||||||
|
" ".repeat(6),
|
||||||
|
" ".repeat(self.board.size())
|
||||||
|
));
|
||||||
|
|
||||||
|
// Evaluate board and update state
|
||||||
|
match (self.state, self.board.evaluate()) {
|
||||||
|
// Start next round
|
||||||
|
(GameState::Red, Some(red_score)) => {
|
||||||
|
self.board = Board::new();
|
||||||
|
self.is_first_print = true;
|
||||||
|
self.is_red_turn = false;
|
||||||
|
self.state = GameState::Blue { red_score }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game over
|
||||||
|
(GameState::Blue { red_score }, Some(blue_score)) => {
|
||||||
|
self.state = match red_score.total_cmp(&blue_score) {
|
||||||
|
Ordering::Equal => GameState::DrawScore { score: red_score },
|
||||||
|
Ordering::Greater => GameState::RedWins { blue_score },
|
||||||
|
Ordering::Less => GameState::BlueWins { blue_score },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Could not evaluate board, tie by default
|
||||||
|
(GameState::Red, None) | (GameState::Blue { .. }, None) => {
|
||||||
|
self.state = GameState::DrawInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other code should make sure this never happens
|
||||||
|
(GameState::BlueWins { .. }, _)
|
||||||
|
| (GameState::RedWins { .. }, _)
|
||||||
|
| (GameState::DrawInvalid, _)
|
||||||
|
| (GameState::DrawScore { .. }, _)
|
||||||
|
| (GameState::Error, _) => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.board.is_stuck() {
|
||||||
|
self.state = GameState::DrawInvalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print depending on new state
|
||||||
|
match self.state {
|
||||||
|
GameState::DrawScore { score } => {
|
||||||
|
(self.game_state_callback)(&format!("Tie! Score: {score:.2}\n\r"));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::DrawInvalid => {
|
||||||
|
(self.game_state_callback)(&format!("Tie, invalid board!\n\r"));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::RedWins { blue_score, .. } => {
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Blue score:{} {blue_score:.2}\n\r",
|
||||||
|
ansi::BLUE,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Red wins!{}\n\r",
|
||||||
|
ansi::RED,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::BlueWins { blue_score, .. } => {
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Blue score:{} {blue_score:.2}\n\r",
|
||||||
|
ansi::BLUE,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Blue wins!{}\n\r",
|
||||||
|
ansi::BLUE,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
GameState::Blue { red_score } => {
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"{}Red score:{} {red_score:.2}\n\r",
|
||||||
|
ansi::RED,
|
||||||
|
ansi::RESET,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other code should make sure this never happens
|
||||||
|
GameState::Error | GameState::Red => unreachable!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
(self.game_state_callback)("\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_board(&mut self, color: &str, player: &str) {
|
||||||
|
let board_label = format!("{}{:<6}{}", color, player, ansi::RESET);
|
||||||
|
|
||||||
|
(self.game_state_callback)(&format!(
|
||||||
|
"\r{}{}{}{}",
|
||||||
|
board_label,
|
||||||
|
if self.is_first_print { '╓' } else { '║' },
|
||||||
|
self.board.prettyprint(),
|
||||||
|
if self.is_first_print { '╖' } else { '║' },
|
||||||
|
));
|
||||||
|
self.is_first_print = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: tests
|
||||||
|
//
|
||||||
|
// TODO:
|
||||||
|
// - infinite loop
|
||||||
|
// - random is different
|
||||||
|
// - incorrect return type
|
||||||
|
// - globals
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn full_random() {
|
||||||
|
const SCRIPT: &str = r#"
|
||||||
|
fn random_action(board) {
|
||||||
|
let symb = rand_symb();
|
||||||
|
let pos = rand_int(0, 10);
|
||||||
|
let action = Action(symb, pos);
|
||||||
|
|
||||||
|
while !board.can_play(action) {
|
||||||
|
let symb = rand_symb();
|
||||||
|
let pos = rand_int(0, 10);
|
||||||
|
action = Action(symb, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_min(board) {
|
||||||
|
random_action(board)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_max(board) {
|
||||||
|
random_action(board)
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut game =
|
||||||
|
MinMaxGame::new_native(&SCRIPT, |_| {}, |_| {}, &SCRIPT, |_| {}, |_| {}, |_| {}).unwrap();
|
||||||
|
|
||||||
|
let mut n = 0;
|
||||||
|
while !game.is_done() {
|
||||||
|
println!("{:?}", game.step());
|
||||||
|
println!("{:?}", game.board);
|
||||||
|
|
||||||
|
n += 1;
|
||||||
|
assert!(n < 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn infinite_loop() {
|
||||||
|
const SCRIPT: &str = r#"
|
||||||
|
fn step_min(board) {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step_max(board) {
|
||||||
|
loop {}
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
|
let mut game =
|
||||||
|
MinMaxGame::new_native(&SCRIPT, |_| {}, |_| {}, &SCRIPT, |_| {}, |_| {}, |_| {}).unwrap();
|
||||||
|
|
||||||
|
while !game.is_done() {
|
||||||
|
println!("{:?}", game.step());
|
||||||
|
println!("{:?}", game.board);
|
||||||
|
}
|
||||||
|
}
|
||||||
288
rust/runner/src/minmaxgamehuman.rs
Normal file
288
rust/runner/src/minmaxgamehuman.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
use minimax::{
|
||||||
|
agents::{Agent, RhaiAgent},
|
||||||
|
game::Board,
|
||||||
|
};
|
||||||
|
use rand::{rngs::StdRng, SeedableRng};
|
||||||
|
use rhai::ParseError;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::{ansi, terminput::TermInput};
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub struct MinMaxGameHuman {
|
||||||
|
/// 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 MinMaxGameHuman {
|
||||||
|
#[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<MinMaxGameHuman, 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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
181
rust/runner/src/terminput.rs
Normal file
181
rust/runner/src/terminput.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use itertools::Itertools;
|
||||||
|
use minimax::game::{Board, PlayerAction, Symb};
|
||||||
|
|
||||||
|
use crate::ansi::{DOWN, LEFT, RESET, RIGHT, UP};
|
||||||
|
|
||||||
|
struct SymbolSelector {
|
||||||
|
symbols: Vec<char>,
|
||||||
|
cursor: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SymbolSelector {
|
||||||
|
fn new(symbols: Vec<char>) -> Self {
|
||||||
|
Self { symbols, cursor: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current(&self) -> char {
|
||||||
|
self.symbols[self.cursor]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&mut self, board: &Board) {
|
||||||
|
while board.contains(Symb::from_char(self.current()).unwrap()) {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
self.cursor = self.symbols.len() - 1;
|
||||||
|
} else {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn down(&mut self, board: &Board) {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
self.cursor = self.symbols.len() - 1;
|
||||||
|
} else {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while board.contains(Symb::from_char(self.current()).unwrap()) {
|
||||||
|
if self.cursor == 0 {
|
||||||
|
self.cursor = self.symbols.len() - 1;
|
||||||
|
} else {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn up(&mut self, board: &Board) {
|
||||||
|
if self.cursor == self.symbols.len() - 1 {
|
||||||
|
self.cursor = 0;
|
||||||
|
} else {
|
||||||
|
self.cursor += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
while board.contains(Symb::from_char(self.current()).unwrap()) {
|
||||||
|
if self.cursor == self.symbols.len() - 1 {
|
||||||
|
self.cursor = 0;
|
||||||
|
} else {
|
||||||
|
self.cursor += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TermInput {
|
||||||
|
player_color: String,
|
||||||
|
cursor: usize,
|
||||||
|
symbol_selector: SymbolSelector,
|
||||||
|
|
||||||
|
/// Set to Some() when the player selects an action.
|
||||||
|
/// Should be cleared and applied immediately.
|
||||||
|
queued_action: Option<PlayerAction>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TermInput {
|
||||||
|
pub fn new(player_color: String) -> Self {
|
||||||
|
Self {
|
||||||
|
cursor: 0,
|
||||||
|
queued_action: None,
|
||||||
|
|
||||||
|
player_color,
|
||||||
|
symbol_selector: SymbolSelector::new(vec![
|
||||||
|
'1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '+', '-', '×', '÷',
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop_action(&mut self) -> Option<PlayerAction> {
|
||||||
|
self.queued_action.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_state(&mut self, board: &Board, minimize: bool) -> String {
|
||||||
|
let cursor_max = board.size() - 1;
|
||||||
|
self.symbol_selector.check(board);
|
||||||
|
|
||||||
|
let board_label = format!(
|
||||||
|
"{}{:<6}{RESET}",
|
||||||
|
self.player_color,
|
||||||
|
if minimize { "Min" } else { "Max" },
|
||||||
|
);
|
||||||
|
|
||||||
|
return format!(
|
||||||
|
"\r{}╙{}{}{}{RESET}{}╜ {}",
|
||||||
|
board_label,
|
||||||
|
// Cursor
|
||||||
|
" ".repeat(self.cursor),
|
||||||
|
self.player_color,
|
||||||
|
if board.is_full() {
|
||||||
|
' '
|
||||||
|
} else {
|
||||||
|
self.symbol_selector.current()
|
||||||
|
},
|
||||||
|
// RESET
|
||||||
|
" ".repeat(cursor_max - self.cursor),
|
||||||
|
self.symbol_selector
|
||||||
|
.symbols
|
||||||
|
.iter()
|
||||||
|
.map(|x| {
|
||||||
|
if board.contains(Symb::from_char(*x).unwrap()) {
|
||||||
|
" ".to_string()
|
||||||
|
} else if *x == self.symbol_selector.current() {
|
||||||
|
format!("{}{x}{RESET}", self.player_color,)
|
||||||
|
} else {
|
||||||
|
format!("{x}",)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.join("")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn process_input(&mut self, board: &Board, data: String) {
|
||||||
|
let cursor_max = board.size() - 1;
|
||||||
|
self.symbol_selector.check(board);
|
||||||
|
|
||||||
|
match &data[..] {
|
||||||
|
RIGHT => {
|
||||||
|
self.cursor = cursor_max.min(self.cursor + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
LEFT => {
|
||||||
|
if self.cursor != 0 {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UP => {
|
||||||
|
self.symbol_selector.up(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
DOWN => {
|
||||||
|
self.symbol_selector.down(board);
|
||||||
|
}
|
||||||
|
|
||||||
|
" " | "\n" | "\r" => {
|
||||||
|
let symb = Symb::from_char(self.symbol_selector.current());
|
||||||
|
if let Some(symb) = symb {
|
||||||
|
let action = PlayerAction {
|
||||||
|
symb,
|
||||||
|
pos: self.cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
if board.can_play(&action) {
|
||||||
|
self.queued_action = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c => {
|
||||||
|
let symb = Symb::from_str(c);
|
||||||
|
if let Some(symb) = symb {
|
||||||
|
let action = PlayerAction {
|
||||||
|
symb,
|
||||||
|
pos: self.cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
if board.can_play(&action) {
|
||||||
|
self.queued_action = Some(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
1
rust/rustfmt.toml
Normal file
1
rust/rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hard_tabs = true
|
||||||
7
webui/.eslintrc.json
Normal file
7
webui/.eslintrc.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals"],
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-img-element": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
webui/.prettierrc
Normal file
4
webui/.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"useTabs": true,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
||||||
719
webui/bun.lock
Normal file
719
webui/bun.lock
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "rhai-playground-next",
|
||||||
|
"dependencies": {
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"codemirror": "^5.65.1",
|
||||||
|
"lucide-react": "^0.548.0",
|
||||||
|
"next": "14.0.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/codemirror": "0.0.96",
|
||||||
|
"@types/node": "^20.8.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"eslint": "^8.51.0",
|
||||||
|
"eslint-config-next": "14.0.0",
|
||||||
|
"typescript": "^5.2.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
|
||||||
|
|
||||||
|
"@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="],
|
||||||
|
|
||||||
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
|
||||||
|
|
||||||
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="],
|
||||||
|
|
||||||
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
|
|
||||||
|
"@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
|
||||||
|
|
||||||
|
"@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||||
|
|
||||||
|
"@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="],
|
||||||
|
|
||||||
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
|
"@next/env": ["@next/env@14.0.0", "", {}, "sha512-cIKhxkfVELB6hFjYsbtEeTus2mwrTC+JissfZYM0n+8Fv+g8ucUfOlm3VEDtwtwydZ0Nuauv3bl0qF82nnCAqA=="],
|
||||||
|
|
||||||
|
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@14.0.0", "", { "dependencies": { "glob": "7.1.7" } }, "sha512-Ye37nNI09V3yt7pzuzSQtwlvuJ2CGzFszHXkcTHHZgNr7EhTMFLipn3VSJChy+e5+ahTdNApPphc3qCPUsn10A=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@14.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HQKi159jCz4SRsPesVCiNN6tPSAFUkOuSkpJsqYTIlbHLKr1mD6be/J0TvWV6fwJekj81bZV9V/Tgx3C2HO9lA=="],
|
||||||
|
|
||||||
|
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@14.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4YyQLMSaCgX/kgC1jjF3s3xSoBnwHuDhnF6WA1DWNEYRsbOOPWjcYhv8TKhRe2ApdOam+VfQSffC4ZD+X4u1Cg=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@14.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-io7fMkJ28Glj7SH8yvnlD6naIhRDnDxeE55CmpQkj3+uaA2Hko6WGY2pT5SzpQLTnGGnviK85cy8EJ2qsETj/g=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@14.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-nC2h0l1Jt8LEzyQeSs/BKpXAMe0mnHIMykYALWaeddTqCv5UEN8nGO3BG8JAqW/Y8iutqJsaMe2A9itS0d/r8w=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@14.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Wf+WjXibJQ7hHXOdNOmSMW5bxeJHVf46Pwb3eLSD2L76NrytQlif9NH7JpHuFlYKCQGfKfgSYYre5rIfmnSwQw=="],
|
||||||
|
|
||||||
|
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@14.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-WTZb2G7B+CTsdigcJVkRxfcAIQj7Lf0ipPNRJ3vlSadU8f0CFGv/ST+sJwF5eSwIe6dxKoX0DG6OljDBaad+rg=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@14.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-7R8/x6oQODmNpnWVW00rlWX90sIlwluJwcvMT6GXNIBOvEf01t3fBg0AGURNKdTJg2xNuP7TyLchCL7Lh2DTiw=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-ia32-msvc": ["@next/swc-win32-ia32-msvc@14.0.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-RLK1nELvhCnxaWPF07jGU4x3tjbyx2319q43loZELqF0+iJtKutZ+Lk8SVmf/KiJkYBc7Cragadz7hb3uQvz4g=="],
|
||||||
|
|
||||||
|
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@14.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-g6hLf1SUko+hnnaywQQZzzb3BRecQsoKkF3o/C+F+dOA4w/noVAJngUVkfwF0+2/8FzNznM7ofM6TGZO9svn7w=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
|
|
||||||
|
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
|
||||||
|
|
||||||
|
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="],
|
||||||
|
|
||||||
|
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||||
|
|
||||||
|
"@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.14.1", "", {}, "sha512-jGTk8UD/RdjsNZW8qq10r0RBvxL8OWtoT+kImlzPDFilmozzM+9QmIJsmze9UiSBrFU45ZxhTYBypn9q9z/VfQ=="],
|
||||||
|
|
||||||
|
"@swc/helpers": ["@swc/helpers@0.5.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw=="],
|
||||||
|
|
||||||
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
|
"@types/codemirror": ["@types/codemirror@0.0.96", "", { "dependencies": { "@types/tern": "*" } }, "sha512-GTswEV26Bl1byRxpD3sKd1rT2AISr0rK9ImlJgEzfvqhcVWeu4xQKFQI6UgSC95NT5swNG4st/oRMeGVZgPj9w=="],
|
||||||
|
|
||||||
|
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@20.19.24", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA=="],
|
||||||
|
|
||||||
|
"@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@18.3.26", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA=="],
|
||||||
|
|
||||||
|
"@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="],
|
||||||
|
|
||||||
|
"@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/parser": ["@typescript-eslint/parser@6.21.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0" } }, "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" } }, "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/types": ["@typescript-eslint/types@6.21.0", "", {}, "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" } }, "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@6.21.0", "", { "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" } }, "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A=="],
|
||||||
|
|
||||||
|
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="],
|
||||||
|
|
||||||
|
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||||
|
|
||||||
|
"@xterm/xterm": ["@xterm/xterm@5.5.0", "", {}, "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A=="],
|
||||||
|
|
||||||
|
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||||
|
|
||||||
|
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
|
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||||
|
|
||||||
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
|
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||||
|
|
||||||
|
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
||||||
|
|
||||||
|
"array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
|
||||||
|
|
||||||
|
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ=="],
|
||||||
|
|
||||||
|
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
|
||||||
|
|
||||||
|
"array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
|
||||||
|
|
||||||
|
"array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="],
|
||||||
|
|
||||||
|
"array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA=="],
|
||||||
|
|
||||||
|
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||||
|
|
||||||
|
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="],
|
||||||
|
|
||||||
|
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
|
"axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="],
|
||||||
|
|
||||||
|
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||||
|
|
||||||
|
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
|
|
||||||
|
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||||
|
|
||||||
|
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||||
|
|
||||||
|
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||||
|
|
||||||
|
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||||
|
|
||||||
|
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||||
|
|
||||||
|
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||||
|
|
||||||
|
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||||
|
|
||||||
|
"caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="],
|
||||||
|
|
||||||
|
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"codemirror": ["codemirror@5.65.20", "", {}, "sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA=="],
|
||||||
|
|
||||||
|
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||||
|
|
||||||
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
|
||||||
|
|
||||||
|
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
|
||||||
|
|
||||||
|
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
|
||||||
|
|
||||||
|
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||||
|
|
||||||
|
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
|
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||||
|
|
||||||
|
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
|
||||||
|
|
||||||
|
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||||
|
|
||||||
|
"dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="],
|
||||||
|
|
||||||
|
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
|
||||||
|
|
||||||
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
|
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
|
|
||||||
|
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||||
|
|
||||||
|
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
|
|
||||||
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
|
"es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="],
|
||||||
|
|
||||||
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
|
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||||
|
|
||||||
|
"es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="],
|
||||||
|
|
||||||
|
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="],
|
||||||
|
|
||||||
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
|
|
||||||
|
"eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="],
|
||||||
|
|
||||||
|
"eslint-config-next": ["eslint-config-next@14.0.0", "", { "dependencies": { "@next/eslint-plugin-next": "14.0.0", "@rushstack/eslint-patch": "^1.3.3", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" }, "peerDependencies": { "eslint": "^7.23.0 || ^8.0.0", "typescript": ">=3.3.1" }, "optionalPeers": ["typescript"] }, "sha512-jtXeE+/pGQ3h9n11QyyuPN50kO13GO5XvjU5ZRq6W+XTpOMjyobWmK2s7aowy0FtzA49krJzYzEU9s1RMwoJ6g=="],
|
||||||
|
|
||||||
|
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="],
|
||||||
|
|
||||||
|
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ=="],
|
||||||
|
|
||||||
|
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@4.6.2", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" } }, "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ=="],
|
||||||
|
|
||||||
|
"eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
|
||||||
|
|
||||||
|
"eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
|
"espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
|
||||||
|
|
||||||
|
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||||
|
|
||||||
|
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
|
|
||||||
|
"estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||||
|
|
||||||
|
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||||
|
|
||||||
|
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||||
|
|
||||||
|
"fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
|
||||||
|
|
||||||
|
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||||
|
|
||||||
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||||
|
|
||||||
|
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
|
"file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="],
|
||||||
|
|
||||||
|
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
|
||||||
|
|
||||||
|
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
|
||||||
|
|
||||||
|
"flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="],
|
||||||
|
|
||||||
|
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||||
|
|
||||||
|
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||||
|
|
||||||
|
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||||
|
|
||||||
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
|
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
|
||||||
|
|
||||||
|
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||||
|
|
||||||
|
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||||
|
|
||||||
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
|
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||||
|
|
||||||
|
"get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="],
|
||||||
|
|
||||||
|
"glob": ["glob@7.1.7", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ=="],
|
||||||
|
|
||||||
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
|
|
||||||
|
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||||
|
|
||||||
|
"globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="],
|
||||||
|
|
||||||
|
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
|
||||||
|
|
||||||
|
"globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
|
||||||
|
|
||||||
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
|
|
||||||
|
"graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
|
||||||
|
|
||||||
|
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||||
|
|
||||||
|
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||||
|
|
||||||
|
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||||
|
|
||||||
|
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||||
|
|
||||||
|
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||||
|
|
||||||
|
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||||
|
|
||||||
|
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||||
|
|
||||||
|
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||||
|
|
||||||
|
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||||
|
|
||||||
|
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||||
|
|
||||||
|
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
|
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||||
|
|
||||||
|
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||||
|
|
||||||
|
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="],
|
||||||
|
|
||||||
|
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||||
|
|
||||||
|
"is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="],
|
||||||
|
|
||||||
|
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||||
|
|
||||||
|
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
|
|
||||||
|
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="],
|
||||||
|
|
||||||
|
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="],
|
||||||
|
|
||||||
|
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||||
|
|
||||||
|
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="],
|
||||||
|
|
||||||
|
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||||
|
|
||||||
|
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||||
|
|
||||||
|
"is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||||
|
|
||||||
|
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||||
|
|
||||||
|
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
|
||||||
|
|
||||||
|
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="],
|
||||||
|
|
||||||
|
"is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="],
|
||||||
|
|
||||||
|
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||||
|
|
||||||
|
"is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||||
|
|
||||||
|
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||||
|
|
||||||
|
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="],
|
||||||
|
|
||||||
|
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="],
|
||||||
|
|
||||||
|
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="],
|
||||||
|
|
||||||
|
"is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="],
|
||||||
|
|
||||||
|
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="],
|
||||||
|
|
||||||
|
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="],
|
||||||
|
|
||||||
|
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
|
||||||
|
|
||||||
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||||
|
|
||||||
|
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g=="],
|
||||||
|
|
||||||
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
|
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||||
|
|
||||||
|
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||||
|
|
||||||
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
|
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||||
|
|
||||||
|
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="],
|
||||||
|
|
||||||
|
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||||
|
|
||||||
|
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="],
|
||||||
|
|
||||||
|
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="],
|
||||||
|
|
||||||
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
|
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||||
|
|
||||||
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lucide-react": ["lucide-react@0.548.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA=="],
|
||||||
|
|
||||||
|
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||||
|
|
||||||
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|
||||||
|
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||||
|
|
||||||
|
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
|
||||||
|
|
||||||
|
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||||
|
|
||||||
|
"next": ["next@14.0.0", "", { "dependencies": { "@next/env": "14.0.0", "@swc/helpers": "0.5.2", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001406", "postcss": "8.4.31", "styled-jsx": "5.1.1", "watchpack": "2.4.0" }, "optionalDependencies": { "@next/swc-darwin-arm64": "14.0.0", "@next/swc-darwin-x64": "14.0.0", "@next/swc-linux-arm64-gnu": "14.0.0", "@next/swc-linux-arm64-musl": "14.0.0", "@next/swc-linux-x64-gnu": "14.0.0", "@next/swc-linux-x64-musl": "14.0.0", "@next/swc-win32-arm64-msvc": "14.0.0", "@next/swc-win32-ia32-msvc": "14.0.0", "@next/swc-win32-x64-msvc": "14.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-J0jHKBJpB9zd4+c153sair0sz44mbaCHxggs8ryVXSFBuBqJ8XdE9/ozoV85xGh2VnSjahwntBZZgsihL9QznA=="],
|
||||||
|
|
||||||
|
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||||
|
|
||||||
|
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||||
|
|
||||||
|
"object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="],
|
||||||
|
|
||||||
|
"object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="],
|
||||||
|
|
||||||
|
"object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="],
|
||||||
|
|
||||||
|
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="],
|
||||||
|
|
||||||
|
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||||
|
|
||||||
|
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
|
||||||
|
|
||||||
|
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||||
|
|
||||||
|
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
|
||||||
|
|
||||||
|
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||||
|
|
||||||
|
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||||
|
|
||||||
|
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||||
|
|
||||||
|
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||||
|
|
||||||
|
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||||
|
|
||||||
|
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||||
|
|
||||||
|
"path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="],
|
||||||
|
|
||||||
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
|
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||||
|
|
||||||
|
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||||
|
|
||||||
|
"postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||||
|
|
||||||
|
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||||
|
|
||||||
|
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
|
"react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="],
|
||||||
|
|
||||||
|
"react-dom": ["react-dom@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g=="],
|
||||||
|
|
||||||
|
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
|
|
||||||
|
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||||
|
|
||||||
|
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||||
|
|
||||||
|
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||||
|
|
||||||
|
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
|
"reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||||
|
|
||||||
|
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||||
|
|
||||||
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
|
|
||||||
|
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||||
|
|
||||||
|
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
|
||||||
|
|
||||||
|
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="],
|
||||||
|
|
||||||
|
"scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
|
||||||
|
|
||||||
|
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|
||||||
|
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||||
|
|
||||||
|
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||||
|
|
||||||
|
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="],
|
||||||
|
|
||||||
|
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||||
|
|
||||||
|
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||||
|
|
||||||
|
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||||
|
|
||||||
|
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||||
|
|
||||||
|
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||||
|
|
||||||
|
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||||
|
|
||||||
|
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
|
||||||
|
|
||||||
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
|
"stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="],
|
||||||
|
|
||||||
|
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||||
|
|
||||||
|
"streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="],
|
||||||
|
|
||||||
|
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg=="],
|
||||||
|
|
||||||
|
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="],
|
||||||
|
|
||||||
|
"string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w=="],
|
||||||
|
|
||||||
|
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="],
|
||||||
|
|
||||||
|
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="],
|
||||||
|
|
||||||
|
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||||
|
|
||||||
|
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||||
|
|
||||||
|
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||||
|
|
||||||
|
"styled-jsx": ["styled-jsx@5.1.1", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" } }, "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw=="],
|
||||||
|
|
||||||
|
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
|
|
||||||
|
"text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="],
|
||||||
|
|
||||||
|
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||||
|
|
||||||
|
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
|
||||||
|
|
||||||
|
"ts-api-utils": ["ts-api-utils@1.4.3", "", { "peerDependencies": { "typescript": ">=4.2.0" } }, "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw=="],
|
||||||
|
|
||||||
|
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||||
|
|
||||||
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
|
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
|
||||||
|
|
||||||
|
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
|
||||||
|
|
||||||
|
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="],
|
||||||
|
|
||||||
|
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="],
|
||||||
|
|
||||||
|
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="],
|
||||||
|
|
||||||
|
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||||
|
|
||||||
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
|
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
|
||||||
|
|
||||||
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"watchpack": ["watchpack@2.4.0", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg=="],
|
||||||
|
|
||||||
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
|
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||||
|
|
||||||
|
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="],
|
||||||
|
|
||||||
|
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||||
|
|
||||||
|
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||||
|
|
||||||
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||||
|
|
||||||
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.3", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||||
|
|
||||||
|
"eslint-plugin-react/resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||||
|
|
||||||
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
|
"is-bun-module/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
||||||
|
|
||||||
|
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||||
|
|
||||||
|
"rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
5
webui/next-env.d.ts
vendored
Normal file
5
webui/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
27
webui/next.config.js
Normal file
27
webui/next.config.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
webpack: (config) => {
|
||||||
|
// Handle WASM files
|
||||||
|
config.experiments = {
|
||||||
|
...config.experiments,
|
||||||
|
asyncWebAssembly: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add rule for .wasm files
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.wasm$/,
|
||||||
|
type: 'asset/resource',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow importing from parent directories (for rust workspace)
|
||||||
|
config.resolve.fallback = {
|
||||||
|
...config.resolve.fallback,
|
||||||
|
fs: false,
|
||||||
|
path: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
31
webui/package.json
Normal file
31
webui/package.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "rhai-playground-next",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@xterm/xterm": "^5.5.0",
|
||||||
|
"clsx": "^2.0.0",
|
||||||
|
"codemirror": "^5.65.1",
|
||||||
|
"lucide-react": "^0.548.0",
|
||||||
|
"next": "14.0.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/codemirror": "0.0.96",
|
||||||
|
"@types/node": "^20.8.0",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"eslint": "^8.51.0",
|
||||||
|
"eslint-config-next": "14.0.0",
|
||||||
|
"typescript": "^5.2.0"
|
||||||
|
},
|
||||||
|
"packageManager": "bun@1.0.0"
|
||||||
|
}
|
||||||
55
webui/src/app/api/get-script/route.ts
Normal file
55
webui/src/app/api/get-script/route.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { CONFIG } from "@/lib/config";
|
||||||
|
|
||||||
|
// Force dynamic rendering for this API route
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = request.nextUrl;
|
||||||
|
const name = searchParams.get("name");
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Script name is required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate filename (same validation as save)
|
||||||
|
if (!CONFIG.FILENAME_REGEX.test(name)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid script name" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDir = CONFIG.SAVE_DIRECTORY;
|
||||||
|
const filename = `${name}.rhai`;
|
||||||
|
const filepath = join(saveDir, filename);
|
||||||
|
|
||||||
|
if (!existsSync(filepath)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Script "${name}" not found` },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = await readFile(filepath, "utf8");
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
name,
|
||||||
|
filename,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Get script error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to read script" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
webui/src/app/api/list-scripts/route.ts
Normal file
40
webui/src/app/api/list-scripts/route.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { readdir } from "fs/promises";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { CONFIG } from "@/lib/config";
|
||||||
|
|
||||||
|
// Force dynamic rendering for this API route
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Cache-Control": "no-store, no-cache, must-revalidate, proxy-revalidate",
|
||||||
|
Pragma: "no-cache",
|
||||||
|
Expires: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET(_request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const saveDir = CONFIG.SAVE_DIRECTORY;
|
||||||
|
|
||||||
|
// If save directory doesn't exist, return empty array
|
||||||
|
if (!existsSync(saveDir)) {
|
||||||
|
return NextResponse.json({ scripts: [] }, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read directory and filter for .rhai files
|
||||||
|
const files = await readdir(saveDir);
|
||||||
|
|
||||||
|
const scripts = files
|
||||||
|
.filter((file) => file.endsWith(".rhai"))
|
||||||
|
.map((file) => file.replace(".rhai", ""))
|
||||||
|
.sort(); // Sort alphabetically
|
||||||
|
|
||||||
|
return NextResponse.json({ scripts }, { headers });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("List scripts error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to list scripts" },
|
||||||
|
{ status: 500, headers }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
webui/src/app/api/save-script/route.ts
Normal file
92
webui/src/app/api/save-script/route.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { writeFile, mkdir } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { CONFIG } from "@/lib/config";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
// Check if saving is enabled
|
||||||
|
if (!CONFIG.ENABLE_SAVE) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Script saving is disabled" },
|
||||||
|
{ status: 403 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, content, secret } = await request.json();
|
||||||
|
|
||||||
|
if (secret !== CONFIG.SAVE_SECRET) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Invalid secret" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || !content) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Name and content are required" },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length > CONFIG.MAX_FILENAME_LENGTH) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: `Filename must be ${CONFIG.MAX_FILENAME_LENGTH} characters or less`,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content.length > CONFIG.MAX_FILE_SIZE) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: `File is too large`,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CONFIG.FILENAME_REGEX.test(name)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Filename can only contain alphanumerics, underscores, spaces, and hyphens",
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure save directory exists
|
||||||
|
const saveDir = CONFIG.SAVE_DIRECTORY;
|
||||||
|
if (!existsSync(saveDir)) {
|
||||||
|
await mkdir(saveDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if file already exists
|
||||||
|
const filename = `${name}.rhai`;
|
||||||
|
const filepath = join(saveDir, filename);
|
||||||
|
|
||||||
|
if (existsSync(filepath)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `A script named "${name}" already exists` },
|
||||||
|
{ status: 409 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the file
|
||||||
|
await writeFile(filepath, content, "utf8");
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Script saved as ${filename}`,
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Save script error:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Failed to save script" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
webui/src/app/layout.tsx
Normal file
24
webui/src/app/layout.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import "@/styles/globals.css";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Minimax",
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const viewport = {
|
||||||
|
width: "device-width",
|
||||||
|
initialScale: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
webui/src/app/page.tsx
Normal file
77
webui/src/app/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { loadAllWasm } from "@/utils/wasmLoader";
|
||||||
|
|
||||||
|
const Playground = dynamic(() => import("@/components/Playground"), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
padding: "8px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Loading WASM...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
const [isWasmLoaded, setIsWasmLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
|
||||||
|
// Load all WASM modules
|
||||||
|
loadAllWasm()
|
||||||
|
.then(() => {
|
||||||
|
setIsWasmLoaded(true);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load WASM modules:', error);
|
||||||
|
// Still allow the app to load, but WASM features may not work
|
||||||
|
setIsWasmLoaded(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isClient || !isWasmLoaded) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="loading"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
padding: "8px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
Loading WASM...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Playground />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
154
webui/src/components/Editor.tsx
Normal file
154
webui/src/components/Editor.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []); // DO NOT FILL ARRAY - intentionally empty to prevent re-initialization
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
);
|
||||||
|
});
|
||||||
776
webui/src/components/Playground.tsx
Normal file
776
webui/src/components/Playground.tsx
Normal file
@@ -0,0 +1,776 @@
|
|||||||
|
"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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Playground() {
|
||||||
|
const [isScriptRunning, setIsScriptRunning] = useState(false);
|
||||||
|
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||||
|
const [fontSize, setFontSize] = useState(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem("playground-fontSize");
|
||||||
|
return saved ? parseInt(saved, 10) : 14;
|
||||||
|
}
|
||||||
|
return 14;
|
||||||
|
});
|
||||||
|
const [bulkRounds, setBulkRounds] = useState(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem("playground-bulkRounds");
|
||||||
|
return saved ? parseInt(saved, 10) : 1000;
|
||||||
|
}
|
||||||
|
return 1000;
|
||||||
|
});
|
||||||
|
const [selectedAgent, setSelectedAgent] = useState(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem("playground-selectedAgent");
|
||||||
|
return saved || "Self";
|
||||||
|
}
|
||||||
|
return "Self";
|
||||||
|
});
|
||||||
|
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||||
|
const [scriptName, setScriptName] = useState("");
|
||||||
|
const [saveSecret, setSaveSecret] = useState("");
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [availableAgents, setAvailableAgents] = useState<string[]>([]);
|
||||||
|
const [savedScripts, setSavedScripts] = useState<Record<string, string>>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
const resultRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const terminalRef = useRef<TerminalRef>(null);
|
||||||
|
|
||||||
|
const runDisabled = isScriptRunning || !isEditorReady;
|
||||||
|
const stopDisabled = !isScriptRunning;
|
||||||
|
|
||||||
|
// Fetch saved scripts and update available agents
|
||||||
|
const loadSavedScripts = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/list-scripts");
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const scripts = data.scripts || [];
|
||||||
|
const scriptContents: Record<string, string> = {};
|
||||||
|
|
||||||
|
// Fetch content for each saved script
|
||||||
|
await Promise.all(
|
||||||
|
scripts.map(async (scriptName: string) => {
|
||||||
|
try {
|
||||||
|
const scriptResponse = await fetch(
|
||||||
|
`/api/get-script?name=${encodeURIComponent(
|
||||||
|
scriptName
|
||||||
|
)}`
|
||||||
|
);
|
||||||
|
const scriptData = await scriptResponse.json();
|
||||||
|
|
||||||
|
if (scriptResponse.ok) {
|
||||||
|
scriptContents[scriptName] = scriptData.content;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to load script ${scriptName}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setSavedScripts(scriptContents);
|
||||||
|
|
||||||
|
// Combine hardcoded agents with saved scripts, ensuring Self and Random are first
|
||||||
|
const combinedAgents = [
|
||||||
|
"Self",
|
||||||
|
...Object.keys(AGENTS).filter((key) => key !== "Self"),
|
||||||
|
...scripts,
|
||||||
|
];
|
||||||
|
|
||||||
|
setAvailableAgents(combinedAgents);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load saved scripts:", error);
|
||||||
|
// Fallback to hardcoded agents only
|
||||||
|
setAvailableAgents(Object.keys(AGENTS));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load saved scripts on component mount
|
||||||
|
useEffect(() => {
|
||||||
|
loadSavedScripts();
|
||||||
|
}, [loadSavedScripts]);
|
||||||
|
|
||||||
|
// Save font size to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem("playground-fontSize", fontSize.toString());
|
||||||
|
}
|
||||||
|
}, [fontSize]);
|
||||||
|
|
||||||
|
// Save bulk rounds to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem(
|
||||||
|
"playground-bulkRounds",
|
||||||
|
bulkRounds.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [bulkRounds]);
|
||||||
|
|
||||||
|
// Save selected agent to localStorage
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem("playground-selectedAgent", selectedAgent);
|
||||||
|
}
|
||||||
|
}, [selectedAgent]);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
// Get script content from either hardcoded agents or saved scripts
|
||||||
|
const hardcodedAgent = AGENTS[selectedAgent as keyof typeof AGENTS];
|
||||||
|
const savedScript = savedScripts[selectedAgent];
|
||||||
|
|
||||||
|
const agentScript = hardcodedAgent || savedScript;
|
||||||
|
const blueScript =
|
||||||
|
agentScript || (editorRef.current?.getValue() ?? "");
|
||||||
|
const redScript = editorRef.current?.getValue() ?? "";
|
||||||
|
const opponentName = agentScript ? 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, savedScripts]);
|
||||||
|
|
||||||
|
const stopScriptHandler = useCallback(() => {
|
||||||
|
stopScript();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveScript = useCallback(async () => {
|
||||||
|
if (!scriptName.trim()) {
|
||||||
|
alert("Please enter a script name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!saveSecret.trim()) {
|
||||||
|
alert("Please enter a secret");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!editorRef.current) {
|
||||||
|
alert("No script content to save");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/save-script", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: scriptName.trim(),
|
||||||
|
content: editorRef.current.getValue(),
|
||||||
|
secret: saveSecret.trim(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert(result.message);
|
||||||
|
setScriptName("");
|
||||||
|
// Reload saved scripts to include the new one
|
||||||
|
loadSavedScripts();
|
||||||
|
} else {
|
||||||
|
alert(result.error || "Failed to save script");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Save error:", error);
|
||||||
|
alert("Network error: Failed to save script");
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsSaving(false);
|
||||||
|
}, [scriptName, saveSecret, loadSavedScripts]);
|
||||||
|
|
||||||
|
const copyScriptToEditor = useCallback(() => {
|
||||||
|
if (!selectedAgent || selectedAgent === "Self") {
|
||||||
|
alert("Please select a script to copy to the editor");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the script content
|
||||||
|
const hardcodedAgent = AGENTS[selectedAgent as keyof typeof AGENTS];
|
||||||
|
const savedScript = savedScripts[selectedAgent];
|
||||||
|
const scriptContent = hardcodedAgent || savedScript;
|
||||||
|
|
||||||
|
if (!scriptContent) {
|
||||||
|
alert("No script content available for the selected agent");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scriptContent.trim().startsWith("// SECRET")) {
|
||||||
|
alert("This script is hidden :)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn user about losing current content
|
||||||
|
const confirmed = confirm(
|
||||||
|
`This will replace your current script with "${selectedAgent}". Your current work will be lost. Continue?`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed && editorRef.current) {
|
||||||
|
editorRef.current.setValue(scriptContent);
|
||||||
|
}
|
||||||
|
}, [selectedAgent, savedScripts]);
|
||||||
|
|
||||||
|
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={availableAgents}
|
||||||
|
selectedAgent={selectedAgent}
|
||||||
|
onSelect={setSelectedAgent}
|
||||||
|
placeholder="Select an agent..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.configField}>
|
||||||
|
<Button
|
||||||
|
variant="info"
|
||||||
|
onClick={copyScriptToEditor}
|
||||||
|
disabled={
|
||||||
|
!selectedAgent ||
|
||||||
|
selectedAgent === "Self"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Copy to Editor
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.saveSection}>
|
||||||
|
<div className={styles.configField}>
|
||||||
|
<label>Script name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className={styles.saveInput}
|
||||||
|
placeholder="Enter script name..."
|
||||||
|
value={scriptName}
|
||||||
|
onChange={(e) =>
|
||||||
|
setScriptName(
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
maxLength={32}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.configField}>
|
||||||
|
<label>Secret</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
className={styles.saveInput}
|
||||||
|
placeholder="Enter secret..."
|
||||||
|
value={saveSecret}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSaveSecret(
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.configField}>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={saveScript}
|
||||||
|
loading={isSaving}
|
||||||
|
disabled={isSaving}
|
||||||
|
>
|
||||||
|
Save Script
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
iconLeft="help-circle"
|
||||||
|
onClick={() => setIsHelpOpen(true)}
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
<div className={styles.leftPanel}>
|
||||||
|
<Editor
|
||||||
|
ref={editorRef}
|
||||||
|
initialValue={initialCode}
|
||||||
|
onChange={() => {}}
|
||||||
|
onReady={() => setIsEditorReady(true)}
|
||||||
|
fontSize={fontSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.rightPanel}>
|
||||||
|
<div className={styles.terminalPanel}>
|
||||||
|
<div className={styles.panelHeader}>Terminal</div>
|
||||||
|
<div className={styles.terminalContainer}>
|
||||||
|
<Terminal
|
||||||
|
ref={terminalRef}
|
||||||
|
onData={sendDataToScript}
|
||||||
|
fontSize={fontSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.outputPanel}>
|
||||||
|
<div className={styles.panelHeader}>Output</div>
|
||||||
|
<textarea
|
||||||
|
ref={resultRef}
|
||||||
|
className={styles.result}
|
||||||
|
readOnly
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Use print() to produce output"
|
||||||
|
style={{ fontSize: `${fontSize}px` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SidePanel isOpen={isHelpOpen} onClose={() => setIsHelpOpen(false)}>
|
||||||
|
<h2>Game Rules</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This game is played in two rounds, on an empty eleven-space
|
||||||
|
board. The first round is played on {"Red's"} board, the
|
||||||
|
second is played on {"Blue's"}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
On {"Red's"} board, {"Red's"} goal is to maximize the value
|
||||||
|
of the expression.
|
||||||
|
{" Blue's"} goal is to minimize it. Players take turns
|
||||||
|
placing the fourteen symbols <code>0123456789+-×÷</code>
|
||||||
|
on the board, with the maximizing player taking the first
|
||||||
|
move.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A {"board's"} syntax must always be valid, and the following
|
||||||
|
rules are enforced:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Each symbol may only be used once</li>
|
||||||
|
<li>
|
||||||
|
The binary operators <code>+-×÷</code> may not be next
|
||||||
|
to one another, and may not be at the end slots.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
The unary operator <code>-</code> (negative) must have a
|
||||||
|
number as an argument. Therefore, it cannot be left of
|
||||||
|
an operator (like <code>-×</code>), and it may not be in
|
||||||
|
the rightmost slot.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Unary <code>+</code> may not be used.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{" "}
|
||||||
|
<code>0</code> may not follow <code>÷</code>. This
|
||||||
|
prevents most cases of zero-division, but{" "}
|
||||||
|
{"isn't perfect"}.<code>÷-0</code> will result in an
|
||||||
|
invalid board (causing a draw), and <code>÷0_+</code> is
|
||||||
|
forbidden despite being valid syntax once the empty slot
|
||||||
|
is filled. This is done to simplyify game logic, and
|
||||||
|
might be improved later.
|
||||||
|
</li>
|
||||||
|
<li>Division by zero results in a draw.</li>
|
||||||
|
<li>
|
||||||
|
An incomplete board with no valid moves results in a
|
||||||
|
draw.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>How to Play</h2>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
Click <strong>Run</strong> to start a single game. Play
|
||||||
|
against your agent in the terminal. Use your arrow keys
|
||||||
|
(up, down, left, right) to select a symbol. Use enter or
|
||||||
|
space to make a move.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Click <strong>Bulk Run</strong> to collect statistics
|
||||||
|
from a many games.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Overview</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>step_min()</code> is called once per turn with the{" "}
|
||||||
|
{"board's"} current state. This function must return an{" "}
|
||||||
|
<code>Action</code> that aims to minimize the total
|
||||||
|
value of the board.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>step_max()</code> is just like{" "}
|
||||||
|
<code>step_min</code>, but should aim to maximize the
|
||||||
|
value of the board.{" "}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Agent code may not be edited between games. Start a new
|
||||||
|
game to use new code.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If your agent takes more than 5 seconds to compute a
|
||||||
|
move, the script will exit with an error.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Rhai basics</h2>
|
||||||
|
<p>
|
||||||
|
Agents are written in <a href="https://rhai.rs">Rhai</a>, a
|
||||||
|
wonderful embedded scripting language powered by Rust. Basic
|
||||||
|
language features are outlined below.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
All statements must be followed by a <code>;</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Use <code>return</code> to return a value from a
|
||||||
|
function.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>print(anything)</code> - Prints to the output
|
||||||
|
panel. Prefer this over <code>debug</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>debug(anything)</code> - Prints to the output
|
||||||
|
panel. Includes extra debug info.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>()</code> is the {'"none"'} type, returned by some
|
||||||
|
methods above.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>for i in 0..5 {"{}"}</code> will iterate five
|
||||||
|
times, with <code>i = 0, 1, 2, 3, 4</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>for i in 0..=5 {"{}"}</code> will iterate six
|
||||||
|
times, with <code>i = 0, 1, 2, 3, 4, 5</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>let a = [];</code> initializes an empty array.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>a.push(value)</code> adds a value to the end of an
|
||||||
|
array
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>a.pop()</code> removes a value from the end of an
|
||||||
|
array and returns it
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>a[0]</code> returns the first item of an array
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>a[1]</code> returns the second item of an array
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Refer to{" "}
|
||||||
|
<a href="https://rhai.rs/book/language/values-and-types.html">
|
||||||
|
the Rhai book
|
||||||
|
</a>{" "}
|
||||||
|
for more details.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Notable Functions</h2>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>Action(symbol, position)</code> - Creates a new
|
||||||
|
action that places <code>symbol</code> at{" "}
|
||||||
|
<code>position</code>. Valid symbols are{" "}
|
||||||
|
<code>01234567890+-/*</code>. Both <code>0</code> and{" "}
|
||||||
|
<code>{'"0"'}</code> are valid symbols.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.can_play(action)</code> - Checks if an
|
||||||
|
action is valid. Returns a boolean.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.size()</code> - Return the total number of
|
||||||
|
spots on this board.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.free_spots()</code> - Count the number of
|
||||||
|
free spots on the board.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.play(action)</code> - Apply the given action
|
||||||
|
on this board. This mutates the <code>board</code>, but
|
||||||
|
does NOT make the move in the game. The only way to
|
||||||
|
commit to an action is to return it from{" "}
|
||||||
|
<code>step_min</code> or <code>step_max</code>. This
|
||||||
|
method lets you compute potential values of a board when
|
||||||
|
used with <code>board.evaluate()</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.ith_free_slot(idx)</code> - Returns the
|
||||||
|
index of the <code>n</code>th free slot on this board.
|
||||||
|
Returns <code>-1</code> if no such slot exists.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.contains(symbol)</code> - Checks if this
|
||||||
|
board contains the given symbol. Returns a boolean.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.evaluate()</code> - Return the value of a
|
||||||
|
board if it can be computed. Returns <code>()</code>{" "}
|
||||||
|
otherwise.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>board.free_spots_idx(action)</code> - Checks if an
|
||||||
|
action is valid. Returns a boolean.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>for i in board {"{ ... }"}</code> - Iterate over
|
||||||
|
all slots on this board. Items are returned as strings,
|
||||||
|
empty slots are the empty string (<code>{'""'}</code>)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>is_op(symbol)</code> - Returns <code>true</code>{" "}
|
||||||
|
if <code>symbol</code> is one of <code>+-*/</code>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<code>rand_symb()</code> - Returns a random symbol
|
||||||
|
(number or operation)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>rand_op()</code> - Returns a random operator
|
||||||
|
symbol (one of <code>+-*/</code>)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>rand_action()</code> - Returns a random{" "}
|
||||||
|
<code>Action</code>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>rand_int(min, max)</code> - Returns a random
|
||||||
|
integer between min and max, including both endpoints.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>rand_bool(probability)</code> - Return{" "}
|
||||||
|
<code>true</code> with the given probability. Otherwise
|
||||||
|
return <code>false</code>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>rand_shuffle(array)</code> - Shuffle the given
|
||||||
|
array
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>for p in permutations(array, 5) {"{}"}</code> -
|
||||||
|
Iterate over all permutations of 5 elements of the given
|
||||||
|
array.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</SidePanel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
135
webui/src/components/Terminal.tsx
Normal file
135
webui/src/components/Terminal.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
forwardRef,
|
||||||
|
RefObject,
|
||||||
|
} from "react";
|
||||||
|
import "@xterm/xterm/css/xterm.css";
|
||||||
|
|
||||||
|
export type TerminalRef = {
|
||||||
|
write: (data: string) => void;
|
||||||
|
clear: () => void;
|
||||||
|
focus: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Terminal = forwardRef<
|
||||||
|
TerminalRef,
|
||||||
|
{
|
||||||
|
onData: (data: String) => void;
|
||||||
|
fontSize?: number;
|
||||||
|
}
|
||||||
|
>(function Terminal(props, ref) {
|
||||||
|
const terminalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const xtermRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
write: (data: string) => {
|
||||||
|
if (xtermRef.current) {
|
||||||
|
xtermRef.current.write(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear: () => {
|
||||||
|
if (xtermRef.current) {
|
||||||
|
xtermRef.current.clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
focus: () => {
|
||||||
|
if (xtermRef.current) {
|
||||||
|
xtermRef.current.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Set to false when this component is unmounted.
|
||||||
|
//
|
||||||
|
// Here's what this flag prevents:
|
||||||
|
// - <Terminal> component mounts
|
||||||
|
// - `useEffect` runs, `mounted = true`
|
||||||
|
// - `init_term()` function begins work
|
||||||
|
// - before init_term() finishes, the user navigates away and <Terminal> to unmounts
|
||||||
|
// - `useEffect` cleans up, `mounted = false`
|
||||||
|
// - `init_term()` ccompletes, and we attempt to set `xtermRef.current`, causing issues
|
||||||
|
let mounted = true;
|
||||||
|
|
||||||
|
init_term(terminalRef, props.onData, () => mounted, props.fontSize)
|
||||||
|
.then((term) => {
|
||||||
|
if (!mounted) return;
|
||||||
|
xtermRef.current = term;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to initialize terminal:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
if (xtermRef.current) {
|
||||||
|
xtermRef.current.dispose();
|
||||||
|
xtermRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [props.onData, props.fontSize]);
|
||||||
|
|
||||||
|
// Update font size when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (xtermRef.current && props.fontSize !== undefined) {
|
||||||
|
xtermRef.current.options.fontSize = props.fontSize;
|
||||||
|
}
|
||||||
|
}, [props.fontSize]);
|
||||||
|
|
||||||
|
return <div ref={terminalRef} style={{ height: "100%", width: "100%" }} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function init_term(
|
||||||
|
ref: RefObject<HTMLDivElement>,
|
||||||
|
|
||||||
|
// Called when the terminal receives data
|
||||||
|
onData: (data: String) => void,
|
||||||
|
isMounted: () => boolean,
|
||||||
|
fontSize?: number
|
||||||
|
) {
|
||||||
|
if (!ref.current) return;
|
||||||
|
|
||||||
|
const { Terminal } = await import("@xterm/xterm");
|
||||||
|
if (!isMounted()) return;
|
||||||
|
|
||||||
|
const term = new Terminal({
|
||||||
|
//"fontFamily": "Fantasque",
|
||||||
|
rows: 24,
|
||||||
|
fontSize: fontSize ?? 18,
|
||||||
|
tabStopWidth: 4,
|
||||||
|
cursorBlink: false,
|
||||||
|
cursorStyle: "block",
|
||||||
|
cursorInactiveStyle: "none",
|
||||||
|
theme: {
|
||||||
|
background: "#1D1F21",
|
||||||
|
foreground: "#F8F8F8",
|
||||||
|
cursor: "#F8F8F2",
|
||||||
|
black: "#282828",
|
||||||
|
blue: "#0087AF",
|
||||||
|
brightBlack: "#555555",
|
||||||
|
brightBlue: "#87DFFF",
|
||||||
|
brightCyan: "#28D1E7",
|
||||||
|
brightGreen: "#A8FF60",
|
||||||
|
brightMagenta: "#985EFF",
|
||||||
|
brightRed: "#FFAA00",
|
||||||
|
brightWhite: "#D0D0D0",
|
||||||
|
brightYellow: "#F1FF52",
|
||||||
|
cyan: "#87DFEB",
|
||||||
|
green: "#B4EC85",
|
||||||
|
magenta: "#BD99FF",
|
||||||
|
red: "#ff2f00ff",
|
||||||
|
white: "#F8F8F8",
|
||||||
|
yellow: "#FFFFB6",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
term.open(ref.current);
|
||||||
|
term.onData(onData);
|
||||||
|
|
||||||
|
console.log("Terminal ready");
|
||||||
|
return term;
|
||||||
|
}
|
||||||
191
webui/src/components/ui/AgentSelector.tsx
Normal file
191
webui/src/components/ui/AgentSelector.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { ChevronDown, Search } from "lucide-react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import styles from "@/styles/AgentSelector.module.css";
|
||||||
|
|
||||||
|
interface AgentSelectorProps {
|
||||||
|
agents: string[];
|
||||||
|
selectedAgent: string;
|
||||||
|
onSelect: (agent: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AgentSelector({
|
||||||
|
agents,
|
||||||
|
selectedAgent,
|
||||||
|
onSelect,
|
||||||
|
placeholder = "Select an agent..."
|
||||||
|
}: AgentSelectorProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [highlightedIndex, setHighlightedIndex] = useState(0);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const filteredAgents = agents.filter(agent =>
|
||||||
|
agent.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && searchRef.current) {
|
||||||
|
searchRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHighlightedIndex(0);
|
||||||
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
updateDropdownPosition();
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case "ArrowDown":
|
||||||
|
e.preventDefault();
|
||||||
|
setHighlightedIndex(prev =>
|
||||||
|
prev < filteredAgents.length - 1 ? prev + 1 : 0
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
e.preventDefault();
|
||||||
|
setHighlightedIndex(prev =>
|
||||||
|
prev > 0 ? prev - 1 : filteredAgents.length - 1
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
e.preventDefault();
|
||||||
|
if (filteredAgents[highlightedIndex]) {
|
||||||
|
onSelect(filteredAgents[highlightedIndex]);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (agent: string) => {
|
||||||
|
onSelect(agent);
|
||||||
|
setIsOpen(false);
|
||||||
|
setSearchTerm("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateDropdownPosition = () => {
|
||||||
|
if (triggerRef.current) {
|
||||||
|
const rect = triggerRef.current.getBoundingClientRect();
|
||||||
|
setDropdownPosition({
|
||||||
|
top: rect.bottom + 2,
|
||||||
|
left: rect.left,
|
||||||
|
width: rect.width
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
updateDropdownPosition();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.agentSelector} ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
className={clsx(styles.trigger, isOpen && styles.triggerOpen)}
|
||||||
|
type="button"
|
||||||
|
onClick={handleOpen}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
>
|
||||||
|
<span className={styles.triggerText}>
|
||||||
|
{selectedAgent || placeholder}
|
||||||
|
</span>
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={clsx(styles.chevron, isOpen && styles.chevronOpen)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
className={styles.dropdown}
|
||||||
|
style={{
|
||||||
|
top: dropdownPosition.top,
|
||||||
|
left: dropdownPosition.left,
|
||||||
|
width: Math.max(dropdownPosition.width, 200)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.searchContainer}>
|
||||||
|
<Search size={16} className={styles.searchIcon} />
|
||||||
|
<input
|
||||||
|
ref={searchRef}
|
||||||
|
type="text"
|
||||||
|
className={styles.searchInput}
|
||||||
|
placeholder="Search agents..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className={styles.agentList} role="listbox">
|
||||||
|
{filteredAgents.length === 0 ? (
|
||||||
|
<li className={styles.noResults}>No agents found</li>
|
||||||
|
) : (
|
||||||
|
filteredAgents.map((agent, index) => (
|
||||||
|
<li
|
||||||
|
key={agent}
|
||||||
|
className={clsx(
|
||||||
|
styles.agentOption,
|
||||||
|
agent === selectedAgent && styles.selected,
|
||||||
|
index === highlightedIndex && styles.highlighted
|
||||||
|
)}
|
||||||
|
onClick={() => handleSelect(agent)}
|
||||||
|
role="option"
|
||||||
|
aria-selected={agent === selectedAgent}
|
||||||
|
>
|
||||||
|
{agent}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
webui/src/components/ui/Button.tsx
Normal file
73
webui/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ButtonHTMLAttributes, ReactNode } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { Play, Square, Loader2 } from "lucide-react";
|
||||||
|
import styles from "@/styles/Button.module.css";
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
play: Play,
|
||||||
|
stop: Square,
|
||||||
|
loading: Loader2,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIcon(iconName: string) {
|
||||||
|
return iconMap[iconName as keyof typeof iconMap];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonProps
|
||||||
|
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "type"> {
|
||||||
|
variant?:
|
||||||
|
| "primary"
|
||||||
|
| "success"
|
||||||
|
| "danger"
|
||||||
|
| "warning"
|
||||||
|
| "info"
|
||||||
|
| "light"
|
||||||
|
| "dark";
|
||||||
|
iconLeft?: string;
|
||||||
|
iconRight?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
type?: "submit" | "reset" | "button";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
variant = "primary",
|
||||||
|
iconLeft,
|
||||||
|
iconRight,
|
||||||
|
loading = false,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
tooltip,
|
||||||
|
type = "button",
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type={type}
|
||||||
|
className={clsx(
|
||||||
|
styles.button,
|
||||||
|
styles[`is-${variant}`],
|
||||||
|
loading && styles.isLoading,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
disabled={disabled || loading}
|
||||||
|
title={tooltip}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{iconLeft && !loading && (() => {
|
||||||
|
const IconComponent = getIcon(iconLeft);
|
||||||
|
return IconComponent ? <IconComponent size={16} /> : null;
|
||||||
|
})()}
|
||||||
|
{loading && <Loader2 size={16} className={styles.spin} />}
|
||||||
|
<span>{children}</span>
|
||||||
|
{iconRight && !loading && (() => {
|
||||||
|
const IconComponent = getIcon(iconRight);
|
||||||
|
return IconComponent ? <IconComponent size={16} /> : null;
|
||||||
|
})()}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
115
webui/src/components/ui/Dropdown.tsx
Normal file
115
webui/src/components/ui/Dropdown.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, useEffect, ReactNode } from "react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { HelpCircle, MoreHorizontal, ChevronDown } from "lucide-react";
|
||||||
|
import styles from "@/styles/Dropdown.module.css";
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
"help-circle": HelpCircle,
|
||||||
|
"dots-horizontal": MoreHorizontal,
|
||||||
|
"menu-down": ChevronDown,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getIcon(iconName: string) {
|
||||||
|
return iconMap[iconName as keyof typeof iconMap];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownItem {
|
||||||
|
text: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownProps {
|
||||||
|
trigger?: string;
|
||||||
|
triggerIcon?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
items?: DropdownItem[];
|
||||||
|
customContent?: ReactNode;
|
||||||
|
align?: "left" | "right";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Dropdown({
|
||||||
|
trigger,
|
||||||
|
triggerIcon,
|
||||||
|
disabled = false,
|
||||||
|
items = [],
|
||||||
|
customContent,
|
||||||
|
align = "left",
|
||||||
|
}: DropdownProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(styles.dropdown, isOpen && styles.isActive)}
|
||||||
|
ref={dropdownRef}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={styles.dropdownTrigger}
|
||||||
|
type="button"
|
||||||
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{triggerIcon && (() => {
|
||||||
|
const IconComponent = getIcon(triggerIcon);
|
||||||
|
return IconComponent ? <IconComponent size={16} /> : null;
|
||||||
|
})()}
|
||||||
|
{trigger && <span>{trigger}</span>}
|
||||||
|
{!triggerIcon && !trigger && <MoreHorizontal size={16} />}
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.dropdownMenu,
|
||||||
|
align === "right" && styles.alignRight,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.dropdownContent}>
|
||||||
|
{customContent ? (
|
||||||
|
<div className={styles.dropdownItem}>
|
||||||
|
{customContent}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
items.map((item, index) => (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href="#"
|
||||||
|
className={styles.dropdownItem}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
item.onClick();
|
||||||
|
setIsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
webui/src/components/ui/SidePanel.tsx
Normal file
57
webui/src/components/ui/SidePanel.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
import styles from "@/styles/SidePanel.module.css";
|
||||||
|
|
||||||
|
interface SidePanelProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SidePanel({ isOpen, onClose, children }: SidePanelProps) {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setIsVisible(true);
|
||||||
|
// Small delay to trigger animation
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
});
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
setIsAnimating(false);
|
||||||
|
// Wait for animation to complete before hiding
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
}, 300); // Match animation duration
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`${styles.overlay} ${isAnimating ? styles.overlayVisible : ""}`}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
<div className={`${styles.sidePanel} ${isAnimating ? styles.sidePanelOpen : ""}`}>
|
||||||
|
<button className={styles.closeButton} onClick={onClose}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<div className={styles.content}>{children}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
webui/src/components/ui/Slider.tsx
Normal file
49
webui/src/components/ui/Slider.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ChangeEvent } from "react";
|
||||||
|
import styles from "@/styles/Slider.module.css";
|
||||||
|
|
||||||
|
interface SliderProps {
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step?: number;
|
||||||
|
onChange: (value: number) => void;
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Slider({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step = 1,
|
||||||
|
onChange,
|
||||||
|
unit = "",
|
||||||
|
}: SliderProps) {
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange(Number(e.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.sliderContainer}>
|
||||||
|
<div className={styles.sliderLabel}>
|
||||||
|
<span>{label}</span>
|
||||||
|
<span className={styles.sliderValue}>
|
||||||
|
{value}
|
||||||
|
{unit}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.slider}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
webui/src/lib/config.ts
Normal file
9
webui/src/lib/config.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const CONFIG = {
|
||||||
|
ENABLE_SAVE: process.env.ENABLE_SAVE === "true" || true,
|
||||||
|
SAVE_SECRET: process.env.SAVE_SECRET || "save",
|
||||||
|
|
||||||
|
SAVE_DIRECTORY: process.env.SAVE_DIRECTORY || "./data/scripts",
|
||||||
|
MAX_FILENAME_LENGTH: parseInt(process.env.MAX_FILENAME_LENGTH || "32"),
|
||||||
|
MAX_FILE_SIZE: parseInt(process.env.MAX_FILE_SIZE || "1048576"),
|
||||||
|
FILENAME_REGEX: /^[a-zA-Z0-9_\s-]+$/,
|
||||||
|
} as const;
|
||||||
106
webui/src/lib/runner.ts
Normal file
106
webui/src/lib/runner.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
let worker: Worker | null = null;
|
||||||
|
|
||||||
|
export function sendDataToScript(data: String) {
|
||||||
|
if (worker) {
|
||||||
|
worker.postMessage({ type: "data", data });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startScript(
|
||||||
|
script: string,
|
||||||
|
appendOutput: (line: string) => void,
|
||||||
|
appendTerminal: (line: string) => void
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (worker) {
|
||||||
|
worker.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
worker = new Worker(new URL("./worker_human.ts", import.meta.url));
|
||||||
|
|
||||||
|
worker.onmessage = (event) => {
|
||||||
|
const { type, line, error } = event.data;
|
||||||
|
|
||||||
|
if (type === "output") {
|
||||||
|
appendOutput(line);
|
||||||
|
} else if (type === "terminal") {
|
||||||
|
appendTerminal(line);
|
||||||
|
} else if (type === "complete") {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
resolve();
|
||||||
|
} else if (type === "error") {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
reject(new Error(error));
|
||||||
|
} else if (type === "stopped") {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage({ type: "run", script });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function startScriptBulk(
|
||||||
|
redScript: string,
|
||||||
|
blueScript: string,
|
||||||
|
opponentName: string,
|
||||||
|
appendOutput: (line: string) => void,
|
||||||
|
appendTerminal: (line: string) => void,
|
||||||
|
rounds: number = 1000
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (worker) {
|
||||||
|
worker.terminate();
|
||||||
|
}
|
||||||
|
|
||||||
|
worker = new Worker(new URL("./worker_bulk.ts", import.meta.url));
|
||||||
|
|
||||||
|
worker.onmessage = (event) => {
|
||||||
|
const { type, line, error } = event.data;
|
||||||
|
|
||||||
|
if (type === "output") {
|
||||||
|
appendOutput(line);
|
||||||
|
} else if (type === "terminal") {
|
||||||
|
appendTerminal(line);
|
||||||
|
} else if (type === "complete") {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
resolve();
|
||||||
|
} else if (type === "error") {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
reject(new Error(error));
|
||||||
|
} else if (type === "stopped") {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
worker?.terminate();
|
||||||
|
worker = null;
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.postMessage({ type: "run", redScript, blueScript, opponentName, rounds });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopScript(): void {
|
||||||
|
if (worker) {
|
||||||
|
worker.postMessage({ type: "stop" });
|
||||||
|
}
|
||||||
|
}
|
||||||
119
webui/src/lib/worker_bulk.ts
Normal file
119
webui/src/lib/worker_bulk.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import init, { MinMaxGame } from "../wasm/runner";
|
||||||
|
|
||||||
|
let wasmReady = false;
|
||||||
|
let wasmInitPromise: Promise<void> | null = null;
|
||||||
|
let currentGame: MinMaxGame | null = null;
|
||||||
|
|
||||||
|
async function initWasm(): Promise<void> {
|
||||||
|
if (wasmReady) return;
|
||||||
|
|
||||||
|
if (wasmInitPromise) {
|
||||||
|
return wasmInitPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasmInitPromise = (async () => {
|
||||||
|
await init();
|
||||||
|
wasmReady = true;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return wasmInitPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = async (event) => {
|
||||||
|
const { type, ...event_data } = event.data;
|
||||||
|
|
||||||
|
if (type === "init") {
|
||||||
|
try {
|
||||||
|
await initWasm();
|
||||||
|
self.postMessage({ type: "ready" });
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({ type: "error", error: String(error) });
|
||||||
|
}
|
||||||
|
} else if (type === "run") {
|
||||||
|
try {
|
||||||
|
await initWasm();
|
||||||
|
|
||||||
|
self.postMessage({
|
||||||
|
type: "output",
|
||||||
|
line: "Output is disabled during bulk runs.",
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendTerminal = (line: string) => {
|
||||||
|
self.postMessage({ type: "terminal", line });
|
||||||
|
};
|
||||||
|
|
||||||
|
const n_rounds = event_data.rounds || 1000;
|
||||||
|
const start = performance.now();
|
||||||
|
let red_wins = 0;
|
||||||
|
let blue_wins = 0;
|
||||||
|
let draw_score = 0;
|
||||||
|
let draw_invalid = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < n_rounds; i++) {
|
||||||
|
appendTerminal(`\n\r`);
|
||||||
|
appendTerminal(`============\n\r`);
|
||||||
|
appendTerminal(`= Round ${i + 1}\n\r`);
|
||||||
|
appendTerminal(`============\n\n\r`);
|
||||||
|
|
||||||
|
currentGame = new MinMaxGame(
|
||||||
|
event_data.redScript,
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
|
||||||
|
event_data.blueScript,
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
|
||||||
|
appendTerminal
|
||||||
|
);
|
||||||
|
|
||||||
|
while (currentGame && !currentGame.is_done()) {
|
||||||
|
currentGame.step();
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTerminal("\r\n");
|
||||||
|
|
||||||
|
if (currentGame.is_error()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentGame.red_won() === true) {
|
||||||
|
red_wins += 1;
|
||||||
|
} else if (currentGame.blue_won() === true) {
|
||||||
|
blue_wins += 1;
|
||||||
|
} else if (currentGame.is_draw_invalid() === true) {
|
||||||
|
draw_invalid += 1;
|
||||||
|
} else if (currentGame.is_draw_score() === true) {
|
||||||
|
draw_score += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = Math.round((performance.now() - start) / 100) / 10;
|
||||||
|
const r_winrate = Math.round((red_wins / n_rounds) * 1000) / 10;
|
||||||
|
const b_winrate = Math.round((blue_wins / n_rounds) * 1000) / 10;
|
||||||
|
|
||||||
|
const opponentName = event_data.opponentName || "Unknown";
|
||||||
|
|
||||||
|
appendTerminal("\r\n");
|
||||||
|
appendTerminal(`Ran ${n_rounds} rounds in ${elapsed}s\r\n`);
|
||||||
|
appendTerminal(
|
||||||
|
`Red won: ${red_wins} (${r_winrate}%) (script)\r\n`
|
||||||
|
);
|
||||||
|
appendTerminal(
|
||||||
|
`Blue won: ${blue_wins} (${b_winrate}%) (${opponentName})\r\n`
|
||||||
|
);
|
||||||
|
appendTerminal("\r\n");
|
||||||
|
appendTerminal(`Draws: ${draw_score}\r\n`);
|
||||||
|
appendTerminal(`Invalid: ${draw_invalid}\r\n`);
|
||||||
|
|
||||||
|
if (currentGame) {
|
||||||
|
self.postMessage({ type: "complete" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({ type: "error", error: String(error) });
|
||||||
|
}
|
||||||
|
} else if (type === "stop") {
|
||||||
|
currentGame = null;
|
||||||
|
self.postMessage({ type: "stopped" });
|
||||||
|
}
|
||||||
|
};
|
||||||
75
webui/src/lib/worker_human.ts
Normal file
75
webui/src/lib/worker_human.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import init, { MinMaxGameHuman } from "../wasm/runner";
|
||||||
|
|
||||||
|
let wasmReady = false;
|
||||||
|
let wasmInitPromise: Promise<void> | null = null;
|
||||||
|
let currentGame: MinMaxGameHuman | 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) {
|
||||||
|
try {
|
||||||
|
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" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({ type: "error", error: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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 MinMaxGameHuman(
|
||||||
|
event_data.script,
|
||||||
|
appendOutput,
|
||||||
|
appendOutput,
|
||||||
|
appendTerminal
|
||||||
|
);
|
||||||
|
|
||||||
|
currentGame.print_start();
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({ type: "error", error: String(error) });
|
||||||
|
}
|
||||||
|
} else if (type === "stop") {
|
||||||
|
currentGame = null;
|
||||||
|
self.postMessage({ type: "stopped" });
|
||||||
|
}
|
||||||
|
};
|
||||||
143
webui/src/styles/AgentSelector.module.css
Normal file
143
webui/src/styles/AgentSelector.module.css
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
.agentSelector {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger:hover {
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger:focus,
|
||||||
|
.triggerOpen {
|
||||||
|
border-color: #007acc;
|
||||||
|
box-shadow: 0 0 3px rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.triggerText {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevronOpen {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
background: #2d2d30;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
margin-top: 2px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchContainer {
|
||||||
|
position: relative;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchIcon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px 6px 32px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput:focus {
|
||||||
|
border-color: #007acc;
|
||||||
|
box-shadow: 0 0 3px rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchInput::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agentList {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agentOption {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agentOption:hover,
|
||||||
|
.highlighted {
|
||||||
|
background: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agentOption:active {
|
||||||
|
background: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
background: #007acc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected:hover,
|
||||||
|
.selected.highlighted {
|
||||||
|
background: #0088dd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.noResults {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
131
webui/src/styles/Button.module.css
Normal file
131
webui/src/styles/Button.module.css
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
.button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
background: #3a3a3a;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isLoading {
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button types */
|
||||||
|
.is-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-primary:hover:not(:disabled) {
|
||||||
|
background: #0056b3;
|
||||||
|
border-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-success {
|
||||||
|
background: #28a745;
|
||||||
|
color: white;
|
||||||
|
border-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-success:hover:not(:disabled) {
|
||||||
|
background: #1e7e34;
|
||||||
|
border-color: #1e7e34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-danger {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-danger:hover:not(:disabled) {
|
||||||
|
background: #c82333;
|
||||||
|
border-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-warning {
|
||||||
|
background: #ffc107;
|
||||||
|
color: #212529;
|
||||||
|
border-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-warning:hover:not(:disabled) {
|
||||||
|
background: #e0a800;
|
||||||
|
border-color: #e0a800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-info {
|
||||||
|
background: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
border-color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-info:hover:not(:disabled) {
|
||||||
|
background: #138496;
|
||||||
|
border-color: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-light {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #212529;
|
||||||
|
border-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-light:hover:not(:disabled) {
|
||||||
|
background: #e2e6ea;
|
||||||
|
border-color: #e2e6ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-dark {
|
||||||
|
background: #343a40;
|
||||||
|
color: white;
|
||||||
|
border-color: #343a40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-dark:hover:not(:disabled) {
|
||||||
|
background: #23272b;
|
||||||
|
border-color: #23272b;
|
||||||
|
}
|
||||||
100
webui/src/styles/Dropdown.module.css
Normal file
100
webui/src/styles/Dropdown.module.css
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownTrigger {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownTrigger:hover:not(:disabled) {
|
||||||
|
background: #3a3a3a;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownTrigger:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isActive .dropdownTrigger {
|
||||||
|
background: #3a3a3a;
|
||||||
|
border-color: #4fc3f7;
|
||||||
|
box-shadow: 0 0 0 2px rgba(79, 195, 247, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 160px;
|
||||||
|
max-width: 90vw;
|
||||||
|
margin-top: 4px;
|
||||||
|
background: #2d2d2d;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
animation: dropdownFadeIn 0.15s ease-out;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu.alignRight {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropdownFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownContent {
|
||||||
|
padding: 4px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 16px;
|
||||||
|
clear: both;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #e0e0e0;
|
||||||
|
text-align: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: normal;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem:hover,
|
||||||
|
.dropdownItem:focus {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
color: #ffffff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
38
webui/src/styles/Editor.module.css
Normal file
38
webui/src/styles/Editor.module.css
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
.editorContainer {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
resize: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced CodeMirror styles from playground */
|
||||||
|
.editorContainer :global(.CodeMirror) {
|
||||||
|
border: none;
|
||||||
|
height: 100% !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-height: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer :global(.rhai-error) {
|
||||||
|
text-decoration: underline wavy red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer :global(.cm-matchhighlight) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editorContainer :global(.CodeMirror-selection-highlight-scrollbar) {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
182
webui/src/styles/Playground.module.css
Normal file
182
webui/src/styles/Playground.module.css
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveInput {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: #3c3c3c;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveInput:focus {
|
||||||
|
border-color: #007acc;
|
||||||
|
box-shadow: 0 0 3px rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveInput::placeholder {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.saveSection {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpPanel {
|
||||||
|
padding: 16px;
|
||||||
|
width: 300px;
|
||||||
|
max-width: 90vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpPanel h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpPanel p {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #cccccc;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpPanel a {
|
||||||
|
color: #4fc3f7;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpPanel a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
border-top: 1px solid #444;
|
||||||
|
padding-top: 12px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
166
webui/src/styles/SidePanel.module.css
Normal file
166
webui/src/styles/SidePanel.module.css
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
.overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
z-index: 1000;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlayVisible {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidePanel {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 50%;
|
||||||
|
background: #1e1e1e;
|
||||||
|
z-index: 1001;
|
||||||
|
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidePanelOpen {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 12px;
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s ease, background 0.2s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px 32px;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 32px 0 16px 0;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 24px 0 12px 0;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content ul,
|
||||||
|
.content ol {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content li {
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content code {
|
||||||
|
background: #2d2d30;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #4fc3f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content pre {
|
||||||
|
background: #2d2d30;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content pre code {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content a {
|
||||||
|
color: #4fc3f7;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content kbd {
|
||||||
|
background: #2d2d30;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidePanel {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
webui/src/styles/Slider.module.css
Normal file
57
webui/src/styles/Slider.module.css
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
.sliderContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderLabel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sliderValue {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #3a3a3a;
|
||||||
|
outline: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4fc3f7;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-webkit-slider-thumb:hover {
|
||||||
|
background: #6dd1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #4fc3f7;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider::-moz-range-thumb:hover {
|
||||||
|
background: #6dd1ff;
|
||||||
|
}
|
||||||
119
webui/src/styles/globals.css
Normal file
119
webui/src/styles/globals.css
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
/* Global styles based on the original site */
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden !important;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||||
|
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||||
|
sans-serif;
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CodeMirror base styles */
|
||||||
|
.CodeMirror {
|
||||||
|
border: 1px solid #444;
|
||||||
|
height: 100% !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 0.95em;
|
||||||
|
line-height: initial;
|
||||||
|
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror .rhai-error {
|
||||||
|
text-decoration: underline wavy red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror .cm-matchhighlight {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror .CodeMirror-selection-highlight-scrollbar {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Basic button styles */
|
||||||
|
button {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover:not(:disabled) {
|
||||||
|
background: #3a3a3a;
|
||||||
|
border-color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input styles */
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
background: #2d2d2d;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
font-family: "Consolas", "Monaco", "Courier New", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form elements */
|
||||||
|
label {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
display: block;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kbd styling */
|
||||||
|
kbd {
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
10
webui/src/types/codemirror.d.ts
vendored
Normal file
10
webui/src/types/codemirror.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Type declarations for CodeMirror modules
|
||||||
|
declare module "codemirror/mode/javascript/javascript";
|
||||||
|
declare module "codemirror/addon/edit/matchbrackets";
|
||||||
|
declare module "codemirror/addon/edit/closebrackets";
|
||||||
|
declare module "codemirror/addon/selection/active-line";
|
||||||
|
declare module "codemirror/addon/fold/foldcode";
|
||||||
|
declare module "codemirror/addon/fold/foldgutter";
|
||||||
|
declare module "codemirror/addon/fold/brace-fold";
|
||||||
|
declare module "codemirror/lib/codemirror.css";
|
||||||
|
declare module "codemirror/addon/fold/foldgutter.css";
|
||||||
91
webui/src/utils/wasmLoader.ts
Normal file
91
webui/src/utils/wasmLoader.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// WASM loader for Rhai CodeMirror mode
|
||||||
|
import init, {
|
||||||
|
RhaiMode,
|
||||||
|
init_codemirror_pass,
|
||||||
|
} from "@/wasm/rhai-codemirror/rhai_codemirror.js";
|
||||||
|
|
||||||
|
let wasmInitialized = false;
|
||||||
|
let wasmModule: any = null;
|
||||||
|
let wasmLoadPromise: Promise<any> | null = null;
|
||||||
|
|
||||||
|
export const loadRhaiWasm = async () => {
|
||||||
|
if (wasmInitialized) {
|
||||||
|
return wasmModule;
|
||||||
|
}
|
||||||
|
if (wasmLoadPromise) {
|
||||||
|
return wasmLoadPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasmLoadPromise = (async () => {
|
||||||
|
try {
|
||||||
|
// Initialize the WASM module
|
||||||
|
wasmModule = await init();
|
||||||
|
wasmInitialized = true;
|
||||||
|
|
||||||
|
return wasmModule;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load Rhai WASM module:", error);
|
||||||
|
wasmLoadPromise = null; // Reset on error
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return wasmLoadPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initRhaiMode = (CodeMirror: any) => {
|
||||||
|
if (!wasmInitialized || !wasmModule) {
|
||||||
|
throw new Error("WASM module not loaded. Call loadRhaiWasm() first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize CodeMirror Pass for the WASM module
|
||||||
|
init_codemirror_pass(CodeMirror.Pass);
|
||||||
|
|
||||||
|
// Define the Rhai mode using the WASM-based RhaiMode
|
||||||
|
CodeMirror.defineMode("rhai", (config: any) => {
|
||||||
|
return new RhaiMode(config.indentUnit || 4);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Function to preload all WASM modules used by the application
|
||||||
|
export const loadAllWasm = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
await loadRhaiWasm();
|
||||||
|
|
||||||
|
// Load Script Runner WASM by creating and immediately terminating a worker
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("../lib/worker_bulk.ts", import.meta.url)
|
||||||
|
);
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
worker.terminate();
|
||||||
|
reject(new Error("Script runner WASM load timeout"));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
worker.postMessage({ type: "init" });
|
||||||
|
worker.onmessage = (event) => {
|
||||||
|
if (event.data.type === "ready") {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
worker.terminate();
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
worker.terminate();
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("All WASM modules loaded successfully");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load WASM modules:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { RhaiMode };
|
||||||
33
webui/tsconfig.json
Normal file
33
webui/tsconfig.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "es6"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"src/types/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user