M1a: deterministischer Rust-Kern #3
@@ -9,4 +9,5 @@
|
|||||||
pub mod codec;
|
pub mod codec;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
|
pub mod mapgen;
|
||||||
pub mod rng;
|
pub mod rng;
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//! Generator for the milestone 1 test map.
|
||||||
|
//!
|
||||||
|
//! The broad layout is fixed and the details are random: a bedrock border, a
|
||||||
|
//! clearing around the spawn point, a rock outcrop right next to the
|
||||||
|
//! clearing, cave-like soft rock in the inner area and hard rock further out.
|
||||||
|
|
||||||
|
use crate::map::{Map, Pos, Terrain, Tile};
|
||||||
|
use crate::rng::Rng;
|
||||||
|
|
||||||
|
/// Edge length of the test map, in tiles.
|
||||||
|
pub const TEST_MAP_SIZE: u32 = 64;
|
||||||
|
/// Tiles within this distance of the spawn point are always open ground.
|
||||||
|
pub const SPAWN_CLEARING_RADIUS: i32 = 5;
|
||||||
|
/// Edge length of the soft rock outcrop that every test map has.
|
||||||
|
pub const OUTCROP_SIZE: i32 = 4;
|
||||||
|
/// Rock at least this many tiles from the spawn point, counted along the
|
||||||
|
/// longer axis, is hard rock.
|
||||||
|
pub const HARD_ROCK_DISTANCE: i32 = 22;
|
||||||
|
|
||||||
|
const INITIAL_ROCK_PERCENT: u32 = 46;
|
||||||
|
const SMOOTHING_PASSES: u32 = 4;
|
||||||
|
/// Keeps the map sequence apart from other uses of the same seed.
|
||||||
|
const SEED_SALT: u64 = 0x6D61_7067_656E_0001;
|
||||||
|
|
||||||
|
/// Spawn point of a map: its centre tile.
|
||||||
|
pub fn spawn_pos(map: &Map) -> Pos {
|
||||||
|
Pos::new((map.width() / 2) as i32, (map.height() / 2) as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top left tile of the soft rock outcrop next to the spawn clearing.
|
||||||
|
pub fn outcrop_origin(map: &Map) -> Pos {
|
||||||
|
let spawn = spawn_pos(map);
|
||||||
|
Pos::new(spawn.x + SPAWN_CLEARING_RADIUS + 2, spawn.y - 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the test map for `seed`. The same seed always gives the same map.
|
||||||
|
pub fn generate_test_map(seed: u64) -> Map {
|
||||||
|
let size = TEST_MAP_SIZE;
|
||||||
|
let mut rng = Rng::new(seed ^ SEED_SALT);
|
||||||
|
|
||||||
|
let mut rock = vec![false; (size as usize) * (size as usize)];
|
||||||
|
for cell in rock.iter_mut() {
|
||||||
|
*cell = rng.below(100) < INITIAL_ROCK_PERCENT;
|
||||||
|
}
|
||||||
|
for _ in 0..SMOOTHING_PASSES {
|
||||||
|
rock = smooth(&rock, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut map = Map::filled(size, size, Terrain::Ground);
|
||||||
|
let spawn = spawn_pos(&map);
|
||||||
|
let outcrop = outcrop_origin(&map);
|
||||||
|
let last = size as i32 - 1;
|
||||||
|
|
||||||
|
for y in 0..=last {
|
||||||
|
for x in 0..=last {
|
||||||
|
let dx = x - spawn.x;
|
||||||
|
let dy = y - spawn.y;
|
||||||
|
let on_border = x == 0 || y == 0 || x == last || y == last;
|
||||||
|
let in_clearing = dx * dx + dy * dy <= SPAWN_CLEARING_RADIUS * SPAWN_CLEARING_RADIUS;
|
||||||
|
let in_outcrop = (outcrop.x..outcrop.x + OUTCROP_SIZE).contains(&x)
|
||||||
|
&& (outcrop.y..outcrop.y + OUTCROP_SIZE).contains(&y);
|
||||||
|
let is_rock = rock[(y as usize) * (size as usize) + (x as usize)];
|
||||||
|
|
||||||
|
let terrain = if on_border {
|
||||||
|
Terrain::Bedrock
|
||||||
|
} else if in_clearing {
|
||||||
|
Terrain::Ground
|
||||||
|
} else if in_outcrop {
|
||||||
|
Terrain::SoftRock
|
||||||
|
} else if !is_rock {
|
||||||
|
Terrain::Ground
|
||||||
|
} else if dx.abs().max(dy.abs()) >= HARD_ROCK_DISTANCE {
|
||||||
|
Terrain::HardRock
|
||||||
|
} else {
|
||||||
|
Terrain::SoftRock
|
||||||
|
};
|
||||||
|
map.set(Pos::new(x, y), Tile::new(terrain));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One pass of cave smoothing: a cell becomes rock when at least five of the
|
||||||
|
/// nine cells around it, itself included, are rock. Cells outside the grid
|
||||||
|
/// count as rock.
|
||||||
|
fn smooth(rock: &[bool], size: u32) -> Vec<bool> {
|
||||||
|
let size = size as i32;
|
||||||
|
let mut out = vec![false; rock.len()];
|
||||||
|
for y in 0..size {
|
||||||
|
for x in 0..size {
|
||||||
|
let mut count = 0;
|
||||||
|
for ny in y - 1..=y + 1 {
|
||||||
|
for nx in x - 1..=x + 1 {
|
||||||
|
let outside = nx < 0 || ny < 0 || nx >= size || ny >= size;
|
||||||
|
if outside || rock[(ny * size + nx) as usize] {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[(y * size + x) as usize] = count >= 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
//! Rules the map generator has to keep for every seed.
|
||||||
|
|
||||||
|
use grovewright_core::map::{Map, Pos, Terrain};
|
||||||
|
use grovewright_core::mapgen::{
|
||||||
|
generate_test_map, outcrop_origin, spawn_pos, HARD_ROCK_DISTANCE, OUTCROP_SIZE,
|
||||||
|
SPAWN_CLEARING_RADIUS, TEST_MAP_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Number of seeds every rule is checked against.
|
||||||
|
const SEEDS: u64 = 300;
|
||||||
|
|
||||||
|
fn terrain_at(map: &Map, x: i32, y: i32) -> Terrain {
|
||||||
|
map.get(Pos::new(x, y))
|
||||||
|
.expect("position is on the map")
|
||||||
|
.terrain
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_same_seed_gives_the_same_map() {
|
||||||
|
assert_eq!(generate_test_map(42), generate_test_map(42));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_seeds_give_different_maps() {
|
||||||
|
assert_ne!(generate_test_map(1), generate_test_map(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_map_has_the_test_size() {
|
||||||
|
let map = generate_test_map(0);
|
||||||
|
assert_eq!((map.width(), map.height()), (TEST_MAP_SIZE, TEST_MAP_SIZE));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bedrock_forms_the_border_and_nothing_else() {
|
||||||
|
let last = TEST_MAP_SIZE as i32 - 1;
|
||||||
|
for seed in 0..SEEDS {
|
||||||
|
let map = generate_test_map(seed);
|
||||||
|
for y in 0..=last {
|
||||||
|
for x in 0..=last {
|
||||||
|
let on_border = x == 0 || y == 0 || x == last || y == last;
|
||||||
|
let is_bedrock = terrain_at(&map, x, y) == Terrain::Bedrock;
|
||||||
|
assert_eq!(is_bedrock, on_border, "seed {seed}, tile {x}/{y}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_spawn_clearing_is_open_ground() {
|
||||||
|
for seed in 0..SEEDS {
|
||||||
|
let map = generate_test_map(seed);
|
||||||
|
let spawn = spawn_pos(&map);
|
||||||
|
for dy in -SPAWN_CLEARING_RADIUS..=SPAWN_CLEARING_RADIUS {
|
||||||
|
for dx in -SPAWN_CLEARING_RADIUS..=SPAWN_CLEARING_RADIUS {
|
||||||
|
if dx * dx + dy * dy > SPAWN_CLEARING_RADIUS * SPAWN_CLEARING_RADIUS {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let terrain = terrain_at(&map, spawn.x + dx, spawn.y + dy);
|
||||||
|
assert_eq!(terrain, Terrain::Ground, "seed {seed}, offset {dx}/{dy}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_soft_rock_outcrop_lies_next_to_the_clearing() {
|
||||||
|
for seed in 0..SEEDS {
|
||||||
|
let map = generate_test_map(seed);
|
||||||
|
let spawn = spawn_pos(&map);
|
||||||
|
let origin = outcrop_origin(&map);
|
||||||
|
assert_eq!(origin.x - spawn.x, SPAWN_CLEARING_RADIUS + 2, "seed {seed}");
|
||||||
|
for y in origin.y..origin.y + OUTCROP_SIZE {
|
||||||
|
for x in origin.x..origin.x + OUTCROP_SIZE {
|
||||||
|
assert_eq!(
|
||||||
|
terrain_at(&map, x, y),
|
||||||
|
Terrain::SoftRock,
|
||||||
|
"seed {seed}, tile {x}/{y}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hard_rock_appears_only_far_from_the_spawn_point() {
|
||||||
|
let last = TEST_MAP_SIZE as i32 - 1;
|
||||||
|
for seed in 0..SEEDS {
|
||||||
|
let map = generate_test_map(seed);
|
||||||
|
let spawn = spawn_pos(&map);
|
||||||
|
for y in 1..last {
|
||||||
|
for x in 1..last {
|
||||||
|
let far = (x - spawn.x).abs().max((y - spawn.y).abs()) >= HARD_ROCK_DISTANCE;
|
||||||
|
match terrain_at(&map, x, y) {
|
||||||
|
Terrain::HardRock => assert!(far, "seed {seed}: hard rock near spawn"),
|
||||||
|
Terrain::SoftRock => assert!(!far, "seed {seed}: soft rock far out"),
|
||||||
|
Terrain::Ground | Terrain::Bedrock => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_map_mixes_rock_and_open_ground() {
|
||||||
|
for seed in 0..SEEDS {
|
||||||
|
let map = generate_test_map(seed);
|
||||||
|
let total = map.tiles().len();
|
||||||
|
let rock = map
|
||||||
|
.tiles()
|
||||||
|
.iter()
|
||||||
|
.filter(|tile| !tile.terrain.is_walkable())
|
||||||
|
.count();
|
||||||
|
let percent = rock * 100 / total;
|
||||||
|
assert!(
|
||||||
|
(20..=80).contains(&percent),
|
||||||
|
"seed {seed}: {percent} percent rock"
|
||||||
|
);
|
||||||
|
let hard = map
|
||||||
|
.tiles()
|
||||||
|
.iter()
|
||||||
|
.filter(|tile| tile.terrain == Terrain::HardRock)
|
||||||
|
.count();
|
||||||
|
assert!(hard > 0, "seed {seed}: no hard rock");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_generated_tile_starts_damaged() {
|
||||||
|
let map = generate_test_map(5);
|
||||||
|
assert!(map.tiles().iter().all(|tile| tile.damage == 0));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user