Files
smart-rgb/crates/borders-core/tests/common/builders.rs
2025-10-25 16:15:50 -05:00

234 lines
8.8 KiB
Rust

/// Test builders for creating Bevy World and game state
///
/// Provides fluent API for constructing test environments with minimal boilerplate.
use borders_core::prelude::*;
pub struct TestWorld {
map_width: u16,
map_height: u16,
players: Vec<(NationId, f32, Option<String>)>, // (id, troops, name)
territories: Vec<(NationId, Vec<U16Vec2>)>,
terrain: Option<TerrainData>,
spawn_phase_active: bool,
rng_seed: u64,
}
impl TestWorld {
/// Create a new test world builder with default settings
pub fn new() -> Self {
Self { map_width: 100, map_height: 100, players: Vec::new(), territories: Vec::new(), terrain: None, spawn_phase_active: false, rng_seed: 0xDEADBEEF }
}
/// Set the map size
pub fn with_map_size(mut self, width: u16, height: u16) -> Self {
self.map_width = width;
self.map_height = height;
self
}
/// Add a player with specified troops
pub fn with_player(mut self, id: NationId, troops: f32) -> Self {
self.players.push((id, troops, None));
self
}
/// Add a named player
pub fn with_named_player(mut self, id: NationId, troops: f32, name: String) -> Self {
self.players.push((id, troops, Some(name)));
self
}
/// Set territory ownership for a player
pub fn with_territory(mut self, id: NationId, tiles: &[U16Vec2]) -> Self {
self.territories.push((id, tiles.to_vec()));
self
}
/// Provide custom terrain data
pub fn with_terrain(mut self, terrain: TerrainData) -> Self {
self.terrain = Some(terrain);
self
}
/// Activate spawn phase
pub fn with_spawn_phase(mut self) -> Self {
self.spawn_phase_active = true;
self
}
/// Set RNG seed for deterministic tests
pub fn with_rng_seed(mut self, seed: u64) -> Self {
self.rng_seed = seed;
self
}
/// Build the World with all configured state
pub fn build(self) -> World {
let mut world = World::new();
// Initialize Time resources (required by many systems)
world.insert_resource(Time::default());
world.insert_resource(FixedTime::from_seconds(0.1));
// Generate or use provided terrain
let terrain = self.terrain.unwrap_or_else(|| MapBuilder::new(self.map_width, self.map_height).all_conquerable().build());
// Initialize TerritoryManager
let mut territory_manager = TerritoryManager::new(self.map_width, self.map_height);
let conquerable_tiles: Vec<bool> = {
let mut tiles = Vec::with_capacity((self.map_width as usize) * (self.map_height as usize));
for y in 0..self.map_height {
for x in 0..self.map_width {
tiles.push(terrain.is_conquerable(U16Vec2::new(x, y)));
}
}
tiles
};
territory_manager.reset(self.map_width, self.map_height, &conquerable_tiles);
// Apply territory ownership
for (nation_id, tiles) in &self.territories {
for &tile in tiles {
territory_manager.conquer(tile, *nation_id);
}
}
// Create player entities
let mut entity_map = PlayerEntityMap::default();
for (i, (nation_id, troops, name)) in self.players.iter().enumerate() {
let player_name = name.clone().unwrap_or_else(|| format!("Player {}", nation_id.get()));
let color = HSLColor::new(
(i as f32 * 137.5) % 360.0, // golden angle distribution
0.6,
0.5,
);
let entity = world.spawn((*nation_id, PlayerName(player_name), PlayerColor(color), BorderTiles::default(), Troops(*troops), TerritorySize(0), borders_core::game::ships::ShipCount::default())).id();
entity_map.0.insert(*nation_id, entity);
}
// Insert core resources
world.insert_resource(entity_map);
world.insert_resource(territory_manager);
world.insert_resource(ActiveAttacks::new());
world.insert_resource(terrain);
world.insert_resource(DeterministicRng::new(self.rng_seed));
world.insert_resource(BorderCache::default());
world.insert_resource(AttackControls::default());
world.insert_resource(ClientPlayerId(NationId::ZERO));
world.insert_resource(HumanPlayerCount(1));
world.insert_resource(LocalPlayerContext::new(NationId::ZERO));
// Optional spawn phase
if self.spawn_phase_active {
world.insert_resource(SpawnPhase { active: true });
world.insert_resource(SpawnTimeout::new(30.0));
}
// Compute coastal tiles
let map_size = U16Vec2::new(self.map_width, self.map_height);
let coastal_tiles = CoastalTiles::compute(world.resource::<TerrainData>(), map_size);
world.insert_resource(coastal_tiles);
world
}
}
impl Default for TestWorld {
fn default() -> Self {
Self::new()
}
}
/// Builder for programmatic terrain generation
///
/// # Example
/// ```
/// let terrain = MapBuilder::new(100, 100)
/// .all_conquerable()
/// .build();
/// ```
pub struct MapBuilder {
width: u16,
height: u16,
terrain_data: Vec<u8>,
tile_types: Vec<TileType>,
}
impl MapBuilder {
/// Create a new map builder
pub fn new(width: u16, height: u16) -> Self {
let size = (width as usize) * (height as usize);
Self { width, height, terrain_data: vec![0; size], tile_types: Vec::new() }
}
/// Make all tiles land and conquerable
pub fn all_conquerable(mut self) -> Self {
let size = (self.width as usize) * (self.height as usize);
self.terrain_data = vec![0x80; size]; // bit 7 = land/conquerable
self.tile_types = vec![TileType { name: "water".to_string(), color_base: "blue".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 255, expansion_cost: 255 }, TileType { name: "land".to_string(), color_base: "green".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }];
self
}
/// Create islands: center 50x50 land, rest water
pub fn islands(mut self) -> Self {
self.tile_types = vec![TileType { name: "water".to_string(), color_base: "blue".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 255, expansion_cost: 255 }, TileType { name: "land".to_string(), color_base: "green".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }];
let center_x = self.width / 2;
let center_y = self.height / 2;
let island_size = 25u16;
for y in 0..self.height {
for x in 0..self.width {
let idx = (y as usize) * (self.width as usize) + (x as usize);
let in_island = x.abs_diff(center_x) < island_size && y.abs_diff(center_y) < island_size;
self.terrain_data[idx] = if in_island { 0x80 } else { 0x00 };
}
}
self
}
/// Create continents: alternating vertical strips of land/water
pub fn continents(mut self) -> Self {
self.tile_types = vec![TileType { name: "water".to_string(), color_base: "blue".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 255, expansion_cost: 255 }, TileType { name: "land".to_string(), color_base: "green".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }];
for y in 0..self.height {
for x in 0..self.width {
let idx = (y as usize) * (self.width as usize) + (x as usize);
// 30-tile wide strips
let is_land = (x / 30) % 2 == 0;
self.terrain_data[idx] = if is_land { 0x80 } else { 0x00 };
}
}
self
}
/// Set specific tiles as land
pub fn with_land(mut self, tiles: &[U16Vec2]) -> Self {
for &tile in tiles {
let idx = (tile.y as usize) * (self.width as usize) + (tile.x as usize);
if idx < self.terrain_data.len() {
self.terrain_data[idx] = 0x80;
}
}
self
}
/// Build the TerrainData
pub fn build(self) -> TerrainData {
let tiles: Vec<u8> = self.terrain_data.iter().map(|&byte| if byte & 0x80 != 0 { 1 } else { 0 }).collect();
let num_land_tiles = tiles.iter().filter(|&&t| t == 1).count();
let terrain_tile_map = TileMap::from_vec(self.width, self.height, self.terrain_data);
TerrainData { _manifest: MapManifest { map: MapMetadata { size: U16Vec2::new(self.width, self.height), num_land_tiles }, name: "Test Map".to_string(), nations: Vec::new() }, terrain_data: terrain_tile_map, tiles, tile_types: self.tile_types }
}
}