Generator für die Testkarte
Baut aus einer Startzahl eine 64 × 64 Felder große Karte mit Rand aus Grundgestein, Lichtung am Startpunkt, Felsblock daneben, weichem Fels innen und hartem Fels außen. Die Regeln sind für 300 Startzahlen geprüft. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,4 +9,5 @@
|
||||
pub mod codec;
|
||||
pub mod hash;
|
||||
pub mod map;
|
||||
pub mod mapgen;
|
||||
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