A
This commit is contained in:
28
rust/script-runner/Cargo.toml
Normal file
28
rust/script-runner/Cargo.toml
Normal 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"] }
|
||||
54
rust/script-runner/README.md
Normal file
54
rust/script-runner/README.md
Normal 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)
|
||||
30
rust/script-runner/package.json
Normal file
30
rust/script-runner/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
4
rust/script-runner/src/colors.rs
Normal file
4
rust/script-runner/src/colors.rs
Normal 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";
|
||||
317
rust/script-runner/src/lib.rs
Normal file
317
rust/script-runner/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user