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(()); } }