269 lines
7.0 KiB
Rust
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");
|
|
}
|