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:
Steven Wroblewski
2026-09-30 20:39:47 +02:00
parent 88d8069f8e
commit aae0eccc8b
3 changed files with 237 additions and 0 deletions
+1
View File
@@ -9,4 +9,5 @@
pub mod codec;
pub mod hash;
pub mod map;
pub mod mapgen;
pub mod rng;
+104
View File
@@ -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
}
+132
View File
@@ -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));
}