This commit is contained in:
2025-10-29 20:36:09 -07:00
commit d90a9b5826
33 changed files with 3239 additions and 0 deletions

View File

@@ -0,0 +1,28 @@
[package]
name = "script-runner"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
minimax = { workspace = true }
anyhow = { workspace = true }
rand = { workspace = true }
wasm-bindgen = { workspace = true }
js-sys = { workspace = true }
web-sys = { workspace = true }
console_error_panic_hook = { workspace = true }
wee_alloc = { workspace = true }
serde = { workspace = true }
serde-wasm-bindgen = { workspace = true }
rhai = { workspace = true }
getrandom = { workspace = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
rhai = { workspace = true, features = ["wasm-bindgen"] }
getrandom = { workspace = true, features = ["js"] }

View File

@@ -0,0 +1,54 @@
# Script Runner WASM
A placeholder Rust script runner using wasm-bindgen.
## Building
```bash
# Install wasm-pack if not already installed
cargo install wasm-pack
# Build for web
npm run build
# Build for different targets
npm run build:nodejs # For Node.js
npm run build:bundler # For bundlers like webpack
npm run dev # Development build
```
## Usage
```javascript
import init, { ScriptRunner, greet, add } from './pkg/script_runner.js';
async function run() {
await init();
// Basic functions
greet("World");
console.log(add(1, 2)); // 3
// Script runner
const runner = new ScriptRunner();
runner.set_context("my_context");
console.log(runner.get_context()); // "my_context"
const result = runner.run_script("console.log('Hello from script')");
console.log(result);
// Evaluate expressions
console.log(runner.evaluate("hello")); // "Hello from Rust!"
console.log(runner.evaluate("version")); // "0.1.0"
}
run();
```
## Features
- Basic WASM initialization with panic hooks
- ScriptRunner class for managing execution context
- Simple expression evaluation
- Console logging integration
- Multiple build targets (web, nodejs, bundler)

View File

@@ -0,0 +1,30 @@
{
"name": "script-runner-wasm",
"version": "0.1.0",
"description": "Rust WASM script runner",
"main": "pkg/script_runner.js",
"types": "pkg/script_runner.d.ts",
"files": [
"pkg"
],
"scripts": {
"build": "wasm-pack build --target web --out-dir pkg",
"build:nodejs": "wasm-pack build --target nodejs --out-dir pkg-node",
"build:bundler": "wasm-pack build --target bundler --out-dir pkg-bundler",
"dev": "wasm-pack build --dev --target web --out-dir pkg"
},
"repository": {
"type": "git",
"url": "."
},
"keywords": [
"wasm",
"rust",
"script-runner"
],
"author": "",
"license": "MIT",
"devDependencies": {
"wasm-pack": "^0.12.1"
}
}

View File

@@ -0,0 +1,4 @@
pub const RESET: &str = "\x1b[0m";
pub const RED: &str = "\x1b[31m";
pub const BLUE: &str = "\x1b[34m";
pub const MAGENTA: &str = "\x1b[35m";

View File

@@ -0,0 +1,317 @@
use minimax::{
agents::{Agent, Rhai},
board::Board,
};
use rand::{rngs::StdRng, SeedableRng};
use wasm_bindgen::prelude::*;
mod colors;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
pub fn init_panic_hook() {
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub struct GameState {
max_agent: Rhai<StdRng>,
max_name: String,
min_agent: Rhai<StdRng>,
min_name: String,
board: Board,
max_turn: bool,
game_state_callback: Box<dyn Fn(&str) + 'static>,
}
#[wasm_bindgen]
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,
min_script: &str,
min_name: &str,
min_print_callback: js_sys::Function,
min_debug_callback: js_sys::Function,
game_state_callback: js_sys::Function,
) -> GameState {
Self::new_native(
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));
},
min_script,
min_name,
move |s| {
let _ = min_print_callback.call1(&JsValue::null(), &JsValue::from_str(s));
},
move |s| {
let _ = min_debug_callback.call1(&JsValue::null(), &JsValue::from_str(s));
},
move |s| {
let _ = game_state_callback.call1(&JsValue::null(), &JsValue::from_str(s));
},
)
}
fn new_native(
max_script: &str,
max_name: &str,
max_print_callback: impl Fn(&str) + 'static,
max_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,
game_state_callback: impl Fn(&str) + 'static,
) -> GameState {
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();
GameState {
board: Board::new(),
max_turn: true,
max_name: max_name.to_owned(),
max_agent: Rhai::new(
max_script,
StdRng::from_seed(seed1),
max_print_callback,
max_debug_callback,
),
min_name: min_name.to_owned(),
min_agent: Rhai::new(
min_script,
StdRng::from_seed(seed2),
min_print_callback,
min_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!("{}{}{}", colors::RED, current_player, colors::RESET)
} else {
format!("{}{}{}", colors::BLUE, current_player, colors::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!(
"{}{}{}",
colors::MAGENTA,
symbol_str,
colors::RESET
));
} else {
result.push_str(&symbol_str);
}
}
None => result.push('_'),
}
}
result.push('║');
result
}
// Play one turn
#[wasm_bindgen]
pub fn step(&mut self) -> Result<Option<String>, String> {
if self.is_done() {
return Ok(None);
}
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 action {
Ok(x) => x,
Err(err) => {
return Err(format!("{err:?}"));
}
};
if !self.board.play(
action,
self.max_turn
.then_some(&self.max_name)
.unwrap_or(&self.min_name)
.to_owned(),
) {
let error_msg = format!(
"{} {} ({}) made an invalid move {}!",
format!("{}ERROR:{}", colors::RED, colors::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()),
action
);
// Print error to game state callback
(self.game_state_callback)(&error_msg);
return Ok(None);
}
self.max_turn = !self.max_turn;
// Print board state after move to terminal (via game state callback)
let board_display = self.format_board_display();
(self.game_state_callback)(&board_display);
// 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);
}
}
return Ok(Some(
self.max_turn
.then_some(&self.min_name)
.unwrap_or(&self.max_name)
.to_owned(),
));
}
}
// TODO: tests
// - 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",
|_| {},
|_| {},
|_| {},
);
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",
|_| {},
|_| {},
|_| {},
);
while !game.is_done() {
println!("{:?}", game.step());
println!("{:?}", game.board);
}
}