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]
|
[workspace]
|
||||||
members = ["runner", "minimax", "rhai-codemirror"]
|
members = ["runner", "minimax", "rhai-codemirror"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
@@ -5,7 +10,6 @@ resolver = "2"
|
|||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
minimax = { path = "./minimax" }
|
minimax = { path = "./minimax" }
|
||||||
|
|
||||||
# TODO: update
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
rand = { version = "0.8.5", features = ["alloc", "small_rng"] }
|
rand = { version = "0.8.5", features = ["alloc", "small_rng"] }
|
||||||
anyhow = "1.0.80"
|
anyhow = "1.0.80"
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
mod rhai;
|
mod rhai;
|
||||||
|
|
||||||
pub use rhai::Rhai;
|
pub use rhai::RhaiAgent;
|
||||||
|
|
||||||
use crate::board::{Board, PlayerAction};
|
use crate::game::{Board, PlayerAction};
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
pub trait Agent {
|
pub trait Agent {
|
||||||
type ErrorType;
|
type ErrorType;
|
||||||
|
|
||||||
|
/// This agent's name
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
/// Try to minimize the value of a board.
|
/// Try to minimize the value of a board.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use itertools::{Itertools, Permutations};
|
use itertools::Itertools;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rand::{seq::SliceRandom, Rng};
|
use rand::{seq::SliceRandom, Rng};
|
||||||
use rhai::{
|
use rhai::{
|
||||||
@@ -8,53 +8,73 @@ use rhai::{
|
|||||||
BasicMathPackage, BasicStringPackage, LanguageCorePackage, LogicPackage, MoreStringPackage,
|
BasicMathPackage, BasicStringPackage, LanguageCorePackage, LogicPackage, MoreStringPackage,
|
||||||
Package,
|
Package,
|
||||||
},
|
},
|
||||||
CallFnOptions, CustomType, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError,
|
CallFnOptions, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError, Position, Scope,
|
||||||
Position, Scope, TypeBuilder, AST,
|
AST,
|
||||||
};
|
};
|
||||||
use std::{sync::Arc, vec::IntoIter};
|
use std::{sync::Arc, vec::IntoIter};
|
||||||
|
|
||||||
use super::Agent;
|
use super::Agent;
|
||||||
use crate::{
|
use crate::game::{Board, PlayerAction, Symb};
|
||||||
board::{Board, PlayerAction},
|
|
||||||
util::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> {
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
type Item = Vec<T>;
|
impl WasmTimer {
|
||||||
type IntoIter = Permutations<I>;
|
pub fn new() -> Self {
|
||||||
|
|
||||||
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 {
|
Self {
|
||||||
inner: self.inner.clone(),
|
start: std::time::Instant::now(),
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone + 'static, I: Clone + Iterator<Item = T> + 'static> CustomType for RhaiPer<T, I> {
|
pub fn elapsed_ms(&self) -> u128 {
|
||||||
fn build(mut builder: TypeBuilder<Self>) {
|
self.start.elapsed().as_millis()
|
||||||
builder
|
}
|
||||||
.with_name("Perutations")
|
}
|
||||||
.is_iterable()
|
|
||||||
.with_fn("to_string", |_s: &mut Self| "Permutation {}".to_owned())
|
// Wasm impl
|
||||||
.with_fn("to_debug", |_s: &mut Self| "Permutation {}".to_owned());
|
#[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)]
|
#[expect(dead_code)]
|
||||||
rng: Arc<Mutex<R>>,
|
rng: Arc<Mutex<R>>,
|
||||||
|
|
||||||
@@ -64,7 +84,7 @@ pub struct Rhai<R: Rng + 'static> {
|
|||||||
print_callback: Arc<dyn Fn(&str) + 'static>,
|
print_callback: Arc<dyn Fn(&str) + 'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Rng + 'static> Rhai<R> {
|
impl<R: Rng + 'static> RhaiAgent<R> {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
script: &str,
|
script: &str,
|
||||||
rng: R,
|
rng: R,
|
||||||
@@ -77,28 +97,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
|||||||
let engine = {
|
let engine = {
|
||||||
let mut engine = Engine::new_raw();
|
let mut engine = Engine::new_raw();
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
let start = WasmTimer::new();
|
||||||
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 max_secs: u64 = 5;
|
let max_secs: u64 = 5;
|
||||||
engine.on_progress(move |ops| {
|
engine.on_progress(move |ops| {
|
||||||
@@ -106,16 +105,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
let elapsed_s = start.elapsed_ms() as u64 / 1000;
|
||||||
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() };
|
|
||||||
|
|
||||||
if elapsed_s > max_secs {
|
if elapsed_s > max_secs {
|
||||||
return Some(
|
return Some(
|
||||||
@@ -126,7 +116,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
|||||||
return None;
|
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.set_optimization_level(OptimizationLevel::Simple);
|
||||||
|
|
||||||
engine.disable_symbol("eval");
|
engine.disable_symbol("eval");
|
||||||
@@ -163,12 +153,16 @@ impl<R: Rng + 'static> Rhai<R> {
|
|||||||
})
|
})
|
||||||
.register_fn("rand_bool", {
|
.register_fn("rand_bool", {
|
||||||
let rng = rng.clone();
|
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", {
|
.register_fn("rand_symb", {
|
||||||
let rng = rng.clone();
|
let rng = rng.clone();
|
||||||
move || Symb::new_random(&mut *rng.lock()).to_string()
|
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", {
|
.register_fn("rand_action", {
|
||||||
let rng = rng.clone();
|
let rng = rng.clone();
|
||||||
move |board: Board| PlayerAction::new_random(&mut *rng.lock(), &board)
|
move |board: Board| PlayerAction::new_random(&mut *rng.lock(), &board)
|
||||||
@@ -193,9 +187,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let per = RhaiPer {
|
let per = helpers::RhaiPer::new(v.into_iter().permutations(size).into());
|
||||||
inner: v.into_iter().permutations(size).into(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Dynamic::from(per))
|
Ok(Dynamic::from(per))
|
||||||
},
|
},
|
||||||
@@ -204,7 +196,7 @@ impl<R: Rng + 'static> Rhai<R> {
|
|||||||
engine
|
engine
|
||||||
.build_type::<Board>()
|
.build_type::<Board>()
|
||||||
.build_type::<PlayerAction>()
|
.build_type::<PlayerAction>()
|
||||||
.build_type::<RhaiPer<Dynamic, IntoIter<Dynamic>>>();
|
.build_type::<helpers::RhaiPer<Dynamic, IntoIter<Dynamic>>>();
|
||||||
engine
|
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;
|
type ErrorType = EvalAltResult;
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
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 rand::Rng;
|
||||||
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
use rhai::{CustomType, EvalAltResult, TypeBuilder};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
pub use board::Board;
|
use super::{Board, Symb};
|
||||||
pub use tree::TreeElement;
|
|
||||||
|
|
||||||
use crate::util::Symb;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct PlayerAction {
|
pub struct PlayerAction {
|
||||||
@@ -8,8 +8,7 @@ use rhai::Position;
|
|||||||
use rhai::TypeBuilder;
|
use rhai::TypeBuilder;
|
||||||
use std::fmt::{Debug, Display, Write};
|
use std::fmt::{Debug, Display, Write};
|
||||||
|
|
||||||
use super::{PlayerAction, TreeElement};
|
use super::{PlayerAction, Symb, TreeElement};
|
||||||
use crate::util::Symb;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum InterTreeElement {
|
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::{
|
use std::{
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
num::NonZeroU8,
|
num::NonZeroU8,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rand::Rng;
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy, Hash)]
|
#[derive(PartialEq, Eq, Clone, Copy, Hash)]
|
||||||
|
|
||||||
pub enum Symb {
|
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> {
|
pub const fn get_char(&self) -> Option<char> {
|
||||||
match self {
|
match self {
|
||||||
Self::Plus => Some('+'),
|
Self::Plus => Some('+'),
|
||||||
@@ -1,3 +1,2 @@
|
|||||||
pub mod agents;
|
pub mod agents;
|
||||||
pub mod board;
|
pub mod game;
|
||||||
pub mod util;
|
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ edition = "2021"
|
|||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = true
|
|
||||||
codegen-units = 1
|
|
||||||
opt-level = 's'
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rhai = { workspace = true, features = ["internals"] }
|
rhai = { workspace = true, features = ["internals"] }
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
#![expect(clippy::allow_attributes)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub const RESET: &str = "\x1b[0m";
|
pub const RESET: &str = "\x1b[0m";
|
||||||
pub const RED: &str = "\x1b[31m";
|
pub const RED: &str = "\x1b[31m";
|
||||||
pub const BLUE: &str = "\x1b[34m";
|
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 wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
use crate::human::HumanInput;
|
|
||||||
|
|
||||||
mod ansi;
|
mod ansi;
|
||||||
mod human;
|
mod gamestate;
|
||||||
|
mod gamestatehuman;
|
||||||
|
mod terminput;
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
|
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() {
|
pub fn init_panic_hook() {
|
||||||
console_error_panic_hook::set_once();
|
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 itertools::Itertools;
|
||||||
use minimax::{
|
use minimax::game::{Board, PlayerAction, Symb};
|
||||||
board::{Board, PlayerAction},
|
|
||||||
util::Symb,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::ansi::{DOWN, LEFT, RESET, RIGHT, UP};
|
use crate::ansi::{DOWN, LEFT, RESET, RIGHT, UP};
|
||||||
|
|
||||||
@@ -63,7 +60,7 @@ impl SymbolSelector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct HumanInput {
|
pub struct TermInput {
|
||||||
player_color: String,
|
player_color: String,
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
symbol_selector: SymbolSelector,
|
symbol_selector: SymbolSelector,
|
||||||
@@ -73,7 +70,7 @@ pub struct HumanInput {
|
|||||||
queued_action: Option<PlayerAction>,
|
queued_action: Option<PlayerAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HumanInput {
|
impl TermInput {
|
||||||
pub fn new(player_color: String) -> Self {
|
pub fn new(player_color: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
@@ -152,7 +149,7 @@ impl HumanInput {
|
|||||||
self.symbol_selector.down(board);
|
self.symbol_selector.down(board);
|
||||||
}
|
}
|
||||||
|
|
||||||
" " | "\n" => {
|
" " | "\n" | "\r" => {
|
||||||
let symb = Symb::from_char(self.symbol_selector.current());
|
let symb = Symb::from_char(self.symbol_selector.current());
|
||||||
if let Some(symb) = symb {
|
if let Some(symb) = symb {
|
||||||
let action = PlayerAction {
|
let action = PlayerAction {
|
||||||
@@ -3,7 +3,7 @@ import { Metadata } from "next";
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Minimax",
|
title: "Minimax",
|
||||||
description: "An interactive Rhai scripting language playground",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const viewport = {
|
export const viewport = {
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ interface EditorProps {
|
|||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
onChange?: (editor: any, changes: any) => void;
|
onChange?: (editor: any, changes: any) => void;
|
||||||
onReady?: (editor: any) => void;
|
onReady?: (editor: any) => void;
|
||||||
|
fontSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = forwardRef<any, EditorProps>(function Editor(
|
export const Editor = forwardRef<any, EditorProps>(function Editor(
|
||||||
{ initialValue = "", onChange, onReady },
|
{ initialValue = "", onChange, onReady, fontSize = 14 },
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -113,6 +114,17 @@ export const Editor = forwardRef<any, EditorProps>(function Editor(
|
|||||||
};
|
};
|
||||||
}, []); // DO NOT FILL ARRAY
|
}, []); // 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 (
|
return (
|
||||||
<div className={styles.editorContainer}>
|
<div className={styles.editorContainer}>
|
||||||
<textarea
|
<textarea
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { useState, useRef, useCallback, useEffect } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/Button";
|
import { Button } from "@/components/ui/Button";
|
||||||
import { Dropdown } from "@/components/ui/Dropdown";
|
import { Dropdown } from "@/components/ui/Dropdown";
|
||||||
|
import { Slider } from "@/components/ui/Slider";
|
||||||
|
import { SidePanel } from "@/components/ui/SidePanel";
|
||||||
import { Editor } from "@/components/Editor";
|
import { Editor } from "@/components/Editor";
|
||||||
import { Terminal, TerminalRef } from "@/components/Terminal";
|
import { Terminal, TerminalRef } from "@/components/Terminal";
|
||||||
import {
|
import {
|
||||||
@@ -19,33 +21,30 @@ fn random_action(board) {
|
|||||||
let pos = rand_int(0, 10);
|
let pos = rand_int(0, 10);
|
||||||
let action = Action(symb, pos);
|
let action = Action(symb, pos);
|
||||||
|
|
||||||
|
// If this action is invalid, randomly select a new one.
|
||||||
while !board.can_play(action) {
|
while !board.can_play(action) {
|
||||||
let symb = rand_symb();
|
let symb = rand_symb();
|
||||||
let pos = rand_int(0, 10);
|
let pos = rand_int(0, 10);
|
||||||
action = Action(symb, pos);
|
action = Action(symb, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
return action
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_min(board) {
|
fn step_min(board) {
|
||||||
random_action(board)
|
return random_action(board);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step_max(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() {
|
export default function Playground() {
|
||||||
const [isScriptRunning, setIsScriptRunning] = useState(false);
|
const [isScriptRunning, setIsScriptRunning] = useState(false);
|
||||||
const [isEditorReady, setIsEditorReady] = useState(false);
|
const [isEditorReady, setIsEditorReady] = useState(false);
|
||||||
|
const [fontSize, setFontSize] = useState(14);
|
||||||
|
const [isHelpOpen, setIsHelpOpen] = useState(false);
|
||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
const resultRef = useRef<HTMLTextAreaElement>(null);
|
const resultRef = useRef<HTMLTextAreaElement>(null);
|
||||||
@@ -87,12 +86,11 @@ export default function Playground() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
const errorMsg = `\nEXCEPTION: "${ex}"\n`;
|
|
||||||
if (resultRef.current) {
|
if (resultRef.current) {
|
||||||
resultRef.current.value += errorMsg;
|
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||||
}
|
}
|
||||||
terminalRef.current?.write(
|
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) {
|
} catch (ex) {
|
||||||
const errorMsg = `\nEXCEPTION: "${ex}"\n`;
|
|
||||||
if (resultRef.current) {
|
if (resultRef.current) {
|
||||||
resultRef.current.value += errorMsg;
|
resultRef.current.value += `\nScript exited with error:\n${ex}\n`;
|
||||||
}
|
}
|
||||||
terminalRef.current?.write(
|
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}>
|
<div className={styles.buttonGroup}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
trigger="Example Scripts"
|
trigger="Config"
|
||||||
disabled={isScriptRunning}
|
|
||||||
items={exampleScriptList.map((item) => ({
|
|
||||||
text: item.text,
|
|
||||||
onClick: () => {},
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
triggerIcon="help-circle"
|
|
||||||
align="right"
|
align="right"
|
||||||
customContent={
|
customContent={
|
||||||
<div className={styles.helpPanel}>
|
<div className={styles.configPanel}>
|
||||||
<h1>What is Rhai?</h1>
|
<Slider
|
||||||
<p>
|
label="Font Size"
|
||||||
<a
|
value={fontSize}
|
||||||
href="https://rhai.rs"
|
min={10}
|
||||||
target="_blank"
|
max={24}
|
||||||
rel="noopener noreferrer"
|
step={1}
|
||||||
>
|
onChange={setFontSize}
|
||||||
Rhai
|
unit="px"
|
||||||
</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>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
iconLeft="help-circle"
|
||||||
|
onClick={() => setIsHelpOpen(true)}
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -248,6 +216,7 @@ export default function Playground() {
|
|||||||
initialValue={initialCode}
|
initialValue={initialCode}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
onReady={() => setIsEditorReady(true)}
|
onReady={() => setIsEditorReady(true)}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.rightPanel}>
|
<div className={styles.rightPanel}>
|
||||||
@@ -257,6 +226,7 @@ export default function Playground() {
|
|||||||
<Terminal
|
<Terminal
|
||||||
ref={terminalRef}
|
ref={terminalRef}
|
||||||
onData={sendDataToScript}
|
onData={sendDataToScript}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,10 +238,78 @@ export default function Playground() {
|
|||||||
readOnly
|
readOnly
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="Use print() to produce output"
|
placeholder="Use print() to produce output"
|
||||||
|
style={{ fontSize: `${fontSize}px` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const Terminal = forwardRef<
|
|||||||
TerminalRef,
|
TerminalRef,
|
||||||
{
|
{
|
||||||
onData: (data: String) => void;
|
onData: (data: String) => void;
|
||||||
|
fontSize?: number;
|
||||||
}
|
}
|
||||||
>(function Terminal(props, ref) {
|
>(function Terminal(props, ref) {
|
||||||
const terminalRef = useRef<HTMLDivElement>(null);
|
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
|
// - `init_term()` ccompletes, and we attempt to set `xtermRef.current`, causing issues
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
|
|
||||||
init_term(terminalRef, props.onData, () => mounted)
|
init_term(terminalRef, props.onData, () => mounted, props.fontSize)
|
||||||
.then((term) => {
|
.then((term) => {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
xtermRef.current = term;
|
xtermRef.current = term;
|
||||||
@@ -70,7 +71,14 @@ export const Terminal = forwardRef<
|
|||||||
xtermRef.current = null;
|
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%" }} />;
|
return <div ref={terminalRef} style={{ height: "100%", width: "100%" }} />;
|
||||||
});
|
});
|
||||||
@@ -80,7 +88,8 @@ async function init_term(
|
|||||||
|
|
||||||
// Called when the terminal receives data
|
// Called when the terminal receives data
|
||||||
onData: (data: String) => void,
|
onData: (data: String) => void,
|
||||||
isMounted: () => boolean
|
isMounted: () => boolean,
|
||||||
|
fontSize?: number
|
||||||
) {
|
) {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
@@ -89,9 +98,9 @@ async function init_term(
|
|||||||
|
|
||||||
const term = new Terminal({
|
const term = new Terminal({
|
||||||
//"fontFamily": "Fantasque",
|
//"fontFamily": "Fantasque",
|
||||||
rows: 24,
|
rows: 30,
|
||||||
fontSize: 16,
|
fontSize: fontSize ?? 18,
|
||||||
tabStopWidth: 8,
|
tabStopWidth: 4,
|
||||||
cursorBlink: false,
|
cursorBlink: false,
|
||||||
cursorStyle: "block",
|
cursorStyle: "block",
|
||||||
cursorInactiveStyle: "none",
|
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(
|
currentGame = new GameState(
|
||||||
event_data.script,
|
event_data.script,
|
||||||
"max",
|
|
||||||
() => {},
|
() => {},
|
||||||
() => {},
|
() => {},
|
||||||
|
|
||||||
// TODO: pick opponent
|
|
||||||
event_data.script,
|
event_data.script,
|
||||||
"min",
|
|
||||||
() => {},
|
() => {},
|
||||||
() => {},
|
() => {},
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ self.onmessage = async (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
currentGame = new GameStateHuman(
|
currentGame = new GameStateHuman(
|
||||||
true,
|
|
||||||
event_data.script,
|
event_data.script,
|
||||||
"Agent",
|
|
||||||
appendOutput,
|
appendOutput,
|
||||||
appendOutput,
|
appendOutput,
|
||||||
appendTerminal
|
appendTerminal
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.terminalPanel {
|
.terminalPanel {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid #333;
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.terminalContainer {
|
.terminalContainer {
|
||||||
flex: 1;
|
flex: 0 0 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
background: #1D1F21;
|
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
|
// Function to preload all WASM modules used by the application
|
||||||
export const loadAllWasm = async (): Promise<void> => {
|
export const loadAllWasm = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
// Load Rhai CodeMirror WASM
|
|
||||||
await loadRhaiWasm();
|
await loadRhaiWasm();
|
||||||
|
|
||||||
// Load Script Runner WASM by creating and immediately terminating a worker
|
// 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) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to load WASM modules:", error);
|
console.error("Failed to load WASM modules:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user