From 53fdd2409321dc73a231dfa0b7be5cd2fcfeb0ca Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 1 Nov 2025 10:01:59 -0700 Subject: [PATCH] Bulk run --- rust/runner/src/lib.rs | 338 ++++++++++++------- webui/src/components/Editor.tsx | 1 - webui/src/components/Playground.tsx | 68 +++- webui/src/lib/runner.ts | 46 ++- webui/src/lib/worker_bulk.ts | 106 ++++++ webui/src/lib/{worker.ts => worker_human.ts} | 29 -- webui/src/utils/wasmLoader.ts | 4 +- 7 files changed, 421 insertions(+), 171 deletions(-) create mode 100644 webui/src/lib/worker_bulk.ts rename webui/src/lib/{worker.ts => worker_human.ts} (76%) diff --git a/rust/runner/src/lib.rs b/rust/runner/src/lib.rs index b0116c2..5afa0a7 100644 --- a/rust/runner/src/lib.rs +++ b/rust/runner/src/lib.rs @@ -25,14 +25,19 @@ pub fn init_panic_hook() { #[wasm_bindgen] pub struct GameState { - max_agent: Rhai, - max_name: String, + red_agent: Rhai, + red_name: String, - min_agent: Rhai, - min_name: String, + blue_agent: Rhai, + blue_name: String, board: Board, - max_turn: bool, + 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, } @@ -41,34 +46,34 @@ pub struct GameState { impl GameState { #[wasm_bindgen(constructor)] pub fn new( - max_script: &str, - max_name: &str, - max_print_callback: js_sys::Function, - max_debug_callback: js_sys::Function, + red_script: &str, + red_name: &str, + red_print_callback: js_sys::Function, + red_debug_callback: js_sys::Function, - min_script: &str, - min_name: &str, - min_print_callback: js_sys::Function, - min_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( - max_script, - max_name, + red_script, + red_name, move |s| { - let _ = max_print_callback.call1(&JsValue::null(), &JsValue::from_str(s)); + let _ = red_print_callback.call1(&JsValue::null(), &JsValue::from_str(s)); }, move |s| { - let _ = max_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s)); + let _ = red_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s)); }, - min_script, - min_name, + blue_script, + blue_name, move |s| { - let _ = min_print_callback.call1(&JsValue::null(), &JsValue::from_str(s)); + let _ = blue_print_callback.call1(&JsValue::null(), &JsValue::from_str(s)); }, move |s| { - let _ = min_debug_callback.call1(&JsValue::null(), &JsValue::from_str(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)); @@ -78,15 +83,15 @@ impl GameState { } fn new_native( - max_script: &str, - max_name: &str, - max_print_callback: impl Fn(&str) + 'static, - max_debug_callback: impl Fn(&str) + 'static, + red_script: &str, + red_name: &str, + red_print_callback: impl Fn(&str) + 'static, + red_debug_callback: impl Fn(&str) + 'static, - min_script: &str, - min_name: &str, - min_print_callback: impl Fn(&str) + 'static, - min_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 { @@ -99,140 +104,221 @@ impl GameState { Ok(GameState { board: Board::new(), - max_turn: true, + is_first_turn: true, + is_error: false, + red_score: None, + is_red_turn: true, + red_is_maximizer: true, + red_won: None, - max_name: max_name.to_owned(), - max_agent: Rhai::new( - max_script, + red_name: red_name.to_owned(), + red_agent: Rhai::new( + red_script, StdRng::from_seed(seed1), - max_print_callback, - max_debug_callback, + red_print_callback, + red_debug_callback, )?, - min_name: min_name.to_owned(), - min_agent: Rhai::new( - min_script, + blue_name: blue_name.to_owned(), + blue_agent: Rhai::new( + blue_script, StdRng::from_seed(seed2), - min_print_callback, - min_debug_callback, + blue_print_callback, + blue_debug_callback, )?, game_state_callback: Box::new(game_state_callback), }) } - #[wasm_bindgen] - pub fn is_done(&self) -> bool { - self.board.is_full() - } - - /// If true, it is the max player's turn. - /// If false, it is the min player's turn. - #[wasm_bindgen] - pub fn is_max_turn(&self) -> bool { - self.max_turn - } - - fn format_board_display(&self) -> String { - let mut result = String::new(); - - // Board label with player name in gray - let current_player = if self.max_turn { - format!("{:<6}", self.max_name) - } else { - format!("{:<6}", self.min_name) - }; - - let colored_player = if self.max_turn { - format!("{}{}{}", ansi::RED, current_player, ansi::RESET) - } else { - format!("{}{}{}", ansi::BLUE, current_player, ansi::RESET) - }; - result.push_str(&format!("\r{}║", colored_player)); - - for (i, symbol) in self.board.get_board().iter().enumerate() { - match symbol { - Some(s) => { - // Highlight the last placed symbol in magenta, everything else normal - let symbol_str = s.to_string(); - if Some(i) == self.board.get_last_placed() { - result.push_str(&format!("{}{}{}", ansi::MAGENTA, symbol_str, ansi::RESET)); - } else { - result.push_str(&symbol_str); - } - } - None => result.push('_'), - } - } - result.push('║'); - - result - } - // Play one turn #[wasm_bindgen] - pub fn step(&mut self) -> Result, String> { + pub fn step(&mut self) -> Result<(), String> { if self.is_done() { - return Ok(None); + return Ok(()); } - let action = match self.is_max_turn() { - true => self.max_agent.step_max(&self.board), - false => self.min_agent.step_min(&self.board), + 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) => { - return Err(format!("{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.max_turn - .then_some(&self.max_name) - .unwrap_or(&self.min_name) + 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.max_turn - .then_some(&self.max_name) - .unwrap_or(&self.min_name), - self.max_turn - .then_some(self.max_agent.name()) - .unwrap_or(self.min_agent.name()), + 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 ); - // Print error to game state callback (self.game_state_callback)(&error_msg); - - return Ok(None); + self.is_error = true; + return Ok(()); } - self.max_turn = !self.max_turn; + if self.board.is_full() { + self.print_end(); + return Ok(()); + } - // Print board state after move to terminal (via game state callback) - let board_display = self.format_board_display(); - (self.game_state_callback)(&board_display); + 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"); - // Show final score if game is complete - if self.is_done() { - if let Some(score) = self.board.evaluate() { - let score_msg = format!("\nFinal score: {:.2}", score); - (self.game_state_callback)(&score_msg); + 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, + )); } } - - return Ok(Some( - self.max_turn - .then_some(&self.min_name) - .unwrap_or(&self.max_name) - .to_owned(), - )); } } @@ -494,7 +580,7 @@ impl GameStateHuman { } } - pub fn print_end(&mut self) { + fn print_end(&mut self) { let board_label = format!( "{}{:<6}{}", self.is_human_turn @@ -609,7 +695,8 @@ fn full_random() { |_| {}, |_| {}, |_| {}, - ); + ) + .unwrap(); let mut n = 0; while !game.is_done() { @@ -643,7 +730,8 @@ fn infinite_loop() { |_| {}, |_| {}, |_| {}, - ); + ) + .unwrap(); while !game.is_done() { println!("{:?}", game.step()); diff --git a/webui/src/components/Editor.tsx b/webui/src/components/Editor.tsx index b61a4b9..1399b96 100644 --- a/webui/src/components/Editor.tsx +++ b/webui/src/components/Editor.tsx @@ -46,7 +46,6 @@ if (typeof window !== "undefined") { interface EditorProps { initialValue?: string; onChange?: (editor: any, changes: any) => void; - onRequestRun?: () => void; onReady?: (editor: any) => void; } diff --git a/webui/src/components/Playground.tsx b/webui/src/components/Playground.tsx index d5c6cd5..2693ac3 100644 --- a/webui/src/components/Playground.tsx +++ b/webui/src/components/Playground.tsx @@ -5,7 +5,12 @@ import { Button } from "@/components/ui/Button"; import { Dropdown } from "@/components/ui/Dropdown"; import { Editor } from "@/components/Editor"; import { Terminal, TerminalRef } from "@/components/Terminal"; -import { sendDataToScript, startScript, stopScript } from "@/lib/runner"; +import { + sendDataToScript, + startScript, + startScriptBulk, + stopScript, +} from "@/lib/runner"; import styles from "@/styles/Playground.module.css"; const initialCode = ` @@ -49,7 +54,7 @@ export default function Playground() { const runDisabled = isScriptRunning || !isEditorReady; const stopDisabled = !isScriptRunning; - const requestRun = useCallback(async () => { + const runHuman = useCallback(async () => { if (resultRef.current) { resultRef.current.value = ""; } @@ -94,6 +99,51 @@ export default function Playground() { setIsScriptRunning(false); }, [runDisabled]); + const runBulk = useCallback(async () => { + if (resultRef.current) { + resultRef.current.value = ""; + } + + if (runDisabled || !editorRef.current) return; + + setIsScriptRunning(true); + + try { + terminalRef.current?.clear(); + terminalRef.current?.focus(); + + await startScriptBulk( + editorRef.current.getValue(), + (line: string) => { + if (resultRef.current) { + let v = resultRef.current.value + line + "\n"; + if (v.length > 10000) { + v = v.substring(v.length - 10000); + } + resultRef.current.value = v; + resultRef.current.scrollTop = + resultRef.current.scrollHeight - + resultRef.current.clientHeight; + } + }, + + (line: string) => { + terminalRef.current?.write(line); + } + ); + } catch (ex) { + const errorMsg = `\nEXCEPTION: "${ex}"\n`; + if (resultRef.current) { + resultRef.current.value += errorMsg; + } + terminalRef.current?.write( + "\r\n\x1B[1;31mEXCEPTION:\x1B[0m " + String(ex) + "\r\n" + ); + } + + setIsScriptRunning(false); + }, [runDisabled]); + const stopScriptHandler = useCallback(() => { stopScript(); }, []); @@ -106,9 +156,7 @@ export default function Playground() {