feat: AI integration - pipe protocol, test framework, replay system
AI Session API (src/ai/session.rs): - GameSession wraps Game with seeded determinism - init/init_empty, step(n), perform_action, get_state - get_cell, get_region, get_entities, get_player, count_material JSON Pipe Protocol (src/ai/protocol.rs): - --mode pipe: stdin/stdout JSON line protocol - Commands: init, step, action, get_state, get_view, get_cell, get_region, get_entities, get_player, count_material, find_material, record_start/stop, replay_save, run_scenario, quit Test Framework: - 27 Rust integration tests (tests/*.rs) covering sand, water, lava, acid, fire physics + entity movement, damage, ragdoll - 8 JSON scenarios (scenarios/*.json) runnable via --mode test - Scenario assertions: cell_is, cell_is_not, no_material_in_region, material_count_in_region, entity_alive/dead, player_on_ground, etc. Replay System (src/ai/replay.rs): - Record all actions + seed for deterministic playback - ReplayPlayer::play() and play_until_tick() for debugging - --mode replay --replay-file PATH Other changes: - src/lib.rs: library target for integration tests - Collision: post-constraint collision pass to reduce tunneling - serde + serde_json dependencies - All enums use rename_all = snake_case for JSON compatibility
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
use verbatim::ai::GameSession;
|
||||
use verbatim::ai::AiAction;
|
||||
|
||||
fn setup_empty() -> GameSession {
|
||||
let mut s = GameSession::new_seeded(42);
|
||||
s.init_empty();
|
||||
s.clear_area(95, 95, 40, 40);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_takes_lava_damage() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 125, w: 20, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 116, y: 123, w: 4, h: 2, material: "lava".into() });
|
||||
s.perform_action(&AiAction::Spawn { kind: "goblin".into(), x: 118.0, y: 120.0 });
|
||||
s.step(60);
|
||||
let entities = s.get_entities();
|
||||
let goblin = entities.into_iter().find(|e| e.kind == "Goblin");
|
||||
assert!(goblin.is_some(), "goblin should exist");
|
||||
let g = goblin.unwrap();
|
||||
assert!(g.health < 40.0, "goblin should have taken damage from lava, hp={}", g.health);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_dies_becomes_corpse() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 125, w: 20, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::Spawn { kind: "goblin".into(), x: 110.0, y: 120.0 });
|
||||
s.step(30);
|
||||
s.perform_action(&AiAction::DamageEntity { id: 1, amount: 100.0 });
|
||||
s.step(1);
|
||||
let entities = s.get_entities();
|
||||
let goblin = entities.into_iter().find(|e| e.id == 1);
|
||||
assert!(goblin.is_some(), "entity should still exist");
|
||||
let g = goblin.unwrap();
|
||||
assert!(!g.alive, "entity should be dead after 100 damage");
|
||||
assert_eq!(g.kind, "Corpse", "dead entity should be a corpse, got {}", g.kind);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_on_fire_takes_damage_over_time() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 125, w: 20, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 116, y: 123, w: 4, h: 2, material: "lava".into() });
|
||||
s.perform_action(&AiAction::Spawn { kind: "goblin".into(), x: 118.0, y: 120.0 });
|
||||
s.step(20);
|
||||
let entities = s.get_entities();
|
||||
let goblin = entities.into_iter().find(|e| e.id == 1);
|
||||
if let Some(g) = goblin {
|
||||
if g.on_fire {
|
||||
let hp_after_fire = g.health;
|
||||
s.step(30);
|
||||
let entities2 = s.get_entities();
|
||||
if let Some(g2) = entities2.into_iter().find(|e| e.id == 1) {
|
||||
assert!(g2.health < hp_after_fire, "entity on fire should lose more health over time");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entity_blocked_by_stone() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 130, w: 30, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 110, y: 126, w: 1, h: 4, material: "stone".into() });
|
||||
s.perform_action(&AiAction::Spawn { kind: "goblin".into(), x: 105.0, y: 125.0 });
|
||||
s.step(30);
|
||||
let entities = s.get_entities();
|
||||
if let Some(g) = entities.into_iter().find(|e| e.id == 1) {
|
||||
assert!(g.pos[0] < 110.0, "goblin should be blocked by stone wall, got x={}", g.pos[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use verbatim::ai::GameSession;
|
||||
use verbatim::ai::AiAction;
|
||||
|
||||
fn setup_empty() -> GameSession {
|
||||
let mut s = GameSession::new_seeded(42);
|
||||
s.init_empty();
|
||||
s.clear_area(95, 95, 40, 35);
|
||||
s.perform_action(&AiAction::FillRect { x: 80, y: 135, w: 80, h: 15, material: "stone".into() });
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_falls_and_lands() {
|
||||
let mut s = GameSession::new_seeded(42);
|
||||
s.init_empty();
|
||||
s.clear_area(120, 128, 10, 5);
|
||||
s.perform_action(&AiAction::FillRect { x: 118, y: 132, w: 14, h: 10, material: "stone".into() });
|
||||
s.step(30);
|
||||
let player = s.get_player().expect("player should exist");
|
||||
assert!(player.alive, "player should be alive");
|
||||
assert!(player.pos[1] < 132.0, "player should land on stone at y=132, got y={}", player.pos[1]);
|
||||
assert!(player.pos[1] > 125.0, "player should have fallen from center");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_blocked_by_stone_wall() {
|
||||
let mut s = setup_empty();
|
||||
s.step(30);
|
||||
let player = s.get_player().expect("player should exist");
|
||||
let x = player.pos[0] as i32;
|
||||
s.perform_action(&AiAction::FillRect { x: x + 5, y: 125, w: 1, h: 10, material: "stone".into() });
|
||||
s.perform_action(&AiAction::MoveRight);
|
||||
s.step(10);
|
||||
let player = s.get_player().expect("player should exist");
|
||||
assert!(player.pos[0] < (x + 5) as f32, "player should be blocked by wall");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_can_move_right() {
|
||||
let mut s = setup_empty();
|
||||
s.step(30);
|
||||
let player = s.get_player().expect("player should exist");
|
||||
let initial_x = player.pos[0];
|
||||
s.perform_action(&AiAction::MoveRight);
|
||||
s.step(10);
|
||||
let player = s.get_player().expect("player should exist");
|
||||
assert!(player.pos[0] > initial_x, "player should have moved right: {} -> {}", initial_x, player.pos[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn player_survives_fall() {
|
||||
let mut s = setup_empty();
|
||||
s.step(60);
|
||||
let player = s.get_player().expect("player should exist");
|
||||
assert!(player.alive, "player should survive a fall onto stone");
|
||||
assert!(player.health > 50.0, "player should not take significant damage from landing, hp={}", player.health);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use verbatim::ai::GameSession;
|
||||
use verbatim::ai::AiAction;
|
||||
use verbatim::ai::{ReplayRecorder, ReplayPlayer};
|
||||
|
||||
#[test]
|
||||
fn replay_deterministic() {
|
||||
let mut s1 = GameSession::new_seeded(123);
|
||||
s1.init();
|
||||
s1.set_recording(true);
|
||||
s1.perform_action(&AiAction::MoveRight);
|
||||
s1.step(10);
|
||||
s1.perform_action(&AiAction::Jump);
|
||||
s1.step(10);
|
||||
let state1 = s1.get_state();
|
||||
|
||||
s1.save_replay("/tmp/verbatim_test_replay.json").expect("save replay");
|
||||
|
||||
let player = ReplayPlayer::load("/tmp/verbatim_test_replay.json").expect("load replay");
|
||||
let s2 = player.play();
|
||||
let state2 = s2.get_state();
|
||||
|
||||
assert_eq!(state1.tick, state2.tick, "ticks should match");
|
||||
if let (Some(p1), Some(p2)) = (&state1.player, &state2.player) {
|
||||
assert_eq!(p1.health, p2.health, "player health should match");
|
||||
assert!((p1.pos[0] - p2.pos[0]).abs() < 0.01, "player x should match: {} vs {}", p1.pos[0], p2.pos[0]);
|
||||
assert!((p1.pos[1] - p2.pos[1]).abs() < 0.01, "player y should match: {} vs {}", p1.pos[1], p2.pos[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_play_until_tick() {
|
||||
let mut s = GameSession::new_seeded(999);
|
||||
s.init();
|
||||
s.set_recording(true);
|
||||
s.perform_action(&AiAction::MoveRight);
|
||||
s.step(5);
|
||||
s.perform_action(&AiAction::MoveLeft);
|
||||
s.step(5);
|
||||
s.perform_action(&AiAction::Jump);
|
||||
s.step(10);
|
||||
s.save_replay("/tmp/verbatim_test_replay2.json").expect("save");
|
||||
|
||||
let player = ReplayPlayer::load("/tmp/verbatim_test_replay2.json").expect("load");
|
||||
let s_half = player.play_until_tick(5);
|
||||
assert_eq!(s_half.tick(), 5, "should stop at tick 5, got {}", s_half.tick());
|
||||
|
||||
let s_full = player.play();
|
||||
assert_eq!(s_full.tick(), 20, "full replay should reach tick 20, got {}", s_full.tick());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_seed_same_state() {
|
||||
let mut s1 = GameSession::new_seeded(42);
|
||||
s1.init();
|
||||
s1.step(30);
|
||||
let state1 = s1.get_state();
|
||||
|
||||
let mut s2 = GameSession::new_seeded(42);
|
||||
s2.init();
|
||||
s2.step(30);
|
||||
let state2 = s2.get_state();
|
||||
|
||||
assert_eq!(state1.tick, state2.tick);
|
||||
if let (Some(p1), Some(p2)) = (&state1.player, &state2.player) {
|
||||
assert!((p1.pos[0] - p2.pos[0]).abs() < 0.01, "x mismatch: {} vs {}", p1.pos[0], p2.pos[0]);
|
||||
assert!((p1.pos[1] - p2.pos[1]).abs() < 0.01, "y mismatch: {} vs {}", p1.pos[1], p2.pos[1]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_protocol_init_and_step() {
|
||||
use std::io::Write;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_verbatim"))
|
||||
.arg("--mode").arg("pipe")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("failed to start process");
|
||||
|
||||
let stdin = child.stdin.as_mut().expect("failed to open stdin");
|
||||
writeln!(stdin, "{{\"cmd\":\"init\",\"seed\":42}}").expect("write init");
|
||||
stdin.flush().expect("flush");
|
||||
|
||||
let mut output = String::new();
|
||||
let stdout = child.stdout.as_mut().expect("failed to open stdout");
|
||||
use std::io::Read;
|
||||
let mut buf = [0u8; 4096];
|
||||
let n = stdout.read(&mut buf).expect("read");
|
||||
output.push_str(&String::from_utf8_lossy(&buf[..n]));
|
||||
|
||||
let json: serde_json::Value = serde_json::from_str(output.trim()).expect("parse response");
|
||||
assert_eq!(json["ok"], true, "init should succeed: {}", output);
|
||||
|
||||
writeln!(stdin, "{{\"cmd\":\"step\",\"n\":10}}").expect("write step");
|
||||
stdin.flush().expect("flush");
|
||||
let n = stdout.read(&mut buf).expect("read");
|
||||
let output2 = String::from_utf8_lossy(&buf[..n]).to_string();
|
||||
let json2: serde_json::Value = serde_json::from_str(output2.trim()).expect("parse step response");
|
||||
assert_eq!(json2["ok"], true);
|
||||
assert_eq!(json2["state"]["tick"], 10, "tick should be 10 after stepping 10");
|
||||
|
||||
writeln!(stdin, "{{\"cmd\":\"quit\"}}").expect("write quit");
|
||||
stdin.flush().expect("flush");
|
||||
child.wait().expect("wait");
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use verbatim::ai::GameSession;
|
||||
use verbatim::ai::AiAction;
|
||||
|
||||
fn setup_empty() -> GameSession {
|
||||
let mut s = GameSession::new_seeded(42);
|
||||
s.init_empty();
|
||||
s.clear_area(95, 95, 40, 40);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acid_dissolves_wood() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 120, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 104, y: 118, material: "wood".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 119, material: "acid".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 104, y: 119, material: "acid".into() });
|
||||
s.step(30);
|
||||
assert_ne!(s.get_cell(104, 118).material, "wood", "acid should have dissolved the wood");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acid_does_not_dissolve_stone() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 110, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 111, material: "acid".into() });
|
||||
s.step(20);
|
||||
assert_eq!(s.get_cell(105, 110).material, "stone", "acid should not dissolve stone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acid_flows_down() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 120, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 105, material: "acid".into() });
|
||||
s.step(20);
|
||||
assert_ne!(s.get_cell(105, 105).material, "acid", "acid should have flowed down from y=105");
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use verbatim::ai::GameSession;
|
||||
use verbatim::ai::AiAction;
|
||||
|
||||
fn setup_empty() -> GameSession {
|
||||
let mut s = GameSession::new_seeded(42);
|
||||
s.init_empty();
|
||||
s.clear_area(95, 95, 40, 40);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lava_flows_down() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 115, w: 10, h: 3, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 110, material: "lava".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 111, material: "lava".into() });
|
||||
s.step(10);
|
||||
let lava_count = s.count_material_in_region(103, 110, 5, 6, "lava");
|
||||
let stone_count = s.count_material_in_region(103, 110, 5, 6, "stone");
|
||||
assert!(lava_count > 0 || stone_count >= 4,
|
||||
"lava should have flowed down or cooled to stone, lava={} stone={}", lava_count, stone_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lava_plus_water_makes_steam() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 120, w: 20, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 99, w: 20, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 110, material: "lava".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 106, y: 110, material: "water".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 109, material: "lava".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 106, y: 109, material: "water".into() });
|
||||
s.step(20);
|
||||
let lava_remaining = s.count_material_in_region(100, 105, 20, 15, "lava");
|
||||
assert_eq!(lava_remaining, 0, "all lava should have been converted by water");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lava_ignites_wood() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 115, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 104, y: 114, material: "wood".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 114, material: "wood".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 106, y: 114, material: "wood".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 113, material: "lava".into() });
|
||||
s.step(15);
|
||||
let wood_remaining = s.count_material_in_region(103, 113, 5, 3, "wood");
|
||||
assert_eq!(wood_remaining, 0, "all wood should have been ignited by lava, got {} wood cells", wood_remaining);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lava_ignites_grass() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 115, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 114, w: 5, h: 1, material: "grass".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 113, material: "lava".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 114, material: "lava".into() });
|
||||
s.step(15);
|
||||
let grass_remaining = s.count_material_in_region(99, 113, 7, 3, "grass");
|
||||
assert_eq!(grass_remaining, 0, "grass should have been ignited by lava");
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use verbatim::ai::GameSession;
|
||||
use verbatim::ai::AiAction;
|
||||
use verbatim::world::cell::MaterialId;
|
||||
|
||||
fn setup_empty() -> GameSession {
|
||||
let mut s = GameSession::new_seeded(42);
|
||||
s.init_empty();
|
||||
s.clear_area(95, 95, 30, 40);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sand_falls_down() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 115, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 105, material: "sand".into() });
|
||||
s.step(10);
|
||||
assert_eq!(s.get_cell(105, 105).material, "empty", "sand should have fallen from y=105");
|
||||
assert_eq!(s.get_cell(105, 114).material, "sand", "sand should be resting on stone at y=114");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sand_displaces_water() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 115, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 110, w: 10, h: 5, material: "water".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 105, material: "sand".into() });
|
||||
s.step(20);
|
||||
let sand_at_bottom = s.get_cell(105, 114).material == "sand";
|
||||
assert!(sand_at_bottom, "sand should sink to bottom through water");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sand_piles_on_stone() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 115, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 105, material: "sand".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 104, material: "sand".into() });
|
||||
s.step(15);
|
||||
let count = s.count_material_in_region(104, 112, 3, 4, "sand");
|
||||
assert!(count >= 2, "both sand cells should have piled up, got {} sand cells", count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sand_does_not_fall_through_stone() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 110, w: 10, h: 1, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 105, y: 105, material: "sand".into() });
|
||||
s.step(10);
|
||||
assert_eq!(s.get_cell(105, 109).material, "sand", "sand should rest on top of stone");
|
||||
assert_eq!(s.get_cell(105, 110).material, "stone", "stone should remain");
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use verbatim::ai::GameSession;
|
||||
use verbatim::ai::AiAction;
|
||||
|
||||
fn setup_empty() -> GameSession {
|
||||
let mut s = GameSession::new_seeded(42);
|
||||
s.init_empty();
|
||||
s.clear_area(95, 95, 40, 40);
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn water_flows_down() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 95, y: 115, w: 40, h: 3, material: "stone".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 110, y: 105, material: "water".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 110, y: 106, material: "water".into() });
|
||||
s.perform_action(&AiAction::SetCell { x: 110, y: 107, material: "water".into() });
|
||||
s.step(20);
|
||||
let water_near_bottom = s.count_material_in_region(105, 112, 10, 4, "water");
|
||||
assert!(water_near_bottom > 0, "water should have flowed down to near the stone floor, found {} water cells near bottom", water_near_bottom);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn water_spreads_sideways() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 95, y: 115, w: 50, h: 3, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 110, y: 111, w: 1, h: 4, material: "water".into() });
|
||||
s.step(50);
|
||||
let left_count = s.count_material_in_region(100, 110, 10, 5, "water");
|
||||
let right_count = s.count_material_in_region(111, 110, 10, 5, "water");
|
||||
assert!(left_count > 0 || right_count > 0, "water should spread sideways: left={} right={}", left_count, right_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn water_does_not_pass_through_stone_wall() {
|
||||
let mut s = setup_empty();
|
||||
s.perform_action(&AiAction::FillRect { x: 95, y: 115, w: 50, h: 3, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 115, y: 110, w: 1, h: 5, material: "stone".into() });
|
||||
s.perform_action(&AiAction::FillRect { x: 100, y: 110, w: 15, h: 5, material: "water".into() });
|
||||
s.step(30);
|
||||
let right_water = s.count_material_in_region(116, 108, 10, 10, "water");
|
||||
assert_eq!(right_water, 0, "water should not pass through stone wall");
|
||||
}
|
||||
Reference in New Issue
Block a user