Files
Verbatim/tests/worldgen.rs
T
2026-06-21 22:27:20 +03:00

269 lines
7.0 KiB
Rust

use std::collections::VecDeque;
use verbatim::ai::GameSession;
fn init_at_depth(depth: u32) -> GameSession {
let mut s = GameSession::new_seeded(123);
s.game.depth = depth;
s.init();
s
}
fn count_distinct_materials(s: &GameSession) -> usize {
let mut present = Vec::new();
for y in 0..s.game.grid.height as i32 {
for x in 0..s.game.grid.width as i32 {
let mat = s.game.grid.get(x, y).material;
if !present.contains(&mat) {
present.push(mat);
}
}
}
present.len()
}
fn total_empty(s: &GameSession) -> usize {
let mut count = 0;
for y in 0..s.game.grid.height as i32 {
for x in 0..s.game.grid.width as i32 {
if s.game.grid.get(x, y).is_empty() {
count += 1;
}
}
}
count
}
fn player_not_in_solid(s: &GameSession) -> bool {
let (px, py) = s.game.player.center(&s.game.entities);
let ix = px as i32;
let iy = py as i32;
for dy in -1..=1 {
for dx in -1..=1 {
if s.game.grid.get(ix + dx, iy + dy).is_solid() {
return false;
}
}
}
true
}
fn flood_fill_count(s: &GameSession, x: i32, y: i32) -> usize {
let w = s.game.grid.width as i32;
let h = s.game.grid.height as i32;
let mut visited = vec![false; (w * h) as usize];
let mut q = VecDeque::new();
q.push_back((x, y));
let mut count = 0;
while let Some((cx, cy)) = q.pop_front() {
let idx = (cy * w + cx) as usize;
if visited[idx] || !s.game.grid.in_bounds(cx, cy) || !s.game.grid.get(cx, cy).is_empty() {
continue;
}
visited[idx] = true;
count += 1;
for (dx, dy) in [(0, 1), (0, -1), (1, 0), (-1, 0)] {
q.push_back((cx + dx, cy + dy));
}
}
count
}
fn count_empty_regions(s: &GameSession) -> usize {
let w = s.game.grid.width as i32;
let h = s.game.grid.height as i32;
let mut visited = vec![false; (w * h) as usize];
let mut regions = 0;
for y in 0..h {
for x in 0..w {
let idx = (y * w + x) as usize;
if !visited[idx] && s.game.grid.get(x, y).is_empty() {
regions += 1;
let mut stack = vec![(x, y)];
while let Some((cx, cy)) = stack.pop() {
let i = (cy * w + cx) as usize;
if visited[i]
|| !s.game.grid.in_bounds(cx, cy)
|| !s.game.grid.get(cx, cy).is_empty()
{
continue;
}
visited[i] = true;
for (dx, dy) in [(0, 1), (0, -1), (1, 0), (-1, 0)] {
stack.push((cx + dx, cy + dy));
}
}
}
}
}
regions
}
#[test]
fn surface_generation_has_stairs_and_open_spawn() {
let s = init_at_depth(1);
assert!(
s.find_material("stairs").is_some(),
"surface should have stairs"
);
assert!(
player_not_in_solid(&s),
"player should spawn in an open cell"
);
assert!(
count_distinct_materials(&s) >= 3,
"surface should have several materials"
);
}
#[test]
fn cave_generation_has_stairs_and_connected_empty() {
let s = init_at_depth(4);
assert!(
s.find_material("stairs").is_some(),
"cave should have stairs"
);
assert!(
player_not_in_solid(&s),
"player should spawn in an open cell"
);
let empty = total_empty(&s);
assert!(
empty > 100,
"cave should have a meaningful empty region: {}",
empty
);
let (px, py) = s.game.player.center(&s.game.entities);
let connected = flood_fill_count(&s, px as i32, py as i32);
assert!(
connected >= empty * 95 / 100,
"cave should be mostly connected: {} of {}",
connected,
empty
);
assert_eq!(
count_empty_regions(&s),
1,
"cave should be a single connected empty region"
);
}
#[test]
fn dungeon_generation_has_stairs_and_rooms() {
let s = init_at_depth(7);
assert!(
s.find_material("stairs").is_some(),
"dungeon should have stairs"
);
assert!(
player_not_in_solid(&s),
"player should spawn in an open cell"
);
assert!(
count_distinct_materials(&s) >= 3,
"dungeon should have several materials"
);
let empty = total_empty(&s);
assert!(
empty > 500,
"dungeon should have many room cells: {}",
empty
);
let regions = count_empty_regions(&s);
assert!(
regions >= 1 && regions <= 4,
"dungeon rooms should be connected or nearly connected: {} regions",
regions
);
}
#[test]
fn different_depths_produce_different_structures() {
let s1 = init_at_depth(1);
let s2 = init_at_depth(4);
let s3 = init_at_depth(7);
let empty1 = total_empty(&s1);
let empty2 = total_empty(&s2);
let empty3 = total_empty(&s3);
assert!(
empty1 != empty2 || empty2 != empty3,
"depths should differ in empty space: {} {} {}",
empty1,
empty2,
empty3
);
let grass1 = s1.count_material_in_region(
0,
0,
s1.game.grid.width as i32,
s1.game.grid.height as i32,
"grass",
);
let grass2 = s2.count_material_in_region(
0,
0,
s2.game.grid.width as i32,
s2.game.grid.height as i32,
"grass",
);
let grass3 = s3.count_material_in_region(
0,
0,
s3.game.grid.width as i32,
s3.game.grid.height as i32,
"grass",
);
assert!(
grass1 > grass2 && grass2 == grass3,
"grass should dominate surface and vanish in deeper levels: {} {} {}",
grass1,
grass2,
grass3
);
}
#[test]
fn dungeon_has_large_empty_rooms() {
let s = init_at_depth(8);
let mut found_room = false;
for y in 5..s.game.grid.height as i32 - 5 {
for x in 5..s.game.grid.width as i32 - 5 {
let mut w = 0;
while x + w < s.game.grid.width as i32 - 5 && s.game.grid.get(x + w, y).is_empty() {
w += 1;
}
let mut h = 0;
while y + h < s.game.grid.height as i32 - 5 && s.game.grid.get(x, y + h).is_empty() {
h += 1;
}
if w >= 5 && h >= 5 {
found_room = true;
}
}
}
assert!(found_room, "dungeon should contain rooms at least 5x5");
}
#[test]
fn world_generation_respects_seeds() {
let s1 = init_at_depth(3);
let s2 = init_at_depth(3);
let (p1, _) = s1.game.player.center(&s1.game.entities);
let (p2, _) = s2.game.player.center(&s2.game.entities);
assert!(
(p1 - p2).abs() < 0.01,
"same seed should place player at the same x"
);
let m1 = s1.find_material("stairs");
let m2 = s2.find_material("stairs");
assert_eq!(m1, m2, "same seed should place stairs at the same location");
}