From ccd4292ed2bcc48ab8a606bfa4392fad35fde64e Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:13:34 -0700 Subject: [PATCH] refactr --- build-wasm.sh | 90 --- build.sh | 12 + rust/Cargo.toml | 6 +- rust/minimax/src/agents/mod.rs | 6 +- rust/minimax/src/agents/rhai.rs | 182 +++-- .../src/{board/mod.rs => game/action.rs} | 9 +- rust/minimax/src/{board => game}/board.rs | 3 +- rust/minimax/src/game/mod.rs | 9 + rust/minimax/src/{util.rs => game/symb.rs} | 13 +- rust/minimax/src/{board => game}/tree.rs | 0 rust/minimax/src/lib.rs | 3 +- rust/rhai-codemirror/Cargo.toml | 5 - rust/runner/src/ansi.rs | 3 + rust/runner/src/gamestate.rs | 338 ++++++++ rust/runner/src/gamestatehuman.rs | 288 +++++++ rust/runner/src/lib.rs | 732 +----------------- rust/runner/src/{human.rs => terminput.rs} | 11 +- webui/src/app/layout.tsx | 2 +- webui/src/components/Playground.tsx | 12 +- webui/src/components/Terminal.tsx | 6 +- webui/src/lib/worker_bulk.ts | 3 - webui/src/lib/worker_human.ts | 2 - webui/src/styles/Playground.module.css | 4 +- webui/src/utils/wasmLoader.ts | 8 +- webui/tsconfig.json | 1 - 25 files changed, 808 insertions(+), 940 deletions(-) delete mode 100644 build-wasm.sh create mode 100644 build.sh rename rust/minimax/src/{board/mod.rs => game/action.rs} (92%) rename rust/minimax/src/{board => game}/board.rs (99%) create mode 100644 rust/minimax/src/game/mod.rs rename rust/minimax/src/{util.rs => game/symb.rs} (93%) rename rust/minimax/src/{board => game}/tree.rs (100%) create mode 100644 rust/runner/src/gamestate.rs create mode 100644 rust/runner/src/gamestatehuman.rs rename rust/runner/src/{human.rs => terminput.rs} (96%) diff --git a/build-wasm.sh b/build-wasm.sh deleted file mode 100644 index 63a733c..0000000 --- a/build-wasm.sh +++ /dev/null @@ -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 diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..b602b76 --- /dev/null +++ b/build.sh @@ -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 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index fc0ab8d..cd3ce43 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,3 +1,8 @@ +[profile.release] +lto = true +codegen-units = 1 +opt-level = 's' + [workspace] members = ["runner", "minimax", "rhai-codemirror"] resolver = "2" @@ -5,7 +10,6 @@ resolver = "2" [workspace.dependencies] minimax = { path = "./minimax" } -# TODO: update serde = { version = "1.0", features = ["derive"] } rand = { version = "0.8.5", features = ["alloc", "small_rng"] } anyhow = "1.0.80" diff --git a/rust/minimax/src/agents/mod.rs b/rust/minimax/src/agents/mod.rs index 7530d06..2c8afc2 100644 --- a/rust/minimax/src/agents/mod.rs +++ b/rust/minimax/src/agents/mod.rs @@ -1,13 +1,13 @@ mod rhai; -pub use rhai::Rhai; +pub use rhai::RhaiAgent; -use crate::board::{Board, PlayerAction}; -use anyhow::Result; +use crate::game::{Board, PlayerAction}; pub trait Agent { type ErrorType; + /// This agent's name fn name(&self) -> &'static str; /// Try to minimize the value of a board. diff --git a/rust/minimax/src/agents/rhai.rs b/rust/minimax/src/agents/rhai.rs index af19e0d..abfb948 100644 --- a/rust/minimax/src/agents/rhai.rs +++ b/rust/minimax/src/agents/rhai.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use itertools::{Itertools, Permutations}; +use itertools::Itertools; use parking_lot::Mutex; use rand::{seq::SliceRandom, Rng}; use rhai::{ @@ -8,53 +8,73 @@ use rhai::{ BasicMathPackage, BasicStringPackage, LanguageCorePackage, LogicPackage, MoreStringPackage, Package, }, - CallFnOptions, CustomType, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError, - Position, Scope, TypeBuilder, AST, + CallFnOptions, Dynamic, Engine, EvalAltResult, OptimizationLevel, ParseError, Position, Scope, + AST, }; use std::{sync::Arc, vec::IntoIter}; use super::Agent; -use crate::{ - board::{Board, PlayerAction}, - util::Symb, -}; +use crate::game::{Board, PlayerAction, Symb}; -pub struct RhaiPer> { - inner: Arc>, +// +// MARK: WasmTimer +// + +// Native impl +#[cfg(not(target_arch = "wasm32"))] +struct WasmTimer { + start: std::time::Instant, } -impl> IntoIterator for RhaiPer { - type Item = Vec; - type IntoIter = Permutations; - - fn into_iter(self) -> Self::IntoIter { - (*self.inner).clone() - } -} - -impl> Clone for RhaiPer { - fn clone(&self) -> Self { +#[cfg(not(target_arch = "wasm32"))] +impl WasmTimer { + pub fn new() -> Self { Self { - inner: self.inner.clone(), + start: std::time::Instant::now(), } } + + pub fn elapsed_ms(&self) -> u128 { + self.start.elapsed().as_millis() + } } -impl + 'static> CustomType for RhaiPer { - fn build(mut builder: TypeBuilder) { - builder - .with_name("Perutations") - .is_iterable() - .with_fn("to_string", |_s: &mut Self| "Permutation {}".to_owned()) - .with_fn("to_debug", |_s: &mut Self| "Permutation {}".to_owned()); +// Wasm impl +#[cfg(target_arch = "wasm32")] +struct WasmTimer { + start: f64, +} + +#[cfg(target_arch = "wasm32")] +impl WasmTimer { + fn performance() -> web_sys::Performance { + use wasm_bindgen::JsCast; + let global = web_sys::js_sys::global(); + let performance = web_sys::js_sys::Reflect::get(&global, &"performance".into()) + .expect("performance should be available"); + + performance + .dyn_into::() + .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 { +pub struct RhaiAgent { #[expect(dead_code)] rng: Arc>, @@ -64,7 +84,7 @@ pub struct Rhai { print_callback: Arc, } -impl Rhai { +impl RhaiAgent { pub fn new( script: &str, rng: R, @@ -77,28 +97,7 @@ impl Rhai { let engine = { let mut engine = Engine::new_raw(); - #[cfg(target_arch = "wasm32")] - fn performance() -> Result { - 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::() - } - - #[cfg(target_arch = "wasm32")] - let start = { - let performance = performance().expect("performance should be available"); - - // In milliseconds - performance.now() - }; - - #[cfg(not(target_arch = "wasm32"))] - let start = { - use std::time::Instant; - Instant::now() - }; + let start = WasmTimer::new(); let max_secs: u64 = 5; engine.on_progress(move |ops| { @@ -106,16 +105,7 @@ impl Rhai { return None; } - #[cfg(target_arch = "wasm32")] - let elapsed_s = { - let performance = performance().expect("performance should be available"); - - // In milliseconds - ((performance.now() - start) / 1000.0).round() as u64 - }; - - #[cfg(not(target_arch = "wasm32"))] - let elapsed_s = { start.elapsed().as_secs() }; + let elapsed_s = start.elapsed_ms() as u64 / 1000; if elapsed_s > max_secs { return Some( @@ -126,7 +116,7 @@ impl Rhai { return None; }); - // Do not use FULL, rand functions are not pure + // Do not use FULL, rand_* functions are not pure engine.set_optimization_level(OptimizationLevel::Simple); engine.disable_symbol("eval"); @@ -169,6 +159,10 @@ impl Rhai { let rng = rng.clone(); move || Symb::new_random(&mut *rng.lock()).to_string() }) + .register_fn("rand_op", { + let rng = rng.clone(); + move || Symb::new_random_op(&mut *rng.lock()).to_string() + }) .register_fn("rand_action", { let rng = rng.clone(); move |board: Board| PlayerAction::new_random(&mut *rng.lock(), &board) @@ -193,9 +187,7 @@ impl Rhai { } }; - let per = RhaiPer { - inner: v.into_iter().permutations(size).into(), - }; + let per = helpers::RhaiPer::new(v.into_iter().permutations(size).into()); Ok(Dynamic::from(per)) }, @@ -204,7 +196,7 @@ impl Rhai { engine .build_type::() .build_type::() - .build_type::>>(); + .build_type::>>(); engine }; @@ -225,7 +217,7 @@ impl Rhai { } } -impl Agent for Rhai { +impl Agent for RhaiAgent { type ErrorType = EvalAltResult; fn name(&self) -> &'static str { @@ -262,3 +254,55 @@ impl Agent for Rhai { } } } + +// +// 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> { + inner: Arc>, + } + + impl> RhaiPer { + pub fn new(inner: Permutations) -> Self { + Self { + inner: Arc::new(inner), + } + } + } + + impl> IntoIterator for RhaiPer { + type Item = Vec; + type IntoIter = Permutations; + + fn into_iter(self) -> Self::IntoIter { + (*self.inner).clone() + } + } + + impl> Clone for RhaiPer { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + + impl + 'static> CustomType for RhaiPer { + fn build(mut builder: TypeBuilder) { + 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()); + } + } +} diff --git a/rust/minimax/src/board/mod.rs b/rust/minimax/src/game/action.rs similarity index 92% rename from rust/minimax/src/board/mod.rs rename to rust/minimax/src/game/action.rs index 1162e68..4efad0f 100644 --- a/rust/minimax/src/board/mod.rs +++ b/rust/minimax/src/game/action.rs @@ -1,15 +1,8 @@ -#[allow(clippy::module_inception)] -mod board; -mod tree; - use rand::Rng; use rhai::{CustomType, EvalAltResult, TypeBuilder}; use std::fmt::Display; -pub use board::Board; -pub use tree::TreeElement; - -use crate::util::Symb; +use super::{Board, Symb}; #[derive(Debug, Clone, Copy)] pub struct PlayerAction { diff --git a/rust/minimax/src/board/board.rs b/rust/minimax/src/game/board.rs similarity index 99% rename from rust/minimax/src/board/board.rs rename to rust/minimax/src/game/board.rs index e87258a..d005b7f 100644 --- a/rust/minimax/src/board/board.rs +++ b/rust/minimax/src/game/board.rs @@ -8,8 +8,7 @@ use rhai::Position; use rhai::TypeBuilder; use std::fmt::{Debug, Display, Write}; -use super::{PlayerAction, TreeElement}; -use crate::util::Symb; +use super::{PlayerAction, Symb, TreeElement}; #[derive(Debug)] enum InterTreeElement { diff --git a/rust/minimax/src/game/mod.rs b/rust/minimax/src/game/mod.rs new file mode 100644 index 0000000..99150eb --- /dev/null +++ b/rust/minimax/src/game/mod.rs @@ -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; diff --git a/rust/minimax/src/util.rs b/rust/minimax/src/game/symb.rs similarity index 93% rename from rust/minimax/src/util.rs rename to rust/minimax/src/game/symb.rs index 2769898..8e506de 100644 --- a/rust/minimax/src/util.rs +++ b/rust/minimax/src/game/symb.rs @@ -1,10 +1,9 @@ +use rand::Rng; use std::{ fmt::{Debug, Display}, num::NonZeroU8, }; -use rand::Rng; - #[derive(PartialEq, Eq, Clone, Copy, Hash)] pub enum Symb { @@ -57,6 +56,16 @@ impl Symb { } } + pub fn new_random_op(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 { match self { Self::Plus => Some('+'), diff --git a/rust/minimax/src/board/tree.rs b/rust/minimax/src/game/tree.rs similarity index 100% rename from rust/minimax/src/board/tree.rs rename to rust/minimax/src/game/tree.rs diff --git a/rust/minimax/src/lib.rs b/rust/minimax/src/lib.rs index ce59f43..13042eb 100644 --- a/rust/minimax/src/lib.rs +++ b/rust/minimax/src/lib.rs @@ -1,3 +1,2 @@ pub mod agents; -pub mod board; -pub mod util; +pub mod game; diff --git a/rust/rhai-codemirror/Cargo.toml b/rust/rhai-codemirror/Cargo.toml index 655e608..c9280c1 100644 --- a/rust/rhai-codemirror/Cargo.toml +++ b/rust/rhai-codemirror/Cargo.toml @@ -6,11 +6,6 @@ edition = "2021" [lib] crate-type = ["cdylib"] -[profile.release] -lto = true -codegen-units = 1 -opt-level = 's' - [dependencies] rhai = { workspace = true, features = ["internals"] } diff --git a/rust/runner/src/ansi.rs b/rust/runner/src/ansi.rs index 131d928..4358c27 100644 --- a/rust/runner/src/ansi.rs +++ b/rust/runner/src/ansi.rs @@ -1,3 +1,6 @@ +#![expect(clippy::allow_attributes)] +#![allow(dead_code)] + pub const RESET: &str = "\x1b[0m"; pub const RED: &str = "\x1b[31m"; pub const BLUE: &str = "\x1b[34m"; diff --git a/rust/runner/src/gamestate.rs b/rust/runner/src/gamestate.rs new file mode 100644 index 0000000..fb51d40 --- /dev/null +++ b/rust/runner/src/gamestate.rs @@ -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, + blue_agent: RhaiAgent, + + board: Board, + is_red_turn: bool, + is_first_turn: bool, + is_error: bool, + red_is_maximizer: bool, + red_score: Option, + red_won: Option, + + game_state_callback: Box, +} + +#[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 { + 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 { + 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 { + 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); + } +} diff --git a/rust/runner/src/gamestatehuman.rs b/rust/runner/src/gamestatehuman.rs new file mode 100644 index 0000000..87097d2 --- /dev/null +++ b/rust/runner/src/gamestatehuman.rs @@ -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, + + board: Board, + is_red_turn: bool, + is_first_turn: bool, + is_error: bool, + red_score: Option, + + game_state_callback: Box, +} + +#[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 { + 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 { + 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(()); + } +} diff --git a/rust/runner/src/lib.rs b/rust/runner/src/lib.rs index 5afa0a7..a052f19 100644 --- a/rust/runner/src/lib.rs +++ b/rust/runner/src/lib.rs @@ -1,15 +1,9 @@ -use minimax::{ - agents::{Agent, Rhai}, - board::Board, -}; -use rand::{rngs::StdRng, SeedableRng}; -use rhai::ParseError; use wasm_bindgen::prelude::*; -use crate::human::HumanInput; - mod ansi; -mod human; +mod gamestate; +mod gamestatehuman; +mod terminput; #[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; @@ -18,723 +12,3 @@ static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; pub fn init_panic_hook() { console_error_panic_hook::set_once(); } - -// -// MARK: GameState -// - -#[wasm_bindgen] -pub struct GameState { - red_agent: Rhai, - red_name: String, - - blue_agent: Rhai, - blue_name: String, - - board: Board, - is_red_turn: bool, - is_first_turn: bool, - is_error: bool, - red_is_maximizer: bool, - red_score: Option, - red_won: Option, - - game_state_callback: Box, -} - -#[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 { - 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 { - 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 { - 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, - agent_name: String, - - board: Board, - is_human_turn: bool, - is_first_turn: bool, - is_error: bool, - red_score: Option, - - game_state_callback: Box, -} - -#[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 { - 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 { - 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); - } -} diff --git a/rust/runner/src/human.rs b/rust/runner/src/terminput.rs similarity index 96% rename from rust/runner/src/human.rs rename to rust/runner/src/terminput.rs index e2a39ba..b8b09d4 100644 --- a/rust/runner/src/human.rs +++ b/rust/runner/src/terminput.rs @@ -1,8 +1,5 @@ use itertools::Itertools; -use minimax::{ - board::{Board, PlayerAction}, - util::Symb, -}; +use minimax::game::{Board, PlayerAction, Symb}; use crate::ansi::{DOWN, LEFT, RESET, RIGHT, UP}; @@ -63,7 +60,7 @@ impl SymbolSelector { } } -pub struct HumanInput { +pub struct TermInput { player_color: String, cursor: usize, symbol_selector: SymbolSelector, @@ -73,7 +70,7 @@ pub struct HumanInput { queued_action: Option, } -impl HumanInput { +impl TermInput { pub fn new(player_color: String) -> Self { Self { cursor: 0, @@ -152,7 +149,7 @@ impl HumanInput { self.symbol_selector.down(board); } - " " | "\n" => { + " " | "\n" | "\r" => { let symb = Symb::from_char(self.symbol_selector.current()); if let Some(symb) = symb { let action = PlayerAction { diff --git a/webui/src/app/layout.tsx b/webui/src/app/layout.tsx index 492c740..1bdfbb5 100644 --- a/webui/src/app/layout.tsx +++ b/webui/src/app/layout.tsx @@ -3,7 +3,7 @@ import { Metadata } from "next"; export const metadata: Metadata = { title: "Minimax", - description: "An interactive Rhai scripting language playground", + description: "", }; export const viewport = { diff --git a/webui/src/components/Playground.tsx b/webui/src/components/Playground.tsx index 2693ac3..f441866 100644 --- a/webui/src/components/Playground.tsx +++ b/webui/src/components/Playground.tsx @@ -87,12 +87,11 @@ export default function Playground() { } ); } catch (ex) { - const errorMsg = `\nEXCEPTION: "${ex}"\n`; if (resultRef.current) { - resultRef.current.value += errorMsg; + resultRef.current.value += `\nScript exited with error:\n${ex}\n`; } terminalRef.current?.write( - "\r\n\x1B[1;31mEXCEPTION:\x1B[0m " + String(ex) + "\r\n" + "\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" + String(ex).replace("\n", "\n\r") + "\r\n" ); } @@ -132,12 +131,11 @@ export default function Playground() { } ); } catch (ex) { - const errorMsg = `\nEXCEPTION: "${ex}"\n`; - if (resultRef.current) { - resultRef.current.value += errorMsg; + if (resultRef.current) { + resultRef.current.value += `\nScript exited with error:\n${ex}\n`; } terminalRef.current?.write( - "\r\n\x1B[1;31mEXCEPTION:\x1B[0m " + String(ex) + "\r\n" + "\r\n\x1B[1;31mScript exited with error:\x1B[0m\n\r" + String(ex).replace("\n", "\n\r") + "\r\n" ); } diff --git a/webui/src/components/Terminal.tsx b/webui/src/components/Terminal.tsx index 19da198..31015b8 100644 --- a/webui/src/components/Terminal.tsx +++ b/webui/src/components/Terminal.tsx @@ -89,9 +89,9 @@ async function init_term( const term = new Terminal({ //"fontFamily": "Fantasque", - rows: 24, - fontSize: 16, - tabStopWidth: 8, + rows: 30, + fontSize: 18, + tabStopWidth: 4, cursorBlink: false, cursorStyle: "block", cursorInactiveStyle: "none", diff --git a/webui/src/lib/worker_bulk.ts b/webui/src/lib/worker_bulk.ts index 5ca8003..b5d2470 100644 --- a/webui/src/lib/worker_bulk.ts +++ b/webui/src/lib/worker_bulk.ts @@ -54,13 +54,10 @@ self.onmessage = async (event) => { currentGame = new GameState( event_data.script, - "max", () => {}, () => {}, - // TODO: pick opponent event_data.script, - "min", () => {}, () => {}, diff --git a/webui/src/lib/worker_human.ts b/webui/src/lib/worker_human.ts index bf71289..cb552ec 100644 --- a/webui/src/lib/worker_human.ts +++ b/webui/src/lib/worker_human.ts @@ -54,9 +54,7 @@ self.onmessage = async (event) => { }; currentGame = new GameStateHuman( - true, event_data.script, - "Agent", appendOutput, appendOutput, appendTerminal diff --git a/webui/src/styles/Playground.module.css b/webui/src/styles/Playground.module.css index 3924e65..34b0642 100644 --- a/webui/src/styles/Playground.module.css +++ b/webui/src/styles/Playground.module.css @@ -45,7 +45,7 @@ } .terminalPanel { - flex: 1; + flex: 0 0 auto; display: flex; flex-direction: column; border-bottom: 1px solid #333; @@ -69,7 +69,7 @@ } .terminalContainer { - flex: 1; + flex: 0 0 auto; overflow: hidden; padding: 8px; background: #1D1F21; diff --git a/webui/src/utils/wasmLoader.ts b/webui/src/utils/wasmLoader.ts index 1ae1185..1f8b5f1 100644 --- a/webui/src/utils/wasmLoader.ts +++ b/webui/src/utils/wasmLoader.ts @@ -47,10 +47,12 @@ export const initRhaiMode = (CodeMirror: any) => { }); }; + + // Function to preload all WASM modules used by the application export const loadAllWasm = async (): Promise => { try { - // Load Rhai CodeMirror WASM + await loadRhaiWasm(); // Load Script Runner WASM by creating and immediately terminating a worker @@ -79,9 +81,9 @@ export const loadAllWasm = async (): Promise => { }; }); - console.log("✅ All WASM modules loaded successfully"); + console.log("All WASM modules loaded successfully"); } catch (error) { - console.error("❌ Failed to load WASM modules:", error); + console.error("Failed to load WASM modules:", error); throw error; } }; diff --git a/webui/tsconfig.json b/webui/tsconfig.json index d312114..f4eeeac 100644 --- a/webui/tsconfig.json +++ b/webui/tsconfig.json @@ -18,7 +18,6 @@ "name": "next" } ], - "baseUrl": ".", "paths": { "@/*": ["./src/*"] }