mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-17 06:13:21 -06:00
Update source files
This commit is contained in:
269
crates/borders-core/tests/common/assertions.rs
Normal file
269
crates/borders-core/tests/common/assertions.rs
Normal file
@@ -0,0 +1,269 @@
|
||||
//! Fluent assertion API for testing Bevy ECS World state
|
||||
//!
|
||||
//! Provides chainable assertion methods that test behavior through public interfaces.
|
||||
use assert2::assert;
|
||||
use extension_traits::extension;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use borders_core::prelude::*;
|
||||
|
||||
/// Fluent assertion builder for World state
|
||||
///
|
||||
/// Access via `world.assert()` to chain multiple assertions.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// world.assert()
|
||||
/// .player_owns(tile, player_id)
|
||||
/// .has_territory_changes()
|
||||
/// .no_invalid_state();
|
||||
/// ```
|
||||
pub struct WorldAssertions<'w> {
|
||||
world: &'w World,
|
||||
}
|
||||
|
||||
/// Extension trait to add assertion capabilities to World
|
||||
#[extension(pub trait WorldAssertExt)]
|
||||
impl World {
|
||||
/// Begin a fluent assertion chain
|
||||
fn assert(&self) -> WorldAssertions<'_> {
|
||||
WorldAssertions { world: self }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'w> WorldAssertions<'w> {
|
||||
/// Assert that a player owns a specific tile
|
||||
#[track_caller]
|
||||
pub fn player_owns(self, tile: U16Vec2, expected: NationId) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
let actual = mgr.get_nation_id(tile);
|
||||
assert!(actual == Some(expected), "Expected player {} to own tile {:?}, but found {:?}", expected.get(), tile, actual.map(|id| id.get()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a tile is unclaimed
|
||||
#[track_caller]
|
||||
pub fn tile_unclaimed(self, tile: U16Vec2) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
let ownership = mgr.get_ownership(tile);
|
||||
assert!(ownership.is_unclaimed(), "Expected tile {:?} to be unclaimed, but it's owned by {:?}", tile, ownership.nation_id());
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that an attack exists between attacker and target
|
||||
#[track_caller]
|
||||
pub fn attack_exists(self, attacker: NationId, target: Option<NationId>) -> Self {
|
||||
let attacks = self.world.resource::<ActiveAttacks>();
|
||||
let player_attacks = attacks.get_attacks_for_player(attacker);
|
||||
|
||||
let exists = player_attacks.iter().any(|(att, tgt, _, _, is_outgoing)| *att == attacker && *tgt == target && *is_outgoing);
|
||||
|
||||
assert!(exists, "Expected attack from player {} to {:?}, but none found. Active attacks: {:?}", attacker.get(), target.map(|t| t.get()), player_attacks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that no attack exists between attacker and target
|
||||
#[track_caller]
|
||||
pub fn no_attack(self, attacker: NationId, target: Option<NationId>) -> Self {
|
||||
let attacks = self.world.resource::<ActiveAttacks>();
|
||||
let player_attacks = attacks.get_attacks_for_player(attacker);
|
||||
|
||||
let exists = player_attacks.iter().any(|(att, tgt, _, _, is_outgoing)| *att == attacker && *tgt == target && *is_outgoing);
|
||||
|
||||
assert!(!exists, "Expected NO attack from player {} to {:?}, but found one. Active attacks: {:?}", attacker.get(), target.map(|t| t.get()), player_attacks);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a player has specific border tiles
|
||||
#[track_caller]
|
||||
pub fn border_tiles(self, player_id: NationId, expected: &HashSet<U16Vec2>) -> Self {
|
||||
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
|
||||
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
|
||||
|
||||
let border_tiles = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
|
||||
|
||||
assert!(&border_tiles.0 == expected, "Border tiles mismatch for player {}.\nExpected: {:?}\nActual: {:?}", player_id.get(), expected, border_tiles.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that TerritoryManager has recorded changes
|
||||
#[track_caller]
|
||||
pub fn has_territory_changes(self) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
assert!(mgr.has_changes(), "Expected territory changes to be tracked, but ChangeBuffer is empty");
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that TerritoryManager has NO recorded changes
|
||||
#[track_caller]
|
||||
pub fn no_territory_changes(self) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
assert!(!mgr.has_changes(), "Expected no territory changes, but ChangeBuffer contains: {:?}", mgr.iter_changes().collect::<Vec<_>>());
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a resource exists in the world
|
||||
#[track_caller]
|
||||
pub fn resource_exists<T: Resource>(self, resource_name: &str) -> Self {
|
||||
assert!(self.world.contains_resource::<T>(), "Expected resource '{}' to exist, but it was not found", resource_name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a resource does NOT exist in the world
|
||||
#[track_caller]
|
||||
pub fn resource_missing<T: Resource>(self, resource_name: &str) -> Self {
|
||||
assert!(!self.world.contains_resource::<T>(), "Expected resource '{}' to be missing, but it exists", resource_name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that no invalid game state exists
|
||||
///
|
||||
/// Checks common invariants:
|
||||
/// - No self-attacks in ActiveAttacks
|
||||
/// - All player entities have required components
|
||||
/// - Territory ownership is consistent
|
||||
#[track_caller]
|
||||
pub fn no_invalid_state(self) -> Self {
|
||||
let attacks = self.world.resource::<ActiveAttacks>();
|
||||
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
|
||||
|
||||
for &player_id in entity_map.0.keys() {
|
||||
let player_attacks = attacks.get_attacks_for_player(player_id);
|
||||
for (attacker, target, _, _, _) in player_attacks {
|
||||
if let Some(target_id) = target {
|
||||
assert!(attacker != target_id, "INVARIANT VIOLATION: Player {} is attacking itself", attacker.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (&player_id, &entity) in &entity_map.0 {
|
||||
assert!(self.world.get::<borders_core::game::Troops>(entity).is_some(), "Player {} entity missing Troops component", player_id.get());
|
||||
assert!(self.world.get::<BorderTiles>(entity).is_some(), "Player {} entity missing BorderTiles component", player_id.get());
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a player has a specific troop count
|
||||
#[track_caller]
|
||||
pub fn player_troops(self, player_id: NationId, expected: f32) -> Self {
|
||||
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
|
||||
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
|
||||
|
||||
let troops = self.world.get::<borders_core::game::Troops>(*entity).expect("Troops component not found");
|
||||
|
||||
let difference = (troops.0 - expected).abs();
|
||||
assert!(difference < 0.01, "Expected player {} to have {} troops, but found {} (difference: {})", player_id.get(), expected, troops.0, difference);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a tile is a border tile
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[track_caller]
|
||||
pub fn is_border(self, tile: U16Vec2) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
assert!(mgr.is_border(tile), "Expected tile {:?} to be a border tile, but it is not", tile);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a tile is NOT a border tile
|
||||
#[track_caller]
|
||||
pub fn not_border(self, tile: U16Vec2) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
assert!(!mgr.is_border(tile), "Expected tile {:?} to NOT be a border tile, but it is", tile);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a player owns all tiles in a slice
|
||||
#[track_caller]
|
||||
pub fn player_owns_all(self, tiles: &[U16Vec2], expected: NationId) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
for &tile in tiles {
|
||||
let actual = mgr.get_nation_id(tile);
|
||||
assert!(actual == Some(expected), "Expected player {} to own tile {:?}, but found {:?}", expected.get(), tile, actual.map(|id| id.get()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that the ChangeBuffer contains exactly N changes
|
||||
#[track_caller]
|
||||
pub fn change_count(self, expected: usize) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
let actual = mgr.iter_changes().count();
|
||||
assert!(actual == expected, "Expected {} changes in ChangeBuffer, but found {}", expected, actual);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a player entity has all standard game components
|
||||
///
|
||||
/// Checks for: PlayerName, PlayerColor, BorderTiles, Troops, TerritorySize
|
||||
#[track_caller]
|
||||
pub fn player_has_components(self, player_id: NationId) -> Self {
|
||||
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
|
||||
let entity = entity_map.0.get(&player_id).unwrap_or_else(|| panic!("Player entity not found for player {}", player_id.get()));
|
||||
|
||||
assert!(self.world.get::<borders_core::game::PlayerName>(*entity).is_some(), "Player {} missing PlayerName component", player_id.get());
|
||||
assert!(self.world.get::<borders_core::game::PlayerColor>(*entity).is_some(), "Player {} missing PlayerColor component", player_id.get());
|
||||
assert!(self.world.get::<BorderTiles>(*entity).is_some(), "Player {} missing BorderTiles component", player_id.get());
|
||||
assert!(self.world.get::<borders_core::game::Troops>(*entity).is_some(), "Player {} missing Troops component", player_id.get());
|
||||
assert!(self.world.get::<borders_core::game::TerritorySize>(*entity).is_some(), "Player {} missing TerritorySize component", player_id.get());
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that the map has specific dimensions
|
||||
#[track_caller]
|
||||
pub fn map_dimensions(self, width: u16, height: u16) -> Self {
|
||||
let mgr = self.world.resource::<TerritoryManager>();
|
||||
assert!(mgr.width() == width, "Expected map width {}, but found {}", width, mgr.width());
|
||||
assert!(mgr.height() == height, "Expected map height {}, but found {}", height, mgr.height());
|
||||
assert!(mgr.len() == (width as usize) * (height as usize), "Expected map size {}, but found {}", (width as usize) * (height as usize), mgr.len());
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that BorderCache is synchronized with ECS BorderTiles component
|
||||
#[track_caller]
|
||||
pub fn border_cache_synced(self, player_id: NationId) -> Self {
|
||||
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
|
||||
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
|
||||
|
||||
let component_borders = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
|
||||
|
||||
let cache = self.world.resource::<BorderCache>();
|
||||
let cache_borders = cache.get(player_id);
|
||||
|
||||
match cache_borders {
|
||||
Some(cached) => {
|
||||
assert!(&component_borders.0 == cached, "BorderCache out of sync with BorderTiles component for player {}.\nECS component: {:?}\nCache: {:?}", player_id.get(), component_borders.0, cached);
|
||||
}
|
||||
None => {
|
||||
assert!(component_borders.0.is_empty(), "BorderCache missing entry for player {} but component has {} borders", player_id.get(), component_borders.0.len());
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a player has NO border tiles
|
||||
#[track_caller]
|
||||
pub fn no_border_tiles(self, player_id: NationId) -> Self {
|
||||
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
|
||||
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
|
||||
|
||||
let border_tiles = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
|
||||
|
||||
assert!(border_tiles.0.is_empty(), "Expected player {} to have no border tiles, but found {} tiles: {:?}", player_id.get(), border_tiles.0.len(), border_tiles.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// Assert that a player has a specific number of border tiles
|
||||
#[track_caller]
|
||||
pub fn border_count(self, player_id: NationId, expected: usize) -> Self {
|
||||
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
|
||||
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
|
||||
|
||||
let border_tiles = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
|
||||
|
||||
assert!(border_tiles.0.len() == expected, "Expected player {} to have {} border tiles, but found {}", player_id.get(), expected, border_tiles.0.len());
|
||||
self
|
||||
}
|
||||
}
|
||||
233
crates/borders-core/tests/common/builders.rs
Normal file
233
crates/borders-core/tests/common/builders.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
/// 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 }
|
||||
}
|
||||
}
|
||||
33
crates/borders-core/tests/common/fixtures.rs
Normal file
33
crates/borders-core/tests/common/fixtures.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! Pre-built test scenarios for integration tests
|
||||
//!
|
||||
//! These fixtures represent common complex scenarios used across multiple tests.
|
||||
//! They are lazily initialized on first use.
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::builders::MapBuilder;
|
||||
use borders_core::prelude::*;
|
||||
|
||||
/// Standard 100x100 plains map (all conquerable)
|
||||
pub static PLAINS_MAP: Lazy<Arc<TerrainData>> = Lazy::new(|| Arc::new(MapBuilder::new(100, 100).all_conquerable().build()));
|
||||
|
||||
/// Island archipelago map: 50x50 islands separated by water
|
||||
pub static ISLAND_MAP: Lazy<Arc<TerrainData>> = Lazy::new(|| Arc::new(MapBuilder::new(100, 100).islands().build()));
|
||||
|
||||
/// Continental map: vertical strips of land and water
|
||||
pub static CONTINENT_MAP: Lazy<Arc<TerrainData>> = Lazy::new(|| Arc::new(MapBuilder::new(100, 100).continents().build()));
|
||||
|
||||
/// Get a clone of the plains map
|
||||
pub fn get_plains_map() -> TerrainData {
|
||||
(*PLAINS_MAP.clone()).clone()
|
||||
}
|
||||
|
||||
/// Get a clone of the island map
|
||||
pub fn get_island_map() -> TerrainData {
|
||||
(*ISLAND_MAP.clone()).clone()
|
||||
}
|
||||
|
||||
/// Get a clone of the continent map
|
||||
pub fn get_continent_map() -> TerrainData {
|
||||
(*CONTINENT_MAP.clone()).clone()
|
||||
}
|
||||
206
crates/borders-core/tests/common/mod.rs
Normal file
206
crates/borders-core/tests/common/mod.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(unused_imports)]
|
||||
|
||||
/// Shared test utilities and helpers
|
||||
///
|
||||
/// This module provides infrastructure for testing the Bevy ECS-based game logic:
|
||||
/// - `builders`: Fluent API for constructing test worlds and maps
|
||||
/// - `assertions`: Fluent assertion API for ECS state verification
|
||||
/// - `fixtures`: Pre-built test scenarios for integration tests
|
||||
///
|
||||
/// # Usage Example
|
||||
/// ```
|
||||
/// use common::builders::TestWorld;
|
||||
///
|
||||
/// let mut world = TestWorld::new()
|
||||
/// .with_player(NationId::ZERO, 100.0)
|
||||
/// .with_map_size(50, 50)
|
||||
/// .build();
|
||||
///
|
||||
/// world.conquer_tile(U16Vec2::new(5, 5), NationId::ZERO);
|
||||
/// world.assert().player_owns(U16Vec2::new(5, 5), NationId::ZERO);
|
||||
/// ```
|
||||
use borders_core::prelude::*;
|
||||
use extension_traits::extension;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
mod assertions;
|
||||
mod builders;
|
||||
mod fixtures;
|
||||
|
||||
// Re-export commonly used items
|
||||
pub use assertions::*;
|
||||
pub use builders::*;
|
||||
pub use fixtures::*;
|
||||
|
||||
/// Extension trait providing convenient action methods for World in tests
|
||||
#[extension(pub trait WorldTestExt)]
|
||||
impl World {
|
||||
/// Conquer a tile for a player
|
||||
fn conquer_tile(&mut self, tile: U16Vec2, player: NationId) {
|
||||
self.resource_mut::<TerritoryManager>().conquer(tile, player);
|
||||
}
|
||||
|
||||
/// Conquer multiple tiles for a player
|
||||
fn conquer_tiles(&mut self, tiles: &[U16Vec2], player: NationId) {
|
||||
let mut territory_manager = self.resource_mut::<TerritoryManager>();
|
||||
for &tile in tiles {
|
||||
territory_manager.conquer(tile, player);
|
||||
}
|
||||
}
|
||||
|
||||
/// Conquer a square region of tiles centered at a position
|
||||
///
|
||||
/// Conquers all tiles within `radius` steps of `center` (using taxicab distance).
|
||||
/// For radius=1, conquers a 3x3 square. For radius=2, conquers a 5x5 square, etc.
|
||||
fn conquer_region(&mut self, center: U16Vec2, radius: i32, player: NationId) {
|
||||
let mut territory_manager = self.resource_mut::<TerritoryManager>();
|
||||
for dy in -radius..=radius {
|
||||
for dx in -radius..=radius {
|
||||
let tile = center.as_ivec2() + glam::IVec2::new(dx, dy);
|
||||
if tile.x >= 0 && tile.y >= 0 {
|
||||
let tile = tile.as_u16vec2();
|
||||
if tile.x < territory_manager.width() && tile.y < territory_manager.height() {
|
||||
territory_manager.conquer(tile, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Conquer all 4-directional neighbors of a tile
|
||||
fn conquer_neighbors(&mut self, center: U16Vec2, player: NationId) {
|
||||
let map_size = {
|
||||
let mgr = self.resource::<TerritoryManager>();
|
||||
U16Vec2::new(mgr.width(), mgr.height())
|
||||
};
|
||||
|
||||
let mut territory_manager = self.resource_mut::<TerritoryManager>();
|
||||
for neighbor in neighbors(center, map_size) {
|
||||
territory_manager.conquer(neighbor, player);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear ownership of a tile, returning the previous owner
|
||||
fn clear_tile(&mut self, tile: U16Vec2) -> Option<NationId> {
|
||||
self.resource_mut::<TerritoryManager>().clear(tile)
|
||||
}
|
||||
|
||||
fn est(&mut self) {}
|
||||
|
||||
/// Clear all territory changes from the change buffer
|
||||
fn clear_territory_changes(&mut self) {
|
||||
self.resource_mut::<TerritoryManager>().clear_changes();
|
||||
}
|
||||
|
||||
/// Deactivate the spawn phase
|
||||
fn deactivate_spawn_phase(&mut self) {
|
||||
self.resource_mut::<SpawnPhase>().active = false;
|
||||
}
|
||||
|
||||
/// Activate the spawn phase
|
||||
fn activate_spawn_phase(&mut self) {
|
||||
self.resource_mut::<SpawnPhase>().active = true;
|
||||
}
|
||||
|
||||
/// Get the number of territory changes in the ChangeBuffer
|
||||
fn get_change_count(&self) -> usize {
|
||||
self.resource::<TerritoryManager>().iter_changes().count()
|
||||
}
|
||||
|
||||
/// Get the entity associated with a player
|
||||
fn get_player_entity(&self, player_id: NationId) -> Entity {
|
||||
let entity_map = self.resource::<PlayerEntityMap>();
|
||||
*entity_map.0.get(&player_id).unwrap_or_else(|| panic!("Player entity not found for player {}", player_id.get()))
|
||||
}
|
||||
|
||||
/// Run the border update logic (inline implementation for testing)
|
||||
fn update_borders(&mut self) {
|
||||
// Check if there are changes
|
||||
{
|
||||
let territory_manager = self.resource::<TerritoryManager>();
|
||||
if !territory_manager.has_changes() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect all data we need from TerritoryManager before mutating
|
||||
let (changed_tiles, map_size, tiles_by_owner) = {
|
||||
let territory_manager = self.resource::<TerritoryManager>();
|
||||
|
||||
let changed_tiles: HashSet<U16Vec2> = territory_manager.iter_changes().collect();
|
||||
let map_size = U16Vec2::new(territory_manager.width(), territory_manager.height());
|
||||
|
||||
let mut affected_tiles = HashSet::with_capacity(changed_tiles.len() * 5);
|
||||
for &tile in &changed_tiles {
|
||||
affected_tiles.insert(tile);
|
||||
affected_tiles.extend(neighbors(tile, map_size));
|
||||
}
|
||||
|
||||
// Group tiles by owner
|
||||
let mut tiles_by_owner: HashMap<NationId, HashSet<U16Vec2>> = HashMap::new();
|
||||
for &tile in &affected_tiles {
|
||||
if let Some(nation_id) = territory_manager.get_nation_id(tile) {
|
||||
tiles_by_owner.entry(nation_id).or_default().insert(tile);
|
||||
}
|
||||
}
|
||||
|
||||
(changed_tiles, map_size, tiles_by_owner)
|
||||
};
|
||||
|
||||
// Build ownership snapshot for border checking
|
||||
let ownership_snapshot: HashMap<U16Vec2, Option<NationId>> = {
|
||||
let territory_manager = self.resource::<TerritoryManager>();
|
||||
let mut snapshot = HashMap::new();
|
||||
for &tile in changed_tiles.iter() {
|
||||
for neighbor in neighbors(tile, map_size) {
|
||||
snapshot.entry(neighbor).or_insert_with(|| territory_manager.get_nation_id(neighbor));
|
||||
}
|
||||
snapshot.insert(tile, territory_manager.get_nation_id(tile));
|
||||
}
|
||||
snapshot
|
||||
};
|
||||
|
||||
// Update each player's borders
|
||||
let mut players_query = self.query::<(&NationId, &mut BorderTiles)>();
|
||||
for (nation_id, mut component_borders) in players_query.iter_mut(self) {
|
||||
let empty_set = HashSet::new();
|
||||
let player_tiles = tiles_by_owner.get(nation_id).unwrap_or(&empty_set);
|
||||
|
||||
// Process tiles owned by this player
|
||||
for &tile in player_tiles {
|
||||
let is_border = neighbors(tile, map_size).any(|neighbor| ownership_snapshot.get(&neighbor).and_then(|&owner| owner) != Some(*nation_id));
|
||||
|
||||
if is_border {
|
||||
component_borders.0.insert(tile);
|
||||
} else {
|
||||
component_borders.0.remove(&tile);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tiles that changed ownership away from this player
|
||||
for &tile in changed_tiles.iter() {
|
||||
if ownership_snapshot.get(&tile).and_then(|&owner| owner) != Some(*nation_id) {
|
||||
component_borders.0.remove(&tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear territory changes
|
||||
fn clear_borders_changes(&mut self) {
|
||||
self.resource_mut::<TerritoryManager>().clear_changes();
|
||||
}
|
||||
|
||||
/// Get border tiles for a player from ECS component
|
||||
fn get_player_borders(&self, player_id: NationId) -> HashSet<U16Vec2> {
|
||||
let entity = self.get_player_entity(player_id);
|
||||
self.get::<BorderTiles>(entity).expect("Player entity missing BorderTiles component").0.clone()
|
||||
}
|
||||
|
||||
/// Get border tiles for a player from BorderCache
|
||||
fn get_border_cache(&self, player_id: NationId) -> Option<HashSet<U16Vec2>> {
|
||||
self.resource::<BorderCache>().get(player_id).cloned()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user