Compare commits
3 Commits
3dd397d60b
...
5ce2371dc1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ce2371dc1 | |||
| e77db1f4c5 | |||
| ccd4292ed2 |
@@ -1,90 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build script for all WASM libraries in the MMX repository
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${BLUE}🚀 Building all WASM libraries for MMX${NC}"
|
||||
|
||||
# Set PATH to include cargo
|
||||
export PATH="$HOME/.cargo/bin:$PATH"
|
||||
|
||||
# Check if wasm-pack is installed
|
||||
if ! command -v wasm-pack &> /dev/null; then
|
||||
echo -e "${RED}❌ wasm-pack not found. Please install it first:${NC}"
|
||||
echo "curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Change to rust directory
|
||||
cd rust
|
||||
|
||||
echo -e "${YELLOW}📋 Found WASM crates to build:${NC}"
|
||||
|
||||
# List all WASM crates (crates with crate-type = ["cdylib"])
|
||||
wasm_crates=()
|
||||
for dir in */; do
|
||||
if [[ -f "$dir/Cargo.toml" ]]; then
|
||||
if grep -q 'crate-type.*=.*\["cdylib"\]' "$dir/Cargo.toml"; then
|
||||
wasm_crates+=("${dir%/}")
|
||||
echo -e " • ${dir%/}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#wasm_crates[@]} -eq 0 ]]; then
|
||||
echo -e "${YELLOW}⚠️ No WASM crates found${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Build each WASM crate
|
||||
for crate in "${wasm_crates[@]}"; do
|
||||
echo -e "${BLUE}🔨 Building $crate...${NC}"
|
||||
|
||||
cd "$crate"
|
||||
|
||||
# Determine output directory based on crate name
|
||||
case "$crate" in
|
||||
"rhai-codemirror")
|
||||
output_dir="../../webui/src/wasm/rhai-codemirror"
|
||||
;;
|
||||
*)
|
||||
output_dir="../../webui/src/wasm/$crate"
|
||||
;;
|
||||
esac
|
||||
|
||||
# Build with wasm-pack
|
||||
if wasm-pack build --target web --out-dir "$output_dir"; then
|
||||
echo -e "${GREEN}✅ Successfully built $crate${NC}"
|
||||
else
|
||||
echo -e "${RED}❌ Failed to build $crate${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo -e "${GREEN}🎉 All WASM libraries built successfully!${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}📦 Built libraries:${NC}"
|
||||
for crate in "${wasm_crates[@]}"; do
|
||||
case "$crate" in
|
||||
"rhai-codemirror")
|
||||
output_dir="../webui/src/wasm/rhai-codemirror"
|
||||
;;
|
||||
*)
|
||||
output_dir="../webui/src/wasm/$crate"
|
||||
;;
|
||||
esac
|
||||
echo -e " • $crate → $output_dir"
|
||||
done
|
||||
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
|
||||
@@ -1,3 +1,8 @@
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 's'
|
||||
|
||||
[workspace]
|
||||
members = ["runner", "minimax", "rhai-codemirror"]
|
||||
resolver = "2"
|
||||
@@ -5,7 +10,6 @@ resolver = "2"
|
||||
[workspace.dependencies]
|
||||
minimax = { path = "./minimax" }
|
||||
|
||||
# TODO: update
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
rand = { version = "0.8.5", features = ["alloc", "small_rng"] }
|
||||
anyhow = "1.0.80"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
mod rhai;
|
||||
|
||||
pub use rhai::Rhai;
|
||||
pub use rhai::RhaiAgent;
|
||||
|
||||
use crate::board::{Board, PlayerAction};
|
||||
use anyhow::Result;
|
||||
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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use itertools::{Itertools, Permutations};
|
||||
use itertools::Itertools;
|
||||
use parking_lot::Mutex;
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use rhai::{
|
||||
@@ -8,53 +8,73 @@ use rhai::{
|
||||
BasicMathPackage, BasicStringPackage, LanguageCorePackage, LogicPackage, MoreStringPackage,
|
||||
Package,
|
||||
},
|
||||
CallFnOptions, CustomType, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError,
|
||||
Position, Scope, TypeBuilder, AST,
|
||||
CallFnOptions, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError, Position, Scope,
|
||||
AST,
|
||||
};
|
||||
use std::{sync::Arc, vec::IntoIter};
|
||||
|
||||
use super::Agent;
|
||||
use crate::{
|
||||
board::{Board, PlayerAction},
|
||||
util::Symb,
|
||||
};
|
||||
use crate::game::{Board, PlayerAction, Symb};
|
||||
|
||||
pub struct RhaiPer<T: Clone, I: Iterator<Item = T>> {
|
||||
inner: Arc<Permutations<I>>,
|
||||
//
|
||||
// MARK: WasmTimer
|
||||
//
|
||||
|
||||
// Native impl
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
struct WasmTimer {
|
||||
start: std::time::Instant,
|
||||
}
|
||||
|
||||
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 {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl WasmTimer {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
start: std::time::Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elapsed_ms(&self) -> u128 {
|
||||
self.start.elapsed().as_millis()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone + 'static, I: Clone + Iterator<Item = T> + 'static> CustomType for RhaiPer<T, I> {
|
||||
fn build(mut builder: TypeBuilder<Self>) {
|
||||
builder
|
||||
.with_name("Perutations")
|
||||
.is_iterable()
|
||||
.with_fn("to_string", |_s: &mut Self| "Permutation {}".to_owned())
|
||||
.with_fn("to_debug", |_s: &mut Self| "Permutation {}".to_owned());
|
||||
// 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 Rhai<R: Rng + 'static> {
|
||||
pub struct RhaiAgent<R: Rng + 'static> {
|
||||
#[expect(dead_code)]
|
||||
rng: Arc<Mutex<R>>,
|
||||
|
||||
@@ -64,7 +84,7 @@ pub struct Rhai<R: Rng + 'static> {
|
||||
print_callback: Arc<dyn Fn(&str) + 'static>,
|
||||
}
|
||||
|
||||
impl<R: Rng + 'static> Rhai<R> {
|
||||
impl<R: Rng + 'static> RhaiAgent<R> {
|
||||
pub fn new(
|
||||
script: &str,
|
||||
rng: R,
|
||||
@@ -77,28 +97,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
||||
let engine = {
|
||||
let mut engine = Engine::new_raw();
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn performance() -> Result<web_sys::Performance, wasm_bindgen::JsValue> {
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
let global = web_sys::js_sys::global();
|
||||
let performance = web_sys::js_sys::Reflect::get(&global, &"performance".into())?;
|
||||
performance.dyn_into::<web_sys::Performance>()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let start = {
|
||||
let performance = performance().expect("performance should be available");
|
||||
|
||||
// In milliseconds
|
||||
performance.now()
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let start = {
|
||||
use std::time::Instant;
|
||||
Instant::now()
|
||||
};
|
||||
let start = WasmTimer::new();
|
||||
|
||||
let max_secs: u64 = 5;
|
||||
engine.on_progress(move |ops| {
|
||||
@@ -106,16 +105,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let elapsed_s = {
|
||||
let performance = performance().expect("performance should be available");
|
||||
|
||||
// In milliseconds
|
||||
((performance.now() - start) / 1000.0).round() as u64
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let elapsed_s = { start.elapsed().as_secs() };
|
||||
let elapsed_s = start.elapsed_ms() as u64 / 1000;
|
||||
|
||||
if elapsed_s > max_secs {
|
||||
return Some(
|
||||
@@ -126,7 +116,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
||||
return None;
|
||||
});
|
||||
|
||||
// Do not use FULL, rand functions are not pure
|
||||
// Do not use FULL, rand_* functions are not pure
|
||||
engine.set_optimization_level(OptimizationLevel::Simple);
|
||||
|
||||
engine.disable_symbol("eval");
|
||||
@@ -163,12 +153,16 @@ impl<R: Rng + 'static> Rhai<R> {
|
||||
})
|
||||
.register_fn("rand_bool", {
|
||||
let rng = rng.clone();
|
||||
move |p: f32| rng.lock().gen_bool(p as f64)
|
||||
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)
|
||||
@@ -193,9 +187,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
||||
}
|
||||
};
|
||||
|
||||
let per = RhaiPer {
|
||||
inner: v.into_iter().permutations(size).into(),
|
||||
};
|
||||
let per = helpers::RhaiPer::new(v.into_iter().permutations(size).into());
|
||||
|
||||
Ok(Dynamic::from(per))
|
||||
},
|
||||
@@ -204,7 +196,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
||||
engine
|
||||
.build_type::<Board>()
|
||||
.build_type::<PlayerAction>()
|
||||
.build_type::<RhaiPer<Dynamic, IntoIter<Dynamic>>>();
|
||||
.build_type::<helpers::RhaiPer<Dynamic, IntoIter<Dynamic>>>();
|
||||
engine
|
||||
};
|
||||
|
||||
@@ -225,7 +217,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Rng + 'static> Agent for Rhai<R> {
|
||||
impl<R: Rng + 'static> Agent for RhaiAgent<R> {
|
||||
type ErrorType = EvalAltResult;
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
@@ -262,3 +254,55 @@ impl<R: Rng + 'static> Agent for Rhai<R> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
mod board;
|
||||
mod tree;
|
||||
|
||||
use rand::Rng;
|
||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub use board::Board;
|
||||
pub use tree::TreeElement;
|
||||
|
||||
use crate::util::Symb;
|
||||
use super::{Board, Symb};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PlayerAction {
|
||||
@@ -8,8 +8,7 @@ use rhai::Position;
|
||||
use rhai::TypeBuilder;
|
||||
use std::fmt::{Debug, Display, Write};
|
||||
|
||||
use super::{PlayerAction, TreeElement};
|
||||
use crate::util::Symb;
|
||||
use super::{PlayerAction, Symb, TreeElement};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InterTreeElement {
|
||||
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;
|
||||
@@ -1,10 +1,9 @@
|
||||
use rand::Rng;
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
num::NonZeroU8,
|
||||
};
|
||||
|
||||
use rand::Rng;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Hash)]
|
||||
|
||||
pub enum Symb {
|
||||
@@ -57,6 +56,16 @@ impl Symb {
|
||||
}
|
||||
}
|
||||
|
||||
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('+'),
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod agents;
|
||||
pub mod board;
|
||||
pub mod util;
|
||||
pub mod game;
|
||||
|
||||
@@ -6,11 +6,6 @@ edition = "2021"
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 's'
|
||||
|
||||
[dependencies]
|
||||
rhai = { workspace = true, features = ["internals"] }
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#![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";
|
||||
|
||||
338
rust/runner/src/gamestate.rs
Normal file
338
rust/runner/src/gamestate.rs
Normal file
@@ -0,0 +1,338 @@
|
||||
use minimax::{
|
||||
agents::{Agent, RhaiAgent},
|
||||
game::Board,
|
||||
};
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
use rhai::ParseError;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::ansi;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct GameState {
|
||||
red_agent: RhaiAgent<StdRng>,
|
||||
blue_agent: RhaiAgent<StdRng>,
|
||||
|
||||
board: Board,
|
||||
is_red_turn: bool,
|
||||
is_first_turn: bool,
|
||||
is_error: bool,
|
||||
red_is_maximizer: bool,
|
||||
red_score: Option<f32>,
|
||||
red_won: Option<bool>,
|
||||
|
||||
game_state_callback: Box<dyn Fn(&str) + 'static>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl GameState {
|
||||
#[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<GameState, 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<GameState, 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(GameState {
|
||||
board: Board::new(),
|
||||
is_first_turn: true,
|
||||
is_error: false,
|
||||
red_score: None,
|
||||
is_red_turn: true,
|
||||
red_is_maximizer: true,
|
||||
red_won: None,
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
|
||||
// Play one turn
|
||||
#[wasm_bindgen]
|
||||
pub fn step(&mut self) -> Result<(), String> {
|
||||
if self.is_done() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let action = match (self.is_red_turn, self.red_is_maximizer) {
|
||||
(false, false) => self.blue_agent.step_max(&self.board),
|
||||
(false, true) => self.blue_agent.step_min(&self.board),
|
||||
(true, false) => self.red_agent.step_min(&self.board),
|
||||
(true, true) => self.red_agent.step_max(&self.board),
|
||||
}
|
||||
.map_err(|err| format!("{err}"))?;
|
||||
|
||||
if !self.board.play(
|
||||
action,
|
||||
self.is_red_turn
|
||||
.then_some("Red")
|
||||
.unwrap_or("Blue")
|
||||
.to_owned(),
|
||||
) {
|
||||
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(self.red_agent.name())
|
||||
.unwrap_or(self.blue_agent.name()),
|
||||
action
|
||||
));
|
||||
}
|
||||
|
||||
if self.board.is_full() {
|
||||
self.print_end()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
self.is_red_turn = !self.is_red_turn;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_done(&self) -> bool {
|
||||
(self.board.is_full() && self.red_score.is_some()) || self.is_error
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn red_won(&self) -> Option<bool> {
|
||||
self.red_won
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.is_error
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn print_start(&mut self) {
|
||||
self.print_board("", "");
|
||||
(self.game_state_callback)("\r\n");
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
let score = match score {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
self.is_error = true;
|
||||
return Err(format!("could not compute final score"));
|
||||
}
|
||||
};
|
||||
|
||||
(self.game_state_callback)(&format!(
|
||||
"\r\n{}{} score:{} {:.2}\r\n",
|
||||
self.red_is_maximizer
|
||||
.then_some(ansi::RED)
|
||||
.unwrap_or(ansi::BLUE),
|
||||
self.red_is_maximizer.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.red_is_maximizer = !self.red_is_maximizer;
|
||||
self.board = Board::new();
|
||||
self.is_red_turn = !self.red_is_maximizer;
|
||||
self.is_first_turn = true;
|
||||
self.is_error = false;
|
||||
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.red_won = Some(red_wins);
|
||||
(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(());
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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 =
|
||||
GameState::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 =
|
||||
GameState::new_native(&SCRIPT, |_| {}, |_| {}, &SCRIPT, |_| {}, |_| {}, |_| {}).unwrap();
|
||||
|
||||
while !game.is_done() {
|
||||
println!("{:?}", game.step());
|
||||
println!("{:?}", game.board);
|
||||
}
|
||||
}
|
||||
288
rust/runner/src/gamestatehuman.rs
Normal file
288
rust/runner/src/gamestatehuman.rs
Normal file
@@ -0,0 +1,288 @@
|
||||
use minimax::{
|
||||
agents::{Agent, RhaiAgent},
|
||||
game::Board,
|
||||
};
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
use rhai::ParseError;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{ansi, terminput::TermInput};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct GameStateHuman {
|
||||
/// Red player
|
||||
human: TermInput,
|
||||
|
||||
// Blue player
|
||||
agent: RhaiAgent<StdRng>,
|
||||
|
||||
board: Board,
|
||||
is_red_turn: bool,
|
||||
is_first_turn: bool,
|
||||
is_error: bool,
|
||||
red_score: Option<f32>,
|
||||
|
||||
game_state_callback: Box<dyn Fn(&str) + 'static>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl GameStateHuman {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
max_script: &str,
|
||||
max_print_callback: js_sys::Function,
|
||||
max_debug_callback: js_sys::Function,
|
||||
|
||||
game_state_callback: js_sys::Function,
|
||||
) -> Result<GameStateHuman, String> {
|
||||
Self::new_native(
|
||||
max_script,
|
||||
move |s| {
|
||||
let _ = max_print_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||
},
|
||||
move |s| {
|
||||
let _ = max_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||
},
|
||||
move |s| {
|
||||
let _ = game_state_callback.call1(&JsValue::null(), &JsValue::from_str(s));
|
||||
},
|
||||
)
|
||||
.map_err(|x| format!("Error at {}: {}", x.1, x.0))
|
||||
}
|
||||
|
||||
fn new_native(
|
||||
max_script: &str,
|
||||
max_print_callback: impl Fn(&str) + 'static,
|
||||
max_debug_callback: impl Fn(&str) + 'static,
|
||||
|
||||
game_state_callback: impl Fn(&str) + 'static,
|
||||
) -> Result<Self, ParseError> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let mut seed1 = [0u8; 32];
|
||||
let mut seed2 = [0u8; 32];
|
||||
getrandom::getrandom(&mut seed1).unwrap();
|
||||
getrandom::getrandom(&mut seed2).unwrap();
|
||||
|
||||
Ok(Self {
|
||||
board: Board::new(),
|
||||
is_red_turn: true,
|
||||
is_first_turn: true,
|
||||
is_error: false,
|
||||
red_score: None,
|
||||
|
||||
human: TermInput::new(ansi::RED.to_string()),
|
||||
agent: RhaiAgent::new(
|
||||
max_script,
|
||||
StdRng::from_seed(seed1),
|
||||
max_print_callback,
|
||||
max_debug_callback,
|
||||
)?,
|
||||
|
||||
game_state_callback: Box::new(game_state_callback),
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_done(&self) -> bool {
|
||||
(self.board.is_full() && self.red_score.is_some()) || self.is_error
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.is_error
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn print_start(&mut self) -> Result<(), String> {
|
||||
self.print_board("", "");
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
if !self.is_red_turn {
|
||||
let action = {
|
||||
if self.red_score.is_none() {
|
||||
self.agent.step_min(&self.board)
|
||||
} else {
|
||||
self.agent.step_max(&self.board)
|
||||
}
|
||||
}
|
||||
.map_err(|err| format!("{err}"))?;
|
||||
|
||||
if !self.board.play(action, "Blue") {
|
||||
self.is_error = true;
|
||||
return Err(format!(
|
||||
"{} ({}) made an invalid move {}",
|
||||
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||
self.is_red_turn
|
||||
.then_some("Human")
|
||||
.unwrap_or(self.agent.name()),
|
||||
action
|
||||
));
|
||||
}
|
||||
|
||||
self.print_board(
|
||||
self.is_red_turn.then_some(ansi::RED).unwrap_or(ansi::BLUE),
|
||||
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||
);
|
||||
(self.game_state_callback)("\r\n");
|
||||
self.is_red_turn = true;
|
||||
}
|
||||
|
||||
self.print_ui();
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn print_board(&mut self, color: &str, player: &str) {
|
||||
let board_label = format!("{}{:<6}{}", color, player, ansi::RESET);
|
||||
|
||||
// Print board
|
||||
(self.game_state_callback)(&format!(
|
||||
"\r{}{}{}{}",
|
||||
board_label,
|
||||
if self.is_first_turn { '╓' } else { '║' },
|
||||
self.board.prettyprint(),
|
||||
if self.is_first_turn { '╖' } else { '║' },
|
||||
));
|
||||
self.is_first_turn = false;
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn print_ui(&mut self) {
|
||||
(self.game_state_callback)(
|
||||
&self
|
||||
.human
|
||||
.print_state(&self.board, self.red_score.is_some()),
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn take_input(&mut self, data: String) -> Result<(), String> {
|
||||
self.human.process_input(&self.board, data);
|
||||
self.print_ui();
|
||||
|
||||
if let Some(action) = self.human.pop_action() {
|
||||
if !self.board.play(action, "Red") {
|
||||
self.is_error = true;
|
||||
return Err(format!(
|
||||
"{} ({}) made an invalid move {}",
|
||||
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||
self.is_red_turn
|
||||
.then_some("Human")
|
||||
.unwrap_or(self.agent.name()),
|
||||
action
|
||||
));
|
||||
}
|
||||
self.is_red_turn = false;
|
||||
|
||||
if self.board.is_full() {
|
||||
self.print_end()?;
|
||||
return Ok(());
|
||||
}
|
||||
self.print_board(ansi::RED, "Red");
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
let action = {
|
||||
if self.red_score.is_none() {
|
||||
self.agent.step_min(&self.board)
|
||||
} else {
|
||||
self.agent.step_max(&self.board)
|
||||
}
|
||||
}
|
||||
.map_err(|err| format!("{err}"))?;
|
||||
|
||||
if !self.board.play(action, "Blue") {
|
||||
self.is_error = true;
|
||||
return Err(format!(
|
||||
"{} ({}) made an invalid move {}",
|
||||
self.is_red_turn.then_some("Red").unwrap_or("Blue"),
|
||||
self.is_red_turn
|
||||
.then_some("Human")
|
||||
.unwrap_or(self.agent.name()),
|
||||
action
|
||||
));
|
||||
}
|
||||
self.is_red_turn = true;
|
||||
|
||||
if self.board.is_full() {
|
||||
self.print_end()?;
|
||||
return Ok(());
|
||||
}
|
||||
self.print_board(ansi::BLUE, "Blue");
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
self.print_ui();
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn print_end(&mut self) -> Result<(), String> {
|
||||
let board_label = format!(
|
||||
"{}{:<6}{}",
|
||||
self.is_red_turn.then_some(ansi::BLUE).unwrap_or(ansi::RED),
|
||||
self.is_red_turn.then_some("Blue").unwrap_or("Red"),
|
||||
ansi::RESET
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&format!("\r{}║{}║", board_label, self.board.prettyprint()));
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
(self.game_state_callback)(&format!(
|
||||
"\r{}╙{}╜",
|
||||
" ",
|
||||
" ".repeat(self.board.size())
|
||||
));
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
let score = self.board.evaluate().unwrap();
|
||||
(self.game_state_callback)(&format!(
|
||||
"\r\n{}{} score:{} {:.2}\r\n",
|
||||
self.red_score
|
||||
.is_none()
|
||||
.then_some(ansi::RED)
|
||||
.unwrap_or(ansi::BLUE),
|
||||
self.red_score.is_none().then_some("Red").unwrap_or("Blue"),
|
||||
ansi::RESET,
|
||||
score
|
||||
));
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
match self.red_score {
|
||||
// Start second round
|
||||
None => {
|
||||
let mut seed1 = [0u8; 32];
|
||||
let mut seed2 = [0u8; 32];
|
||||
getrandom::getrandom(&mut seed1).unwrap();
|
||||
getrandom::getrandom(&mut seed2).unwrap();
|
||||
|
||||
self.board = Board::new();
|
||||
self.is_red_turn = false;
|
||||
self.is_first_turn = true;
|
||||
self.is_error = false;
|
||||
self.human = TermInput::new(ansi::RED.to_string());
|
||||
self.red_score = Some(score);
|
||||
|
||||
self.print_start()?;
|
||||
}
|
||||
|
||||
// End game
|
||||
Some(red_score) => {
|
||||
if red_score == score {
|
||||
(self.game_state_callback)(&format!("Tie! Score: {:.2}\r\n", score));
|
||||
} else {
|
||||
let red_wins = red_score > score;
|
||||
(self.game_state_callback)(&format!(
|
||||
"{}{} wins!{}",
|
||||
red_wins.then_some(ansi::RED).unwrap_or(ansi::BLUE),
|
||||
red_wins.then_some("Red").unwrap_or("Blue"),
|
||||
ansi::RESET,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,9 @@
|
||||
use minimax::{
|
||||
agents::{Agent, Rhai},
|
||||
board::Board,
|
||||
};
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
use rhai::ParseError;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::human::HumanInput;
|
||||
|
||||
mod ansi;
|
||||
mod human;
|
||||
mod gamestate;
|
||||
mod gamestatehuman;
|
||||
mod terminput;
|
||||
|
||||
#[global_allocator]
|
||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
@@ -18,723 +12,3 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
||||
pub fn init_panic_hook() {
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: GameState
|
||||
//
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct GameState {
|
||||
red_agent: Rhai<StdRng>,
|
||||
red_name: String,
|
||||
|
||||
blue_agent: Rhai<StdRng>,
|
||||
blue_name: String,
|
||||
|
||||
board: Board,
|
||||
is_red_turn: bool,
|
||||
is_first_turn: bool,
|
||||
is_error: bool,
|
||||
red_is_maximizer: bool,
|
||||
red_score: Option<f32>,
|
||||
red_won: Option<bool>,
|
||||
|
||||
game_state_callback: Box<dyn Fn(&str) + 'static>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl GameState {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
red_script: &str,
|
||||
red_name: &str,
|
||||
red_print_callback: js_sys::Function,
|
||||
red_debug_callback: js_sys::Function,
|
||||
|
||||
blue_script: &str,
|
||||
blue_name: &str,
|
||||
blue_print_callback: js_sys::Function,
|
||||
blue_debug_callback: js_sys::Function,
|
||||
|
||||
game_state_callback: js_sys::Function,
|
||||
) -> Result<GameState, String> {
|
||||
Self::new_native(
|
||||
red_script,
|
||||
red_name,
|
||||
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,
|
||||
blue_name,
|
||||
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_name: &str,
|
||||
red_print_callback: impl Fn(&str) + 'static,
|
||||
red_debug_callback: impl Fn(&str) + 'static,
|
||||
|
||||
blue_script: &str,
|
||||
blue_name: &str,
|
||||
blue_print_callback: impl Fn(&str) + 'static,
|
||||
blue_debug_callback: impl Fn(&str) + 'static,
|
||||
|
||||
game_state_callback: impl Fn(&str) + 'static,
|
||||
) -> Result<GameState, 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(GameState {
|
||||
board: Board::new(),
|
||||
is_first_turn: true,
|
||||
is_error: false,
|
||||
red_score: None,
|
||||
is_red_turn: true,
|
||||
red_is_maximizer: true,
|
||||
red_won: None,
|
||||
|
||||
red_name: red_name.to_owned(),
|
||||
red_agent: Rhai::new(
|
||||
red_script,
|
||||
StdRng::from_seed(seed1),
|
||||
red_print_callback,
|
||||
red_debug_callback,
|
||||
)?,
|
||||
|
||||
blue_name: blue_name.to_owned(),
|
||||
blue_agent: Rhai::new(
|
||||
blue_script,
|
||||
StdRng::from_seed(seed2),
|
||||
blue_print_callback,
|
||||
blue_debug_callback,
|
||||
)?,
|
||||
|
||||
game_state_callback: Box::new(game_state_callback),
|
||||
})
|
||||
}
|
||||
|
||||
// Play one turn
|
||||
#[wasm_bindgen]
|
||||
pub fn step(&mut self) -> Result<(), String> {
|
||||
if self.is_done() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let action = match (self.is_red_turn, self.red_is_maximizer) {
|
||||
(false, false) => self.blue_agent.step_max(&self.board),
|
||||
(false, true) => self.blue_agent.step_min(&self.board),
|
||||
(true, false) => self.red_agent.step_min(&self.board),
|
||||
(true, true) => self.red_agent.step_max(&self.board),
|
||||
};
|
||||
|
||||
let action = match action {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
let error_msg = format!(
|
||||
"{}ERROR:{} Error while computing next move: {:?}",
|
||||
ansi::RED,
|
||||
ansi::RESET,
|
||||
err
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
self.is_error = true;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if !self.board.play(
|
||||
action,
|
||||
self.is_red_turn
|
||||
.then_some(&self.red_name)
|
||||
.unwrap_or(&self.blue_name)
|
||||
.to_owned(),
|
||||
) {
|
||||
let error_msg = format!(
|
||||
"{} {} ({}) made an invalid move {}!",
|
||||
format!("{}ERROR:{}", ansi::RED, ansi::RESET),
|
||||
self.is_red_turn
|
||||
.then_some(&self.red_name)
|
||||
.unwrap_or(&self.blue_name),
|
||||
self.is_red_turn
|
||||
.then_some(self.red_agent.name())
|
||||
.unwrap_or(self.blue_agent.name()),
|
||||
action
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
self.is_error = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.board.is_full() {
|
||||
self.print_end();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
self.is_red_turn = !self.is_red_turn;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_done(&self) -> bool {
|
||||
(self.board.is_full() && self.red_score.is_some()) || self.is_error
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn red_won(&self) -> Option<bool> {
|
||||
self.red_won
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn is_error(&self) -> bool {
|
||||
self.is_error
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn print_start(&mut self) {
|
||||
self.print_board("", "");
|
||||
(self.game_state_callback)("\r\n");
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
|
||||
fn print_end(&mut self) {
|
||||
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();
|
||||
|
||||
let score = match score {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
let error_msg = format!(
|
||||
"{}ERROR:{} Could not compute final score.\n\r",
|
||||
ansi::RED,
|
||||
ansi::RESET,
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
(self.game_state_callback)("This was probably a zero division.\n\r");
|
||||
self.is_error = true;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
(self.game_state_callback)(&format!(
|
||||
"\r\n{}{} score:{} {:.2}\r\n",
|
||||
self.red_is_maximizer
|
||||
.then_some(ansi::RED)
|
||||
.unwrap_or(ansi::BLUE),
|
||||
self.red_is_maximizer.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.red_is_maximizer = !self.red_is_maximizer;
|
||||
self.board = Board::new();
|
||||
self.is_red_turn = !self.red_is_maximizer;
|
||||
self.is_first_turn = true;
|
||||
self.is_error = false;
|
||||
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));
|
||||
return;
|
||||
}
|
||||
|
||||
let red_wins = red_score > score;
|
||||
self.red_won = Some(red_wins);
|
||||
(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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: GameStateHuman
|
||||
//
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct GameStateHuman {
|
||||
human: HumanInput,
|
||||
|
||||
/// If true, human goes first and maximizes score.
|
||||
/// If false, human goes second and minimizes score.
|
||||
human_is_maximizer: bool,
|
||||
|
||||
agent: Rhai<StdRng>,
|
||||
agent_name: String,
|
||||
|
||||
board: Board,
|
||||
is_human_turn: bool,
|
||||
is_first_turn: bool,
|
||||
is_error: bool,
|
||||
red_score: Option<f32>,
|
||||
|
||||
game_state_callback: Box<dyn Fn(&str) + 'static>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl GameStateHuman {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
human_is_maximizer: bool,
|
||||
max_script: &str,
|
||||
max_name: &str,
|
||||
max_print_callback: js_sys::Function,
|
||||
max_debug_callback: js_sys::Function,
|
||||
|
||||
game_state_callback: js_sys::Function,
|
||||
) -> Result<GameStateHuman, String> {
|
||||
Self::new_native(
|
||||
human_is_maximizer,
|
||||
max_script,
|
||||
max_name,
|
||||
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(
|
||||
human_is_maximizer: bool,
|
||||
max_script: &str,
|
||||
max_name: &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 {
|
||||
human_is_maximizer,
|
||||
board: Board::new(),
|
||||
is_human_turn: human_is_maximizer,
|
||||
is_first_turn: true,
|
||||
is_error: false,
|
||||
red_score: None,
|
||||
|
||||
human: HumanInput::new(ansi::RED.to_string()),
|
||||
agent_name: max_name.to_owned(),
|
||||
agent: Rhai::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 human_is_maximizer(&self) -> bool {
|
||||
self.human_is_maximizer
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn print_start(&mut self) {
|
||||
self.print_board("", "");
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
if !self.is_human_turn {
|
||||
let action = {
|
||||
if self.human_is_maximizer {
|
||||
self.agent.step_min(&self.board)
|
||||
} else {
|
||||
self.agent.step_max(&self.board)
|
||||
}
|
||||
};
|
||||
|
||||
let action = match action {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
let error_msg = format!(
|
||||
"{}ERROR:{} Error while computing next move: {:?}",
|
||||
ansi::RED,
|
||||
ansi::RESET,
|
||||
err
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
self.is_error = true;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !self.board.play(action, "Blue") {
|
||||
let error_msg = format!(
|
||||
"{}ERROR:{} {} made an invalid move {}!",
|
||||
ansi::RED,
|
||||
ansi::RESET,
|
||||
self.agent_name,
|
||||
action
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
}
|
||||
|
||||
self.print_board(
|
||||
self.is_human_turn
|
||||
.then_some(ansi::RED)
|
||||
.unwrap_or(ansi::BLUE),
|
||||
self.is_human_turn.then_some("Red").unwrap_or("Blue"),
|
||||
);
|
||||
(self.game_state_callback)("\r\n");
|
||||
self.is_human_turn = true;
|
||||
}
|
||||
|
||||
self.print_ui();
|
||||
}
|
||||
|
||||
#[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.human_is_maximizer),
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn take_input(&mut self, data: 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") {
|
||||
let error_msg = format!(
|
||||
"{}ERROR:{} {} made an invalid move {}!",
|
||||
ansi::RED,
|
||||
ansi::RESET,
|
||||
"Human",
|
||||
action
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
}
|
||||
self.is_human_turn = false;
|
||||
|
||||
if self.board.is_full() {
|
||||
self.print_end();
|
||||
return;
|
||||
}
|
||||
self.print_board(ansi::RED, "Red");
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
let action = {
|
||||
if self.human_is_maximizer {
|
||||
self.agent.step_min(&self.board)
|
||||
} else {
|
||||
self.agent.step_max(&self.board)
|
||||
}
|
||||
};
|
||||
|
||||
let action = match action {
|
||||
Ok(x) => x,
|
||||
Err(err) => {
|
||||
let error_msg = format!(
|
||||
"{}ERROR:{} Error while computing next move: {:?}",
|
||||
ansi::RED,
|
||||
ansi::RESET,
|
||||
err
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
self.is_error = true;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !self.board.play(action, "Blue") {
|
||||
let error_msg = format!(
|
||||
"{}ERROR:{} {} made an invalid move {}!",
|
||||
ansi::RED,
|
||||
ansi::RESET,
|
||||
self.agent_name,
|
||||
action
|
||||
);
|
||||
|
||||
(self.game_state_callback)(&error_msg);
|
||||
}
|
||||
self.is_human_turn = true;
|
||||
|
||||
if self.board.is_full() {
|
||||
self.print_end();
|
||||
return;
|
||||
}
|
||||
self.print_board(ansi::BLUE, "Blue");
|
||||
(self.game_state_callback)("\r\n");
|
||||
|
||||
self.print_ui();
|
||||
}
|
||||
}
|
||||
|
||||
fn print_end(&mut self) {
|
||||
let board_label = format!(
|
||||
"{}{:<6}{}",
|
||||
self.is_human_turn
|
||||
.then_some(ansi::BLUE)
|
||||
.unwrap_or(ansi::RED),
|
||||
self.is_human_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.human_is_maximizer
|
||||
.then_some(ansi::RED)
|
||||
.unwrap_or(ansi::BLUE),
|
||||
self.human_is_maximizer.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.human_is_maximizer = !self.human_is_maximizer;
|
||||
self.board = Board::new();
|
||||
self.is_human_turn = !self.human_is_maximizer;
|
||||
self.is_first_turn = true;
|
||||
self.is_error = false;
|
||||
self.human = HumanInput::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));
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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 = GameState::new_native(
|
||||
&SCRIPT,
|
||||
"max",
|
||||
|_| {},
|
||||
|_| {},
|
||||
&SCRIPT,
|
||||
"min",
|
||||
|_| {},
|
||||
|_| {},
|
||||
|_| {},
|
||||
)
|
||||
.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 = GameState::new_native(
|
||||
&SCRIPT,
|
||||
"max",
|
||||
|_| {},
|
||||
|_| {},
|
||||
&SCRIPT,
|
||||
"min",
|
||||
|_| {},
|
||||
|_| {},
|
||||
|_| {},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
while !game.is_done() {
|
||||
println!("{:?}", game.step());
|
||||
println!("{:?}", game.board);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use itertools::Itertools;
|
||||
use minimax::{
|
||||
board::{Board, PlayerAction},
|
||||
util::Symb,
|
||||
};
|
||||
use minimax::game::{Board, PlayerAction, Symb};
|
||||
|
||||
use crate::ansi::{DOWN, LEFT, RESET, RIGHT, UP};
|
||||
|
||||
@@ -63,7 +60,7 @@ impl SymbolSelector {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HumanInput {
|
||||
pub struct TermInput {
|
||||
player_color: String,
|
||||
cursor: usize,
|
||||
symbol_selector: SymbolSelector,
|
||||
@@ -73,7 +70,7 @@ pub struct HumanInput {
|
||||
queued_action: Option<PlayerAction>,
|
||||
}
|
||||
|
||||
impl HumanInput {
|
||||
impl TermInput {
|
||||
pub fn new(player_color: String) -> Self {
|
||||
Self {
|
||||
cursor: 0,
|
||||
@@ -152,7 +149,7 @@ impl HumanInput {
|
||||
self.symbol_selector.down(board);
|
||||
}
|
||||
|
||||
" " | "\n" => {
|
||||
" " | "\n" | "\r" => {
|
||||
let symb = Symb::from_char(self.symbol_selector.current());
|
||||
if let Some(symb) = symb {
|
||||
let action = PlayerAction {
|
||||
@@ -3,7 +3,7 @@ import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Minimax",
|
||||
description: "An interactive Rhai scripting language playground",
|
||||
description: "",
|
||||
};
|
||||
|
||||
export const viewport = {
|
||||
|
||||
@@ -47,10 +47,11 @@ interface EditorProps {
|
||||
initialValue?: string;
|
||||
onChange?: (editor: any, changes: any) => void;
|
||||
onReady?: (editor: any) => void;
|
||||
fontSize?: number;
|
||||
}
|
||||
|
||||
export const Editor = forwardRef<any, EditorProps>(function Editor(
|
||||
{ initialValue = "", onChange, onReady },
|
||||
{ initialValue = "", onChange, onReady, fontSize = 14 },
|
||||
ref
|
||||
) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -113,6 +114,17 @@ export const Editor = forwardRef<any, EditorProps>(function Editor(
|
||||
};
|
||||
}, []); // DO NOT FILL ARRAY
|
||||
|
||||
// Update font size when it changes
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
const wrapper = editorRef.current.getWrapperElement();
|
||||
if (wrapper) {
|
||||
wrapper.style.fontSize = `${fontSize}px`;
|
||||
editorRef.current.refresh();
|
||||
}
|
||||
}
|
||||
}, [fontSize]);
|
||||
|
||||
return (
|
||||
<div className={styles.editorContainer}>
|
||||
<textarea
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
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 { Editor } from "@/components/Editor";
|
||||
import { Terminal, TerminalRef } from "@/components/Terminal";
|
||||
import {
|
||||
@@ -19,33 +21,30 @@ fn random_action(board) {
|
||||
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
|
||||
return action;
|
||||
}
|
||||
|
||||
fn step_min(board) {
|
||||
random_action(board)
|
||||
return random_action(board);
|
||||
}
|
||||
|
||||
fn step_max(board) {
|
||||
random_action(board)
|
||||
return random_action(board);
|
||||
}
|
||||
`;
|
||||
|
||||
const exampleScriptList = [
|
||||
{ value: "./hello.rhai", text: "hello.rhai" },
|
||||
{ value: "./fibonacci.rhai", text: "fibonacci.rhai" },
|
||||
{ value: "./arrays.rhai", text: "arrays.rhai" },
|
||||
];
|
||||
|
||||
export default function Playground() {
|
||||
const [isScriptRunning, setIsScriptRunning] = useState(false);
|
||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||
const [fontSize, setFontSize] = useState(14);
|
||||
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||
|
||||
const editorRef = useRef<any>(null);
|
||||
const resultRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -87,12 +86,11 @@ export default function Playground() {
|
||||
}
|
||||
);
|
||||
} catch (ex) {
|
||||
const errorMsg = `\nEXCEPTION: "${ex}"\n`;
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value += errorMsg;
|
||||
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||
}
|
||||
terminalRef.current?.write(
|
||||
"\r\n\x1B[1;31mEXCEPTION:\x1B[0m " + String(ex) + "\r\n"
|
||||
"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" + String(ex).replace("\n", "\n\r") + "\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,12 +130,11 @@ export default function Playground() {
|
||||
}
|
||||
);
|
||||
} catch (ex) {
|
||||
const errorMsg = `\nEXCEPTION: "${ex}"\n`;
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value += errorMsg;
|
||||
if (resultRef.current) {
|
||||
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||
}
|
||||
terminalRef.current?.write(
|
||||
"\r\n\x1B[1;31mEXCEPTION:\x1B[0m " + String(ex) + "\r\n"
|
||||
"\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" + String(ex).replace("\n", "\n\r") + "\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,58 +182,29 @@ export default function Playground() {
|
||||
|
||||
<div className={styles.buttonGroup}>
|
||||
<Dropdown
|
||||
trigger="Example Scripts"
|
||||
disabled={isScriptRunning}
|
||||
items={exampleScriptList.map((item) => ({
|
||||
text: item.text,
|
||||
onClick: () => {},
|
||||
}))}
|
||||
/>
|
||||
|
||||
<Dropdown
|
||||
triggerIcon="help-circle"
|
||||
trigger="Config"
|
||||
align="right"
|
||||
customContent={
|
||||
<div className={styles.helpPanel}>
|
||||
<h1>What is Rhai?</h1>
|
||||
<p>
|
||||
<a
|
||||
href="https://rhai.rs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Rhai
|
||||
</a>{" "}
|
||||
is an embedded scripting language and
|
||||
evaluation engine for Rust that gives a
|
||||
safe and easy way to add scripting to
|
||||
any application.
|
||||
</p>
|
||||
<h1>Hotkeys</h1>
|
||||
<p>
|
||||
You can run the script by pressing{" "}
|
||||
<kbd>Ctrl</kbd> + <kbd>Enter</kbd> when
|
||||
focused in the editor.
|
||||
</p>
|
||||
<div className={styles.footer}>
|
||||
<span>
|
||||
<a
|
||||
href="https://github.com/rhaiscript/playground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Rhai Playground
|
||||
</a>{" "}
|
||||
version: 0.1.0
|
||||
</span>
|
||||
<br />
|
||||
<span>
|
||||
compiled with Rhai (placeholder)
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.configPanel}>
|
||||
<Slider
|
||||
label="Font Size"
|
||||
value={fontSize}
|
||||
min={10}
|
||||
max={24}
|
||||
step={1}
|
||||
onChange={setFontSize}
|
||||
unit="px"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Button
|
||||
iconLeft="help-circle"
|
||||
onClick={() => setIsHelpOpen(true)}
|
||||
>
|
||||
Help
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -248,6 +216,7 @@ export default function Playground() {
|
||||
initialValue={initialCode}
|
||||
onChange={() => {}}
|
||||
onReady={() => setIsEditorReady(true)}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.rightPanel}>
|
||||
@@ -257,6 +226,7 @@ export default function Playground() {
|
||||
<Terminal
|
||||
ref={terminalRef}
|
||||
onData={sendDataToScript}
|
||||
fontSize={fontSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,10 +238,78 @@ export default function Playground() {
|
||||
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>
|
||||
|
||||
|
||||
<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>Available 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, NOT including max.</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>
|
||||
|
||||
<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(any type)</code> - Prints a message to the Output panel. Prefer this over <code>debug</code>.</li>
|
||||
<li><code>debug(any type)</code> - Prints a message to the Output panel, with 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>
|
||||
|
||||
</SidePanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export const Terminal = forwardRef<
|
||||
TerminalRef,
|
||||
{
|
||||
onData: (data: String) => void;
|
||||
fontSize?: number;
|
||||
}
|
||||
>(function Terminal(props, ref) {
|
||||
const terminalRef = useRef<HTMLDivElement>(null);
|
||||
@@ -54,7 +55,7 @@ export const Terminal = forwardRef<
|
||||
// - `init_term()` ccompletes, and we attempt to set `xtermRef.current`, causing issues
|
||||
let mounted = true;
|
||||
|
||||
init_term(terminalRef, props.onData, () => mounted)
|
||||
init_term(terminalRef, props.onData, () => mounted, props.fontSize)
|
||||
.then((term) => {
|
||||
if (!mounted) return;
|
||||
xtermRef.current = term;
|
||||
@@ -70,7 +71,14 @@ export const Terminal = forwardRef<
|
||||
xtermRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [props.onData]);
|
||||
}, [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%" }} />;
|
||||
});
|
||||
@@ -80,7 +88,8 @@ async function init_term(
|
||||
|
||||
// Called when the terminal receives data
|
||||
onData: (data: String) => void,
|
||||
isMounted: () => boolean
|
||||
isMounted: () => boolean,
|
||||
fontSize?: number
|
||||
) {
|
||||
if (!ref.current) return;
|
||||
|
||||
@@ -89,9 +98,9 @@ async function init_term(
|
||||
|
||||
const term = new Terminal({
|
||||
//"fontFamily": "Fantasque",
|
||||
rows: 24,
|
||||
fontSize: 16,
|
||||
tabStopWidth: 8,
|
||||
rows: 30,
|
||||
fontSize: fontSize ?? 18,
|
||||
tabStopWidth: 4,
|
||||
cursorBlink: false,
|
||||
cursorStyle: "block",
|
||||
cursorInactiveStyle: "none",
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -54,13 +54,10 @@ self.onmessage = async (event) => {
|
||||
|
||||
currentGame = new GameState(
|
||||
event_data.script,
|
||||
"max",
|
||||
() => {},
|
||||
() => {},
|
||||
|
||||
// TODO: pick opponent
|
||||
event_data.script,
|
||||
"min",
|
||||
() => {},
|
||||
() => {},
|
||||
|
||||
|
||||
@@ -54,9 +54,7 @@ self.onmessage = async (event) => {
|
||||
};
|
||||
|
||||
currentGame = new GameStateHuman(
|
||||
true,
|
||||
event_data.script,
|
||||
"Agent",
|
||||
appendOutput,
|
||||
appendOutput,
|
||||
appendTerminal
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
|
||||
.terminalPanel {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid #333;
|
||||
@@ -69,7 +69,7 @@
|
||||
}
|
||||
|
||||
.terminalContainer {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
background: #1D1F21;
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -47,10 +47,12 @@ export const initRhaiMode = (CodeMirror: any) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Function to preload all WASM modules used by the application
|
||||
export const loadAllWasm = async (): Promise<void> => {
|
||||
try {
|
||||
// Load Rhai CodeMirror WASM
|
||||
|
||||
await loadRhaiWasm();
|
||||
|
||||
// Load Script Runner WASM by creating and immediately terminating a worker
|
||||
@@ -79,9 +81,9 @@ export const loadAllWasm = async (): Promise<void> => {
|
||||
};
|
||||
});
|
||||
|
||||
console.log("✅ All WASM modules loaded successfully");
|
||||
console.log("All WASM modules loaded successfully");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to load WASM modules:", error);
|
||||
console.error("Failed to load WASM modules:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user