#![cfg(all(feature = "compile", feature = "verify"))]
use std::sync::{Arc, Mutex};
use keleusma::bytecode::Value;
use keleusma::compiler::compile;
use keleusma::lexer::tokenize;
use keleusma::parser::parse;
use keleusma::vm::{DEFAULT_ARENA_CAPACITY, Vm, VmError, VmState};
use keleusma::{Arena, Module};
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn word_tuple(arena: &keleusma::Arena, v: &Value) -> Vec<i64> {
use keleusma::bytecode::TupleBody;
use keleusma::value_layout::ScalarKind;
match v {
Value::Tuple(TupleBody::Boxed(elems)) => elems
.iter()
.map(|e| match e {
Value::Int(n) => *n,
other => panic!("non-int tuple element: {:?}", other),
})
.collect(),
Value::Tuple(TupleBody::Flat(fc)) => {
let bytes = fc
.resolve(arena)
.expect("flat tuple body resolves (read-before-resume)");
(0..bytes.len() / 8)
.map(|i| {
match Value::read_scalar_le(bytes, i * 8, ScalarKind::Int, 8, 8)
.expect("flat tuple element decodes")
{
Value::Int(n) => n,
other => panic!("non-int flat tuple element: {:?}", other),
}
})
.collect()
}
other => panic!("expected a Word tuple, got {:?}", other),
}
}
const SRC_BESTIARY: &str = include_str!("../examples/scripts/rogue/rogue_bestiary.kel");
const SRC_GEAR: &str = include_str!("../examples/scripts/rogue/rogue_gear.kel");
const SRC_DUNGEN: &str = include_str!("../examples/scripts/rogue/rogue_dungen.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_IDLE: &str = include_str!("../examples/scripts/rogue/rogue_ai_idle.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_CHASER: &str = include_str!("../examples/scripts/rogue/rogue_ai_chaser.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_WANDER: &str = include_str!("../examples/scripts/rogue/rogue_ai_wander.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_SLEEPER: &str = include_str!("../examples/scripts/rogue/rogue_ai_sleeper.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_RANGED: &str = include_str!("../examples/scripts/rogue/rogue_ai_ranged.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_FAST: &str = include_str!("../examples/scripts/rogue/rogue_ai_fast.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_SMART: &str = include_str!("../examples/scripts/rogue/rogue_ai_smart.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_AI_BOSS: &str = include_str!("../examples/scripts/rogue/rogue_ai_boss.kel");
const SRC_AI_TRACKER: &str = include_str!("../examples/scripts/rogue/rogue_ai_tracker.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_ITEM_POTION: &str = include_str!("../examples/scripts/rogue/rogue_item_potion.kel");
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
const SRC_ITEM_SCROLL: &str = include_str!("../examples/scripts/rogue/rogue_item_scroll.kel");
const SRC_GAME: &str = include_str!("../examples/scripts/rogue/rogue_game.kel");
const SRC_PLAYER_AI: &str = include_str!("../examples/scripts/rogue/rogue_player_ai.kel");
const SRC_COMBAT: &str = include_str!("../examples/scripts/rogue/rogue_combat.kel");
fn build(src: &str) -> Module {
let tokens = tokenize(src).expect("lex error");
let program = parse(&tokens).expect("parse error");
compile(&program).expect("compile error")
}
struct DungenStub {
rng_state: u32,
map_set_count: usize,
spawn_monster_count: usize,
spawn_item_count: usize,
place_player: Option<(i32, i32)>,
place_stairs: Option<(i32, i32)>,
place_exit: Option<(i32, i32)>,
clear_count: usize,
}
impl DungenStub {
fn new() -> Self {
Self {
rng_state: 0x9E37_79B9,
map_set_count: 0,
spawn_monster_count: 0,
spawn_item_count: 0,
place_player: None,
place_stairs: None,
place_exit: None,
clear_count: 0,
}
}
fn rng_next(&mut self) -> u32 {
let mut s = self.rng_state;
s ^= s << 13;
s ^= s >> 17;
s ^= s << 5;
self.rng_state = s;
s
}
}
fn register_dungen_stub(vm: &mut Vm, state: &Arc<Mutex<DungenStub>>) {
let s = state.clone();
vm.register_native_closure(
"host::rng_range",
Box::new(move |args: &[Value]| -> Result<Value, VmError> {
let lo = match args[0] {
Value::Int(n) => n,
_ => 0,
};
let hi = match args[1] {
Value::Int(n) => n,
_ => 1,
};
if hi <= lo {
return Err(VmError::NativeError(format!(
"rng_range: hi {} not greater than lo {}",
hi, lo
)));
}
let mut st = s.lock().unwrap();
let r = st.rng_next() % (hi - lo) as u32;
Ok(Value::Int(lo + r as i64))
}),
);
let s = state.clone();
vm.register_native_closure(
"host::map_set",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> {
s.lock().unwrap().map_set_count += 1;
Ok(Value::Unit)
}),
);
vm.register_native_closure(
"host::map_get",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> { Ok(Value::Int(1)) }),
);
vm.register_native_closure(
"host::map_w",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> { Ok(Value::Int(80)) }),
);
vm.register_native_closure(
"host::map_h",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> { Ok(Value::Int(24)) }),
);
let s = state.clone();
vm.register_native_closure(
"host::clear_floor",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> {
s.lock().unwrap().clear_count += 1;
Ok(Value::Unit)
}),
);
let s = state.clone();
vm.register_native_closure(
"host::place_player",
Box::new(move |args: &[Value]| -> Result<Value, VmError> {
let (x, y) = match (&args[0], &args[1]) {
(Value::Int(x), Value::Int(y)) => (*x as i32, *y as i32),
_ => (0, 0),
};
s.lock().unwrap().place_player = Some((x, y));
Ok(Value::Unit)
}),
);
let s = state.clone();
vm.register_native_closure(
"host::place_stairs",
Box::new(move |args: &[Value]| -> Result<Value, VmError> {
let (x, y) = match (&args[0], &args[1]) {
(Value::Int(x), Value::Int(y)) => (*x as i32, *y as i32),
_ => (0, 0),
};
s.lock().unwrap().place_stairs = Some((x, y));
Ok(Value::Unit)
}),
);
let s = state.clone();
vm.register_native_closure(
"host::place_exit",
Box::new(move |args: &[Value]| -> Result<Value, VmError> {
let (x, y) = match (&args[0], &args[1]) {
(Value::Int(x), Value::Int(y)) => (*x as i32, *y as i32),
_ => (0, 0),
};
s.lock().unwrap().place_exit = Some((x, y));
Ok(Value::Unit)
}),
);
let s = state.clone();
vm.register_native_closure(
"host::spawn_monster",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> {
s.lock().unwrap().spawn_monster_count += 1;
Ok(Value::Unit)
}),
);
let s = state.clone();
vm.register_native_closure(
"host::spawn_item",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> {
s.lock().unwrap().spawn_item_count += 1;
Ok(Value::Unit)
}),
);
vm.register_native_closure(
"host::floor",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> { Ok(Value::Int(1)) }),
);
}
#[test]
fn dungen_compiles() {
let _ = build(SRC_DUNGEN);
}
#[test]
fn game_tick_compiles() {
let _ = build(SRC_GAME);
}
#[test]
fn player_ai_compiles() {
let _ = build(SRC_PLAYER_AI);
}
#[test]
fn ai_tracker_compiles() {
let _ = build(SRC_AI_TRACKER);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_tracker_chases_when_seen() {
let module = build(SRC_AI_TRACKER);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
let input = Value::tuple(vec![
Value::Int(5),
Value::Int(5),
Value::Int(10),
Value::Int(10),
Value::Int(1),
]);
let result = vm.call_with_shared(&mut shared, &[input]).expect("vm call");
match result {
VmState::Yielded(ref v @ Value::Tuple(_)) => {
let c = word_tuple(&arena, v);
assert_eq!(c.len(), 3, "expected Yielded triple");
assert_eq!(c[0], 1);
assert_eq!((c[1], c[2]), (6, 6));
}
other => panic!("expected Yielded triple, got {:?}", other),
}
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_tracker_pursues_last_known_when_unseen() {
let module = build(SRC_AI_TRACKER);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
let visible_input = Value::tuple(vec![
Value::Int(5),
Value::Int(5),
Value::Int(10),
Value::Int(10),
Value::Int(1),
]);
vm.call_with_shared(&mut shared, &[visible_input])
.expect("vm call");
let unseen_input = Value::tuple(vec![
Value::Int(6),
Value::Int(6),
Value::Int(0),
Value::Int(0),
Value::Int(0),
]);
let mut state = vm
.resume_with_shared(&mut shared, unseen_input.clone())
.expect("vm resume");
for _ in 0..16 {
match state {
VmState::Yielded(ref v @ Value::Tuple(_)) => {
let c = word_tuple(&arena, v);
assert_eq!(c.len(), 3, "non-int tuple");
assert_eq!(c[0], 1, "should chase last known");
assert_eq!((c[1], c[2]), (7, 7), "step toward (10, 10)");
return;
}
VmState::Reset => {
state = vm
.resume_with_shared(&mut shared, unseen_input.clone())
.expect("vm resume");
}
other => panic!("expected Yielded or Reset, got {:?}", other),
}
}
panic!("tracker did not yield within sixteen resumes");
}
#[test]
fn combat_compiles() {
let _ = build(SRC_COMBAT);
}
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn run_player_ai(mx: i64, my: i64, cmd: i64) -> (i64, i64, i64) {
let module = build(SRC_PLAYER_AI);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let result = vm
.call(&[Value::Int(mx), Value::Int(my), Value::Int(cmd)])
.expect("vm call");
match result {
VmState::Finished(ref v @ Value::Tuple(_)) => {
let c = word_tuple(&arena, v);
assert_eq!(c.len(), 3, "player ai returned non-int tuple components");
(c[0], c[1], c[2])
}
other => panic!("expected Finished triple, got {:?}", other),
}
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn player_ai_wait_returns_action_zero() {
let (action, tx, ty) = run_player_ai(5, 5, 0);
assert_eq!(action, 0);
assert_eq!((tx, ty), (5, 5));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn player_ai_move_north_returns_action_one() {
let (action, tx, ty) = run_player_ai(5, 5, 1);
assert_eq!(action, 1);
assert_eq!((tx, ty), (5, 4));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn player_ai_move_diagonal_southeast() {
let (action, tx, ty) = run_player_ai(5, 5, 8);
assert_eq!(action, 1);
assert_eq!((tx, ty), (6, 6));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn player_ai_descend_returns_action_three() {
let (action, _tx, _ty) = run_player_ai(5, 5, 9);
assert_eq!(action, 3);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn player_ai_quaff_returns_action_four() {
let (action, _tx, _ty) = run_player_ai(5, 5, 10);
assert_eq!(action, 4);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn player_ai_read_returns_action_five() {
let (action, _tx, _ty) = run_player_ai(5, 5, 11);
assert_eq!(action, 5);
}
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn run_combat(skill: i64, dmg: i64, evasion: i64, armor: i64, roll: i64) -> (i64, i64) {
let module = build(SRC_COMBAT);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let result = vm
.call(&[
Value::Int(skill),
Value::Int(dmg),
Value::Int(evasion),
Value::Int(armor),
Value::Int(roll),
])
.expect("vm call");
match result {
VmState::Finished(ref v @ Value::Tuple(_)) => {
let c = word_tuple(&arena, v);
assert_eq!(c.len(), 2, "combat returned non-int tuple");
(c[0], c[1])
}
other => panic!("expected Finished pair, got {:?}", other),
}
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn combat_fumble_always_misses() {
let (hit, dmg) = run_combat(20, 10, 0, 0, 1);
assert_eq!((hit, dmg), (0, 0));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn combat_critical_always_hits() {
let (hit, dmg) = run_combat(0, 5, 50, 0, 20);
assert_eq!(hit, 2);
assert_eq!(dmg, 10);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn combat_ordinary_hit_subtracts_armor() {
let (hit, dmg) = run_combat(10, 8, 0, 3, 12);
assert_eq!(hit, 1);
assert_eq!(dmg, 5);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn combat_miss_returns_zero_damage() {
let (hit, dmg) = run_combat(0, 8, 5, 0, 10);
assert_eq!((hit, dmg), (0, 0));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn combat_damage_floored_at_one() {
let (hit, dmg) = run_combat(10, 2, 0, 8, 12);
assert_eq!(hit, 1);
assert_eq!(dmg, 1);
}
#[test]
fn game_tick_runs_with_stubbed_natives() {
use std::cell::RefCell;
let module = build(SRC_GAME);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
let monster_calls = std::rc::Rc::new(RefCell::new(0_i64));
let book_calls = std::rc::Rc::new(RefCell::new(0_i64));
vm.register_native_closure(
"host::run_player_turn",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> { Ok(Value::Int(0)) }),
);
vm.register_native_closure(
"host::monster_count",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> { Ok(Value::Int(3)) }),
);
let mc = monster_calls.clone();
vm.register_native_closure(
"host::run_monster_ai",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> {
*mc.borrow_mut() += 1;
Ok(Value::Unit)
}),
);
let bc = book_calls.clone();
vm.register_native_closure(
"host::tick_book_keeping",
Box::new(move |_args: &[Value]| -> Result<Value, VmError> {
*bc.borrow_mut() += 1;
Ok(Value::Int(0))
}),
);
let result = vm
.call_with_shared(&mut shared, &[Value::Int(0)])
.expect("vm call");
match result {
VmState::Yielded(Value::Int(outcome)) => {
assert_eq!(outcome, 0, "first turn should yield continue");
}
other => panic!("expected Yielded(Int), got {:?}", other),
}
assert_eq!(
*monster_calls.borrow(),
3,
"run_monster_ai should fire once per declared monster"
);
assert_eq!(
*book_calls.borrow(),
1,
"tick_book_keeping should fire once per turn"
);
}
#[test]
fn bestiary_compiles() {
let _ = build(SRC_BESTIARY);
}
#[test]
fn bestiary_negative_one_returns_last_entry() {
let module = build(SRC_BESTIARY);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
vm.call_with_shared(&mut shared, &[Value::Int(-1)])
.expect("call -1");
let last_id = match vm.get_shared(&shared, 0).expect("get_shared id") {
Value::Int(n) => n,
other => panic!("expected Int, got {:?}", other),
};
assert_eq!(last_id, 99, "the bestiary ships with one hundred entries");
}
#[test]
fn bestiary_returns_name_as_text() {
let module = build(SRC_BESTIARY);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
let state = vm
.call_with_shared(&mut shared, &[Value::Int(0)])
.expect("call 0");
match state {
VmState::Finished(v) => {
let s = v.as_str_with_arena(&arena).expect("resolve").unwrap_or("");
assert_eq!(s, "Sewer Rat");
}
other => panic!("expected Finished, got {:?}", other),
}
}
#[test]
fn bestiary_corpse_data_derived_from_shape() {
let module = build(SRC_BESTIARY);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
vm.call_with_shared(&mut shared, &[Value::Int(0)])
.expect("call 0");
let read = |slot: usize| -> i64 {
match vm.get_shared(&shared, slot).expect("get_shared") {
Value::Int(n) => n,
_ => panic!(),
}
};
assert_eq!(read(16), 50, "Tiny corpse drop chance");
assert_eq!(read(17), 8, "Tiny corpse satiation");
assert_eq!(read(18), 0, "Tiny corpse hp delta");
}
#[test]
fn bestiary_entry_zero_is_sewer_rat_stats() {
let module = build(SRC_BESTIARY);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
vm.call_with_shared(&mut shared, &[Value::Int(0)])
.expect("call 0");
let read = |slot: usize| -> i64 {
match vm.get_shared(&shared, slot).expect("get_shared") {
Value::Int(n) => n,
_ => panic!(),
}
};
assert_eq!(read(0), 0, "id");
assert_eq!(read(1), 0, "shape Tiny");
assert_eq!(read(8), 3, "max_hp");
assert_eq!(read(13), 2, "ai Chaser");
assert_eq!(read(14), 1, "first_floor");
assert_eq!(read(15), 1, "score");
}
#[test]
fn gear_compiles() {
let _ = build(SRC_GEAR);
}
#[test]
fn gear_weapon_zero_is_fists_damage_two() {
let module = build(SRC_GEAR);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
vm.call_with_shared(&mut shared, &[Value::Int(0), Value::Int(0)])
.expect("call");
let value = match vm.get_shared(&shared, 1).expect("get_shared") {
Value::Int(n) => n,
_ => panic!(),
};
assert_eq!(value, 2, "fists damage");
}
#[test]
fn gear_armor_negative_one_is_last_guard_defense_forty() {
let module = build(SRC_GEAR);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
vm.call_with_shared(&mut shared, &[Value::Int(1), Value::Int(-1)])
.expect("call");
let read = |slot: usize| -> i64 {
match vm.get_shared(&shared, slot).expect("get_shared") {
Value::Int(n) => n,
_ => panic!(),
}
};
assert_eq!(read(0), 19, "last guard id");
assert_eq!(read(1), 40, "last guard defense");
}
#[test]
fn dungen_runs_floor_1() {
let module = build(SRC_DUNGEN);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let stub = Arc::new(Mutex::new(DungenStub::new()));
register_dungen_stub(&mut vm, &stub);
let mut shared = vec![0u8; vm.shared_data_bytes()];
let result = vm
.call_with_shared(&mut shared, &[Value::Int(1)])
.expect("vm call");
match result {
VmState::Finished(_) => {}
other => panic!("expected Finished, got {:?}", other),
}
let st = stub.lock().unwrap();
assert_eq!(st.clear_count, 1, "clear_floor should be called once");
assert!(st.place_player.is_some(), "player should be placed");
assert!(
st.place_stairs.is_some(),
"stairs should be placed on floor 1"
);
assert!(st.place_exit.is_none(), "exit should not appear on floor 1");
assert!(
st.spawn_monster_count > 0,
"at least one monster should spawn"
);
assert!(st.spawn_item_count > 0, "at least one item should spawn");
assert!(st.map_set_count > 100, "many map cells should be written");
}
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn call_ai(src: &str, mx: i64, my: i64, px: i64, py: i64, sees: i64) -> (i64, i64, i64) {
let module = build(src);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
vm.register_native_closure(
"host::rng_range",
Box::new(move |args: &[Value]| -> Result<Value, VmError> {
let lo = match args[0] {
Value::Int(n) => n,
_ => 0,
};
Ok(Value::Int(lo))
}),
);
let result = vm
.call(&[
Value::Int(mx),
Value::Int(my),
Value::Int(px),
Value::Int(py),
Value::Int(sees),
])
.expect("ai vm call");
match result {
VmState::Finished(ref v @ Value::Tuple(_)) => {
let c = word_tuple(&arena, v);
assert_eq!(c.len(), 3, "ai returned non-int tuple components");
(c[0], c[1], c[2])
}
other => panic!("expected Finished tuple, got {:?}", other),
}
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_idle_waits_in_place() {
let (action, tx, ty) = call_ai(SRC_AI_IDLE, 5, 5, 10, 10, 1);
assert_eq!(action, 0);
assert_eq!((tx, ty), (5, 5));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_chaser_steps_toward_player_when_seen() {
let (action, tx, ty) = call_ai(SRC_AI_CHASER, 5, 5, 10, 10, 1);
assert_eq!(action, 1);
assert_eq!((tx, ty), (6, 6));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_chaser_waits_when_unseen() {
let (action, _tx, _ty) = call_ai(SRC_AI_CHASER, 5, 5, 10, 10, 0);
assert_eq!(action, 0);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_wander_chases_when_seen() {
let (action, tx, ty) = call_ai(SRC_AI_WANDER, 5, 5, 10, 10, 1);
assert_eq!(action, 1);
assert_eq!((tx, ty), (6, 6));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_sleeper_chases_when_seen() {
let (action, tx, ty) = call_ai(SRC_AI_SLEEPER, 5, 5, 10, 10, 1);
assert_eq!(action, 1);
assert_eq!((tx, ty), (6, 6));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_sleeper_waits_when_unseen() {
let (action, _tx, _ty) = call_ai(SRC_AI_SLEEPER, 5, 5, 10, 10, 0);
assert_eq!(action, 0);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_ranged_attacks_when_distant() {
let (action, tx, ty) = call_ai(SRC_AI_RANGED, 5, 5, 10, 10, 1);
assert_eq!(action, 2);
assert_eq!((tx, ty), (10, 10));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_ranged_retreats_when_adjacent() {
let (action, tx, ty) = call_ai(SRC_AI_RANGED, 5, 5, 6, 5, 1);
assert_eq!(action, 1);
assert_eq!((tx, ty), (4, 5));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_fast_steps_toward_player() {
let (action, tx, ty) = call_ai(SRC_AI_FAST, 5, 5, 10, 10, 1);
assert_eq!(action, 1);
assert_eq!((tx, ty), (6, 6));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_smart_dominant_axis_step() {
let (action, tx, ty) = call_ai(SRC_AI_SMART, 5, 5, 20, 5, 1);
assert_eq!(action, 1);
assert_eq!((tx, ty), (6, 5));
}
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn call_boss_first(
mx: i64,
my: i64,
px: i64,
py: i64,
sees: i64,
) -> (i64, i64, i64, Vm<'static, 'static>, Vec<u8>) {
let module = build(SRC_AI_BOSS);
let arena: Arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let arena_ref: &'static Arena = Box::leak(Box::new(arena));
let mut vm: Vm<'static, 'static> = Vm::new(module, arena_ref).expect("vm new");
let mut shared = vec![0u8; vm.shared_data_bytes()];
let input = Value::tuple(vec![
Value::Int(mx),
Value::Int(my),
Value::Int(px),
Value::Int(py),
Value::Int(sees),
]);
let result = vm.call_with_shared(&mut shared, &[input]).expect("vm call");
let triple = match result {
VmState::Yielded(ref v @ Value::Tuple(_)) => {
let c = word_tuple(arena_ref, v);
assert_eq!(c.len(), 3, "boss yielded non-int tuple components");
(c[0], c[1], c[2])
}
other => panic!("expected Yielded triple, got {:?}", other),
};
(triple.0, triple.1, triple.2, vm, shared)
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_boss_first_turn_attacks_at_range_when_distant() {
let (action, tx, ty, _vm, _shared) = call_boss_first(5, 5, 12, 12, 1);
assert_eq!(action, 2);
assert_eq!((tx, ty), (12, 12));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_boss_first_turn_waits_when_unseen() {
let (action, _tx, _ty, _vm, _shared) = call_boss_first(5, 5, 12, 12, 0);
assert_eq!(action, 0);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn ai_boss_second_turn_chases() {
let (_a1, _x1, _y1, mut vm, mut shared) = call_boss_first(5, 5, 12, 12, 1);
let input = Value::tuple(vec![
Value::Int(5),
Value::Int(5),
Value::Int(12),
Value::Int(12),
Value::Int(1),
]);
let mut state = vm
.resume_with_shared(&mut shared, input.clone())
.expect("vm resume");
for _ in 0..16 {
match state {
VmState::Yielded(ref v @ Value::Tuple(_)) => {
let c = word_tuple(vm.arena(), v);
assert_eq!(c.len(), 3, "non-int tuple components");
assert_eq!(c[0], 1, "second turn should chase");
assert_eq!((c[1], c[2]), (6, 6), "should step diagonally toward player");
return;
}
VmState::Reset => {
state = vm
.resume_with_shared(&mut shared, input.clone())
.expect("vm resume after reset");
}
other => panic!("expected Yielded or Reset, got {:?}", other),
}
}
panic!("boss vm did not yield within sixteen resume cycles");
}
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn call_5_tuple(src: &str, args: &[i64]) -> (i64, i64, i64, i64, i64) {
let module = build(src);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let values: Vec<Value> = args.iter().map(|n| Value::Int(*n)).collect();
let result = vm.call(&values).expect("vm call");
match result {
VmState::Finished(ref v @ Value::Tuple(_)) => {
let c = word_tuple(&arena, v);
assert_eq!(c.len(), 5, "expected 5-tuple");
(c[0], c[1], c[2], c[3], c[4])
}
other => panic!("expected 5-tuple, got {:?}", other),
}
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn potion_healing_heals_five() {
let (hp, _, _, _, _) = call_5_tuple(SRC_ITEM_POTION, &[0, 5, 12]);
assert_eq!(hp, 5);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn potion_greater_healing_heals_fifteen() {
let (hp, _, _, _, _) = call_5_tuple(SRC_ITEM_POTION, &[1, 5, 30]);
assert_eq!(hp, 15);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn potion_restoration_returns_status_11() {
let (_, _, _, status, _) = call_5_tuple(SRC_ITEM_POTION, &[2, 5, 30]);
assert_eq!(status, 11);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn potion_poison_damages_three() {
let (hp, _, _, _, _) = call_5_tuple(SRC_ITEM_POTION, &[3, 5, 30]);
assert_eq!(hp, -3);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn potion_strength_raises_max_hp() {
let (hp, max_hp, _, _, _) = call_5_tuple(SRC_ITEM_POTION, &[5, 5, 12]);
assert_eq!((hp, max_hp), (2, 2));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn potion_skill_raises_skill() {
let (_, _, skill, _, _) = call_5_tuple(SRC_ITEM_POTION, &[6, 5, 12]);
assert_eq!(skill, 1);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn scroll_identify_returns_status_3() {
let (_, _, _, status, _) = call_5_tuple(SRC_ITEM_SCROLL, &[0]);
assert_eq!(status, 3);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn scroll_magic_mapping_returns_status_1() {
let (_, _, _, status, _) = call_5_tuple(SRC_ITEM_SCROLL, &[1]);
assert_eq!(status, 1);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn scroll_teleport_returns_status_2() {
let (_, _, _, status, _) = call_5_tuple(SRC_ITEM_SCROLL, &[2]);
assert_eq!(status, 2);
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn scroll_enchant_weapon_returns_status_4_arg_1() {
let (_, _, _, status, arg) = call_5_tuple(SRC_ITEM_SCROLL, &[3]);
assert_eq!((status, arg), (4, 1));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn scroll_enchant_armor_returns_status_5_arg_1() {
let (_, _, _, status, arg) = call_5_tuple(SRC_ITEM_SCROLL, &[4]);
assert_eq!((status, arg), (5, 1));
}
#[test]
#[cfg(not(any(
feature = "narrow-word-8",
feature = "narrow-word-16",
feature = "narrow-word-32"
)))]
fn dungen_runs_floor_100_places_exit() {
let module = build(SRC_DUNGEN);
let arena = Arena::with_capacity(DEFAULT_ARENA_CAPACITY);
let mut vm = Vm::new(module, &arena).expect("vm new");
let stub = Arc::new(Mutex::new(DungenStub::new()));
register_dungen_stub(&mut vm, &stub);
let mut shared = vec![0u8; vm.shared_data_bytes()];
vm.call_with_shared(&mut shared, &[Value::Int(100)])
.expect("vm call");
let st = stub.lock().unwrap();
assert!(st.place_exit.is_some(), "floor 100 should place the exit");
assert!(
st.place_stairs.is_none(),
"floor 100 should not place stairs down"
);
}