use std::cmp::Ordering; use minimax::{ agents::{Agent, RhaiAgent}, game::Board, }; use rand::{rngs::StdRng, SeedableRng}; use rhai::ParseError; use wasm_bindgen::prelude::*; use crate::ansi; #[derive(Debug, Clone, Copy)] enum GameState { /// Round 1, red is maximizing Red, /// Round 2, red is minimizing (and goes second) Blue { red_score: f32 }, /// Game over, red won RedWins { blue_score: f32 }, /// Game over, blue won BlueWins { blue_score: f32 }, /// Game over, draw DrawScore { score: f32 }, /// Invalid board, draw DrawInvalid, /// Error, end early Error, } #[wasm_bindgen] pub struct MinMaxGame { red_agent: RhaiAgent, blue_agent: RhaiAgent, board: Board, is_red_turn: bool, is_first_print: bool, state: GameState, game_state_callback: Box, } #[wasm_bindgen] impl MinMaxGame { #[wasm_bindgen(constructor)] pub fn new( red_script: &str, red_print_callback: js_sys::Function, red_debug_callback: js_sys::Function, blue_script: &str, blue_print_callback: js_sys::Function, blue_debug_callback: js_sys::Function, game_state_callback: js_sys::Function, ) -> Result { 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(MinMaxGame { board: Board::new(), is_first_print: true, is_red_turn: true, state: GameState::Red, red_agent: RhaiAgent::new( red_script, StdRng::from_seed(seed1), red_print_callback, red_debug_callback, )?, blue_agent: RhaiAgent::new( blue_script, StdRng::from_seed(seed2), blue_print_callback, blue_debug_callback, )?, game_state_callback: Box::new(game_state_callback), }) } /// Is this game over for any reason? #[wasm_bindgen] pub fn is_done(&self) -> bool { match self.state { GameState::DrawScore { .. } | GameState::DrawInvalid | GameState::Error | GameState::BlueWins { .. } | GameState::RedWins { .. } => true, _ => false, } } #[wasm_bindgen] pub fn red_won(&self) -> Option { match self.state { GameState::DrawScore { .. } => Some(false), GameState::DrawInvalid { .. } => Some(false), GameState::BlueWins { .. } => Some(false), GameState::RedWins { .. } => Some(true), _ => None, } } #[wasm_bindgen] pub fn blue_won(&self) -> Option { match self.state { GameState::DrawScore { .. } => Some(false), GameState::DrawInvalid { .. } => Some(false), GameState::BlueWins { .. } => Some(true), GameState::RedWins { .. } => Some(false), _ => None, } } #[wasm_bindgen] pub fn is_draw_score(&self) -> Option { match self.state { GameState::DrawScore { .. } => Some(true), GameState::DrawInvalid { .. } => Some(false), GameState::BlueWins { .. } => Some(false), GameState::RedWins { .. } => Some(false), _ => None, } } #[wasm_bindgen] pub fn is_draw_invalid(&self) -> Option { match self.state { GameState::DrawScore { .. } => Some(false), GameState::DrawInvalid { .. } => Some(true), GameState::BlueWins { .. } => Some(false), GameState::RedWins { .. } => Some(false), _ => None, } } #[wasm_bindgen] pub fn is_error(&self) -> bool { match self.state { GameState::Error => true, _ => false, } } // Play one turn #[wasm_bindgen] pub fn step(&mut self) -> Result<(), String> { if self.is_first_print { self.print_board("", ""); (self.game_state_callback)("\r\n"); } let action = match (self.state, self.is_red_turn) { (GameState::Blue { .. }, false) => self.blue_agent.step_max(&self.board), (GameState::Red, false) => self.blue_agent.step_min(&self.board), (GameState::Blue { .. }, true) => self.red_agent.step_min(&self.board), (GameState::Red, true) => self.red_agent.step_max(&self.board), // Game is done, do nothing (GameState::Error, _) | (GameState::BlueWins { .. }, _) | (GameState::RedWins { .. }, _) | (GameState::DrawInvalid, _) | (GameState::DrawScore { .. }, _) => return Ok(()), } .map_err(|err| format!("{err}"))?; let player = self.is_red_turn.then_some("Red").unwrap_or("Blue"); let player_name = self .is_red_turn .then_some(self.red_agent.name()) .unwrap_or(self.blue_agent.name()); if !self.board.play(action, player) { self.state = GameState::Error; return Err(format!( "{player} ({player_name}) made an invalid move {action}", )); } self.print_board( self.is_red_turn.then_some(ansi::RED).unwrap_or(ansi::BLUE), self.is_red_turn.then_some("Red").unwrap_or("Blue"), ); (self.game_state_callback)("\n\r"); if !self.board.is_full() && !self.board.is_stuck() { // This was not the last move self.is_red_turn = !self.is_red_turn; } else { // This was the last move // Close board (self.game_state_callback)(&format!( "{}╙{}╜\n\r", " ".repeat(6), " ".repeat(self.board.size()) )); // Evaluate board and update state match (self.state, self.board.evaluate()) { // Start next round (GameState::Red, Some(red_score)) => { self.board = Board::new(); self.is_first_print = true; self.is_red_turn = false; self.state = GameState::Blue { red_score } } // Game over (GameState::Blue { red_score }, Some(blue_score)) => { self.state = match red_score.total_cmp(&blue_score) { Ordering::Equal => GameState::DrawScore { score: red_score }, Ordering::Greater => GameState::RedWins { blue_score }, Ordering::Less => GameState::BlueWins { blue_score }, } } // Could not evaluate board, tie by default (GameState::Red, None) | (GameState::Blue { .. }, None) => { self.state = GameState::DrawInvalid } // Other code should make sure this never happens (GameState::BlueWins { .. }, _) | (GameState::RedWins { .. }, _) | (GameState::DrawInvalid, _) | (GameState::DrawScore { .. }, _) | (GameState::Error, _) => unreachable!(), } if self.board.is_stuck() { self.state = GameState::DrawInvalid; } // Print depending on new state match self.state { GameState::DrawScore { score } => { (self.game_state_callback)(&format!("Tie! Score: {score:.2}\n\r")); } GameState::DrawInvalid => { (self.game_state_callback)(&format!("Tie, invalid board!\n\r")); } GameState::RedWins { blue_score, .. } => { (self.game_state_callback)(&format!( "{}Blue score:{} {blue_score:.2}\n\r", ansi::BLUE, ansi::RESET, )); (self.game_state_callback)(&format!( "{}Red wins!{}\n\r", ansi::RED, ansi::RESET, )); } GameState::BlueWins { blue_score, .. } => { (self.game_state_callback)(&format!( "{}Blue score:{} {blue_score:.2}\n\r", ansi::BLUE, ansi::RESET, )); (self.game_state_callback)(&format!( "{}Blue wins!{}\n\r", ansi::BLUE, ansi::RESET, )); } GameState::Blue { red_score } => { (self.game_state_callback)(&format!( "{}Red score:{} {red_score:.2}\n\r", ansi::RED, ansi::RESET, )); } // Other code should make sure this never happens GameState::Error | GameState::Red => unreachable!(), } (self.game_state_callback)("\r\n"); } return Ok(()); } fn print_board(&mut self, color: &str, player: &str) { let board_label = format!("{}{:<6}{}", color, player, ansi::RESET); (self.game_state_callback)(&format!( "\r{}{}{}{}", board_label, if self.is_first_print { '╓' } else { '║' }, self.board.prettyprint(), if self.is_first_print { '╖' } else { '║' }, )); self.is_first_print = false; } } // // MARK: tests // // TODO: // - infinite loop // - random is different // - incorrect return type // - globals #[test] fn full_random() { const SCRIPT: &str = r#" fn random_action(board) { let symb = rand_symb(); let pos = rand_int(0, 10); let action = Action(symb, pos); while !board.can_play(action) { let symb = rand_symb(); let pos = rand_int(0, 10); action = Action(symb, pos); } return action } fn step_min(board) { random_action(board) } fn step_max(board) { random_action(board) } "#; let mut game = MinMaxGame::new_native(&SCRIPT, |_| {}, |_| {}, &SCRIPT, |_| {}, |_| {}, |_| {}).unwrap(); let mut n = 0; while !game.is_done() { println!("{:?}", game.step()); println!("{:?}", game.board); n += 1; assert!(n < 10); } } #[test] fn infinite_loop() { const SCRIPT: &str = r#" fn step_min(board) { loop {} } fn step_max(board) { loop {} } "#; let mut game = MinMaxGame::new_native(&SCRIPT, |_| {}, |_| {}, &SCRIPT, |_| {}, |_| {}, |_| {}).unwrap(); while !game.is_done() { println!("{:?}", game.step()); println!("{:?}", game.board); } }