makepad/libs/rtsmap/tests/generate.rs
Admin c460905e2a maps + route: the self-baking Amsterdam test map, reversible mkmap weave, the rtsmap generator, road faces, guarded first-run
Squashed from work:
- rtsmap: one seeded generator for tiled strategy maps
- map: bake a runnable Amsterdam test map from inside the route app
- route: the first-run test map starts itself, and its buttons are guarded
- map: the test map bakes its road faces too, from the same shared pass
- map_tiles: mkmap-extract — the weave is reversible, world-cells reconstructible from world.mkmap
- route: the local dispatcher rides the hub — its copied engine is deleted, limits preserved (aicore P1)
- route + converse: off makepad_ai — the Agent seam moves to converse, route's cloud dispatcher rides the hub's Claude p
- ai-hub: chats run the machine election — route to a serving holder, wait on a loading one, claim and publish when open
- libs: the zero-warning sweep — stitch casts say what they mean, xatlas keeps upstream's surface quietly
2026-09-01 16:46:33 +02:00

310 lines
11 KiB
Rust

//! What the generator promises: the same seed makes the same map, every map
//! is legal against the world-grid contract, and every map is FAIR — which
//! is checked here independently of the generator's own retry loop.
use makepad_rtsmap::verify::{distance_field, passable_letter};
use makepad_rtsmap::{
amount, generate, generate_checked, pick_tiles, tiles, MapSpec, Style, Terrain, TileSet,
MASK_ALL,
};
fn spec(seed: u32, style: Style, players: u8) -> MapSpec {
MapSpec { seed, style, players, width: 64, height: 64, ..MapSpec::default() }
}
#[test]
fn rtsmap_same_seed_makes_the_same_map() {
for style in Style::ALL {
for seed in 1..=6u32 {
let s = spec(seed, style, 4);
let first = generate(&s);
let second = generate(&s);
assert_eq!(first, second, "{} seed {seed}", style.name());
}
}
}
#[test]
fn rtsmap_different_seeds_make_different_maps() {
let a = generate(&spec(1, Style::Temperate, 2));
let b = generate(&spec(2, Style::Temperate, 2));
assert_ne!(a.grid, b.grid);
}
#[test]
fn rtsmap_grid_uses_only_contract_letters() {
for style in Style::ALL {
let map = generate(&spec(11, style, 4));
for (index, letter) in map.grid.iter().enumerate() {
assert!(
matches!(letter, b'.' | b'#' | b'w' | b'r' | b'b' | b't'),
"cell {index} of {} is {:?}",
style.name(),
*letter as char
);
}
assert_eq!(map.grid.len(), map.width as usize * map.height as usize);
assert_eq!(map.grid_rows().len(), map.height as usize);
}
}
#[test]
fn rtsmap_every_style_and_player_count_generates_a_fair_map() {
for style in Style::ALL {
for players in [2u8, 3, 4, 6] {
for seed in 1..=8u32 {
let map = generate_checked(&spec(seed, style, players)).unwrap_or_else(|report| {
panic!("{} {players}p seed {seed}: {}", style.name(), report.summary())
});
assert_eq!(map.starts.len(), players as usize);
assert_eq!(map.houses.len(), players as usize);
}
}
}
}
#[test]
fn rtsmap_every_start_can_walk_to_every_other_start() {
for style in Style::ALL {
for players in [2u8, 4, 6] {
let map = generate(&spec(5, style, players));
let starts = map.starts.clone();
for from in &starts {
let field = distance_field(&map, (from.x, from.y));
for to in &starts {
let at = to.y as usize * map.width as usize + to.x as usize;
assert_ne!(
field[at],
u16::MAX,
"{} {players}p: ({},{}) cannot reach ({},{})",
style.name(),
from.x,
from.y,
to.x,
to.y
);
}
}
}
}
}
#[test]
fn rtsmap_every_start_has_a_buildable_pocket_and_a_field_in_reach() {
for style in Style::ALL {
let map = generate(&spec(3, style, 4));
for start in &map.starts {
assert!(start.pocket >= makepad_rtsmap::verify::POCKET_MIN, "{start:?}");
assert!(
start.resource_cells >= makepad_rtsmap::verify::RESOURCE_CELLS_MIN,
"{start:?}"
);
assert!(start.resource_distance <= makepad_rtsmap::RESOURCE_REACH, "{start:?}");
}
}
}
#[test]
fn rtsmap_resource_amount_follows_the_asked_for_density() {
let sparse = generate(&MapSpec { resources: amount("low"), ..spec(9, Style::Desert, 4) });
let heavy = generate(&MapSpec { resources: amount("heavy"), ..spec(9, Style::Desert, 4) });
assert!(
heavy.resources.len() > sparse.resources.len() * 2,
"sparse={} heavy={}",
sparse.resources.len(),
heavy.resources.len()
);
// Richest at the heart: a field's best cell must be near-full.
assert!(heavy.resources.iter().map(|r| r.stage).max().unwrap_or(0) >= 10);
assert!(heavy.resources.iter().all(|r| r.stage > 0 && r.stage <= 11));
}
#[test]
fn rtsmap_a_desert_has_no_water_and_a_temperate_map_does() {
let desert = generate(&spec(4, Style::Desert, 4));
assert!(!desert.terrain.contains(&Terrain::Water));
let temperate = generate(&MapSpec { water: 1.0, ..spec(4, Style::Temperate, 4) });
assert!(temperate.terrain.contains(&Terrain::Water));
// Shores exist wherever water does, or the bank is a cliff of nothing.
assert!(temperate.terrain.contains(&Terrain::Shore));
}
#[test]
fn rtsmap_plateaus_are_rimmed_and_reachable() {
let map = generate(&MapSpec { cliffs: 1.0, ..spec(7, Style::Desert, 4) });
assert!(map.terrain.contains(&Terrain::Cliff));
assert!(map.terrain.contains(&Terrain::Plateau));
// Every plateau top is walkable from a start: the ramp pass exists so
// high ground is ground.
let field = distance_field(&map, (map.starts[0].x, map.starts[0].y));
let tops = map
.terrain
.iter()
.enumerate()
.filter(|(_, t)| **t == Terrain::Plateau)
.count();
let reached = map
.terrain
.iter()
.enumerate()
.filter(|(at, t)| **t == Terrain::Plateau && field[*at] != u16::MAX)
.count();
assert!(tops > 0);
assert!(
reached * 4 >= tops * 3,
"only {reached}/{tops} plateau cells are walkable from a start"
);
}
#[test]
fn rtsmap_roads_cross_water_so_the_map_stays_connected() {
let map = generate(&MapSpec { water: 1.0, roads: 1.0, ..spec(2, Style::Temperate, 4) });
assert!(map.terrain.contains(&Terrain::Road));
// Every road cell is passable, including the ones that were river.
for (at, terrain) in map.terrain.iter().enumerate() {
if *terrain == Terrain::Road {
assert!(passable_letter(map.grid[at]));
}
}
}
#[test]
fn rtsmap_tile_picking_is_stable_and_edge_matched() {
let map = generate(&spec(6, Style::Desert, 4));
let mut set = TileSet::new();
// A pretend pack: one interior piece per terrain, plus authored edges
// for the raised ground.
for terrain in [
Terrain::Clear,
Terrain::Rough,
Terrain::Shore,
Terrain::Road,
Terrain::Water,
Terrain::Cliff,
Terrain::Plateau,
Terrain::Resource,
] {
set.push_single(terrain, 1000 + terrain as u32);
}
for mask in 0..MASK_ALL {
set.push_masked(Terrain::Cliff, mask, 2000 + mask as u32);
}
let first = pick_tiles(&map, &set, 17);
assert_eq!(first, pick_tiles(&map, &set, 17));
assert_eq!(first.len(), map.terrain.len());
// A cliff cell whose neighbourhood is not full must draw the piece
// authored for exactly that neighbourhood.
let mut checked = 0;
for y in 0..map.height as i32 {
for x in 0..map.width as i32 {
let at = (y * map.width as i32 + x) as usize;
if map.terrain[at] != Terrain::Cliff {
continue;
}
let mask = tiles::neighbour_mask(&map, x, y);
if mask == MASK_ALL {
continue;
}
assert_eq!(first[at], Some(2000 + mask as u32));
checked += 1;
}
}
assert!(checked > 0, "no edge cliff cells to check");
}
#[test]
fn rtsmap_arena_is_the_same_map_from_every_chair() {
let map = generate(&spec(12, Style::Arena, 4));
// Rotational symmetry: every house's sorted distance profile is identical,
// which the fairness check measures as a zero spread.
assert!(map.report.start_spread < 0.02, "{}", map.report.summary());
// and each house has the same number of resource cells in reach.
let counts: Vec<u16> = map.starts.iter().map(|s| s.resource_cells).collect();
let lo = *counts.iter().min().unwrap() as f32;
let hi = *counts.iter().max().unwrap() as f32;
assert!(hi <= lo * 1.25, "resource cells per house: {counts:?}");
}
#[test]
fn rtsmap_spec_is_clamped_to_something_playable() {
let map = generate(&MapSpec {
width: 4,
height: 4000,
players: 99,
..MapSpec::default()
});
assert_eq!(map.width, makepad_rtsmap::MIN_SIZE);
assert_eq!(map.height, makepad_rtsmap::MAX_SIZE);
assert_eq!(map.starts.len(), makepad_rtsmap::MAX_PLAYERS as usize);
}
#[test]
fn rtsmap_emits_the_contract_sidecars() {
use makepad_rtsmap::emit::{place_text, EmitOpts, House, PropArt};
let map = generate(&spec(21, Style::Temperate, 2));
let opts = EmitOpts {
source: "test".into(),
world_key: "worlds/gen-test".into(),
houses: vec![House { name: "A".into(), color: "e8c040".into(), side: "0".into() }],
resource_key: "billboards/test/res".into(),
props: PropArt {
trees: vec!["billboards/test/t01".into()],
rocks: vec!["billboards/test/rock".into()],
ruins: vec!["billboards/test/ruin".into()],
blooms: vec!["billboards/test/bloom".into()],
},
roster: vec!["billboards/test/tank".into()],
..EmitOpts::default()
};
let place = place_text(&map, &opts);
assert!(place.starts_with("world-place 1\n"));
assert!(place.contains("\nmode rts\n"));
assert!(place.contains("\ncell 6.0\n"));
assert!(place.contains("\ngrid worlds/gen-test.grid\n"));
assert!(place.contains("\nhouse A color=e8c040 side=0\n"));
assert!(place.contains("\nroster billboards/test/tank\n"));
assert!(place.contains(" class=resource stage="));
let grid = makepad_rtsmap::grid_text(&map, 6.0);
assert!(grid.starts_with("world-grid 1\n"));
assert_eq!(grid.lines().filter(|l| l.starts_with("row ")).count(), map.height as usize);
let spawn = makepad_rtsmap::spawn_text(&map, 6.0, 60.0);
assert_eq!(spawn.lines().filter(|l| l.starts_with("start ")).count(), 2);
assert!(spawn.contains("\neye 60\n"));
}
/// Not a check — a look. Writes one PNG per style and seed so a human can
/// tell "legal" from "good".
#[test]
#[ignore]
fn rtsmap_write_preview_maps() {
let out = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../local/agent_state/cnc/m5-maps");
std::fs::create_dir_all(&out).expect("create preview dir");
let mut index = String::from("# M5 generated maps\n\n");
for style in Style::ALL {
for (slot, seed) in [3u32, 17, 42, 91].into_iter().enumerate() {
let players = [2u8, 4, 4, 6][slot];
let map = generate(&MapSpec {
seed,
players,
width: 72,
height: 72,
resources: if slot == 3 { 1.0 } else { 0.6 },
..spec(seed, style, players)
});
let (rgba, w, h) = makepad_rtsmap::preview::rgba(&map, 6);
let name = format!("{}-{players}p-{seed}.png", style.name());
std::fs::write(out.join(&name), makepad_rtsmap::preview::png(&rgba, w, h))
.expect("write preview");
index.push_str(&format!(
"- `{name}` — {}x{} cells, {} resource cells, {} props — {}\n",
map.width,
map.height,
map.resources.len(),
map.props.len(),
map.report.summary()
));
}
}
std::fs::write(out.join("README.md"), index).expect("write index");
}