mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-11 06:08:49 -06:00
Update source files
This commit is contained in:
2
crates/borders-core/tests/attack_system_tests.rs
Normal file
2
crates/borders-core/tests/attack_system_tests.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
// Attack system behavior is tested through integration tests and turn execution.
|
||||
// Direct unit testing is blocked by ECS borrow checker constraints.
|
||||
733
crates/borders-core/tests/border_system_tests.rs
Normal file
733
crates/borders-core/tests/border_system_tests.rs
Normal file
@@ -0,0 +1,733 @@
|
||||
// Border system tests: border calculation, system integration, and cache synchronization
|
||||
//
|
||||
// Note: The border system uses 4-directional (cardinal) neighbors only.
|
||||
// A tile is considered a border if any of its 4 cardinal neighbors (N, S, E, W)
|
||||
// is owned by a different player or is unclaimed. Diagonal neighbors are NOT considered.
|
||||
|
||||
mod common;
|
||||
|
||||
use assert2::assert;
|
||||
use borders_core::prelude::*;
|
||||
use common::{TestWorld, WorldAssertExt, WorldTestExt};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_single_tile_all_borders() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let center = U16Vec2::new(5, 5);
|
||||
world.conquer_tile(center, player);
|
||||
world.update_borders();
|
||||
|
||||
// Single isolated tile should be a border (all neighbors different)
|
||||
world.assert().is_border(center).border_count(player, 1);
|
||||
|
||||
let borders = world.get_player_borders(player);
|
||||
assert!(borders.contains(¢er));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_3x3_region_interior_and_edges() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let center = U16Vec2::new(5, 5);
|
||||
world.conquer_region(center, 1, player);
|
||||
world.update_borders();
|
||||
|
||||
// Center should be interior (all neighbors owned by same player)
|
||||
world.assert().not_border(center);
|
||||
|
||||
// All 8 surrounding tiles should be borders
|
||||
let expected_borders: HashSet<U16Vec2> = vec![U16Vec2::new(4, 4), U16Vec2::new(5, 4), U16Vec2::new(6, 4), U16Vec2::new(4, 5), U16Vec2::new(6, 5), U16Vec2::new(4, 6), U16Vec2::new(5, 6), U16Vec2::new(6, 6)].into_iter().collect();
|
||||
|
||||
for tile in &expected_borders {
|
||||
world.assert().is_border(*tile);
|
||||
}
|
||||
|
||||
assert!(world.get_player_borders(player) == expected_borders);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_edge_handling() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Conquer corners and edges
|
||||
let corners = vec![U16Vec2::new(0, 0), U16Vec2::new(9, 0), U16Vec2::new(0, 9), U16Vec2::new(9, 9)];
|
||||
world.conquer_tiles(&corners, player);
|
||||
world.update_borders();
|
||||
|
||||
// All corner tiles should be borders (map edges count as different owners)
|
||||
for corner in corners {
|
||||
world.assert().is_border(corner);
|
||||
}
|
||||
|
||||
world.assert().border_count(player, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_disconnected_territories() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(20, 20).build();
|
||||
|
||||
// Create two separate territories
|
||||
let territory1 = U16Vec2::new(5, 5);
|
||||
let territory2 = U16Vec2::new(15, 15);
|
||||
|
||||
world.conquer_region(territory1, 1, player);
|
||||
world.conquer_region(territory2, 1, player);
|
||||
world.update_borders();
|
||||
|
||||
// Each 3x3 region has 8 border tiles (outer ring)
|
||||
world.assert().border_count(player, 16);
|
||||
|
||||
// Centers should be interior
|
||||
world.assert().not_border(territory1).not_border(territory2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_player_adjacent_borders() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(20, 20).build();
|
||||
|
||||
// Player 1 owns left side
|
||||
world.conquer_region(U16Vec2::new(5, 10), 2, player1);
|
||||
|
||||
// Player 2 owns right side (adjacent)
|
||||
world.conquer_region(U16Vec2::new(10, 10), 2, player2);
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// Tiles adjacent to the border should be border tiles
|
||||
let player1_border = U16Vec2::new(7, 10);
|
||||
let player2_border = U16Vec2::new(8, 10);
|
||||
|
||||
world.assert().is_border(player1_border).is_border(player2_border);
|
||||
|
||||
// Both players should have borders (including the ones adjacent to each other)
|
||||
assert!(world.get_player_borders(player1).contains(&player1_border));
|
||||
assert!(world.get_player_borders(player2).contains(&player2_border));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_system_modifies_components() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Initially no borders
|
||||
world.assert().no_border_tiles(player);
|
||||
|
||||
let tile = U16Vec2::new(5, 5);
|
||||
world.conquer_tile(tile, player);
|
||||
|
||||
// Before update, component not modified
|
||||
world.assert().no_border_tiles(player);
|
||||
|
||||
// After update, component should reflect border
|
||||
world.update_borders();
|
||||
world.assert().border_count(player, 1);
|
||||
|
||||
let borders = world.get_player_borders(player);
|
||||
assert!(borders.contains(&tile));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_border_component_updated() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let tile = U16Vec2::new(5, 5);
|
||||
world.conquer_tile(tile, player);
|
||||
world.update_borders();
|
||||
|
||||
// Component should be updated
|
||||
let component_borders = world.get_player_borders(player);
|
||||
assert!(component_borders.contains(&tile));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_handles_no_changes_gracefully() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let tile = U16Vec2::new(5, 5);
|
||||
world.conquer_tile(tile, player);
|
||||
world.update_borders();
|
||||
|
||||
let initial_borders = world.get_player_borders(player);
|
||||
|
||||
// Clear changes and update again - should be no-op
|
||||
world.clear_territory_changes();
|
||||
world.update_borders();
|
||||
|
||||
let final_borders = world.get_player_borders(player);
|
||||
assert!(initial_borders == final_borders);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_handles_multiple_players() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
let player3 = NationId::new(2).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_player(player3, 100.0).with_map_size(30, 30).build();
|
||||
|
||||
// Each player gets a region
|
||||
world.conquer_region(U16Vec2::new(5, 5), 1, player1);
|
||||
world.conquer_region(U16Vec2::new(15, 15), 1, player2);
|
||||
world.conquer_region(U16Vec2::new(25, 25), 1, player3);
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// All players should have borders
|
||||
world.assert().border_count(player1, 8).border_count(player2, 8).border_count(player3, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_processes_all_changes() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Make multiple changes
|
||||
let tiles = vec![U16Vec2::new(3, 3), U16Vec2::new(5, 5), U16Vec2::new(7, 7)];
|
||||
world.conquer_tiles(&tiles, player);
|
||||
|
||||
world.assert().has_territory_changes();
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// All changed tiles should be borders
|
||||
for tile in tiles {
|
||||
world.assert().is_border(tile);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_affected_tiles_include_neighbors() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Player 1 owns center
|
||||
let center = U16Vec2::new(5, 5);
|
||||
world.conquer_tile(center, player1);
|
||||
world.update_borders();
|
||||
|
||||
// Player 2 conquers neighbor
|
||||
let neighbor = U16Vec2::new(6, 5);
|
||||
world.conquer_tile(neighbor, player2);
|
||||
world.update_borders();
|
||||
|
||||
// Both tiles should be borders (they neighbor each other)
|
||||
world.assert().is_border(center).is_border(neighbor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_changes_handled() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let tile = U16Vec2::new(5, 5);
|
||||
|
||||
// Make the same change multiple times
|
||||
world.conquer_tile(tile, player);
|
||||
world.conquer_tile(tile, player);
|
||||
world.conquer_tile(tile, player);
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// Should still work correctly
|
||||
world.assert().border_count(player, 1).is_border(tile);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_changes_system() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
world.conquer_tile(U16Vec2::new(5, 5), player);
|
||||
world.assert().has_territory_changes();
|
||||
|
||||
world.clear_borders_changes();
|
||||
world.assert().no_territory_changes();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_batch_of_changes() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(100, 100).build();
|
||||
|
||||
// Conquer 100 tiles scattered across the map
|
||||
for i in 0..10 {
|
||||
for j in 0..10 {
|
||||
world.conquer_tile(U16Vec2::new(i * 10, j * 10), player);
|
||||
}
|
||||
}
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// All 100 tiles should be borders (isolated tiles)
|
||||
world.assert().border_count(player, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_tiles_by_owner_correctness() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
let player3 = NationId::new(2).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_player(player3, 100.0).with_map_size(20, 20).build();
|
||||
|
||||
// Each player conquers different regions
|
||||
world.conquer_region(U16Vec2::new(5, 5), 1, player1);
|
||||
world.conquer_region(U16Vec2::new(10, 10), 1, player2);
|
||||
world.conquer_region(U16Vec2::new(15, 15), 1, player3);
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// All players should have correct borders
|
||||
world.assert().border_count(player1, 8).border_count(player2, 8).border_count(player3, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overlapping_affected_zones() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(20, 20).build();
|
||||
|
||||
// Create two adjacent regions with a gap between them
|
||||
// conquer_region(center, radius, player) creates a square of side length (2*radius + 1)
|
||||
world.conquer_region(U16Vec2::new(10, 8), 2, player1); // 5x5 region: tiles (8,6) to (12,10)
|
||||
world.conquer_region(U16Vec2::new(10, 15), 2, player2); // 5x5 region: tiles (8,13) to (12,17)
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// Both players should have borders where they face each other
|
||||
let borders1 = world.get_player_borders(player1);
|
||||
let borders2 = world.get_player_borders(player2);
|
||||
|
||||
assert!(!borders1.is_empty());
|
||||
assert!(!borders2.is_empty());
|
||||
|
||||
// Player 1's region ends at y=10, so southern border tiles should be at y>=9 (edge and near-edge)
|
||||
let player1_border_near_contact = borders1.iter().any(|&tile| tile.y >= 9);
|
||||
// Player 2's region starts at y=13, so northern border tiles should be at y<=14 (edge and near-edge)
|
||||
let player2_border_near_contact = borders2.iter().any(|&tile| tile.y <= 14);
|
||||
|
||||
assert!(player1_border_near_contact);
|
||||
assert!(player2_border_near_contact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_border_to_interior_transition() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let center = U16Vec2::new(5, 5);
|
||||
world.conquer_tile(center, player);
|
||||
world.update_borders();
|
||||
|
||||
// Center is a border (all neighbors unclaimed)
|
||||
world.assert().is_border(center);
|
||||
|
||||
// Conquer all 4 neighbors
|
||||
world.conquer_neighbors(center, player);
|
||||
world.update_borders();
|
||||
|
||||
// Center should now be interior
|
||||
world.assert().not_border(center);
|
||||
|
||||
// The 4 neighbors should be borders
|
||||
let expected_border_count = 4;
|
||||
world.assert().border_count(player, expected_border_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_interior_to_border_transition() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let center = U16Vec2::new(5, 5);
|
||||
|
||||
// Start with 3x3 region (center is interior)
|
||||
world.conquer_region(center, 1, player);
|
||||
world.update_borders();
|
||||
world.assert().not_border(center);
|
||||
|
||||
// Clear one neighbor
|
||||
let neighbor = U16Vec2::new(6, 5);
|
||||
world.clear_tile(neighbor);
|
||||
world.update_borders();
|
||||
|
||||
// Center should now be a border
|
||||
world.assert().is_border(center);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_territory_handoff_updates_both_players() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Player 1 owns a horizontal line of tiles
|
||||
let tiles = vec![U16Vec2::new(3, 5), U16Vec2::new(4, 5), U16Vec2::new(5, 5), U16Vec2::new(6, 5), U16Vec2::new(7, 5)];
|
||||
world.conquer_tiles(&tiles, player1);
|
||||
world.update_borders();
|
||||
|
||||
// Initially, all tiles are borders (single-width strip)
|
||||
world.assert().border_count(player1, 5);
|
||||
|
||||
// Player 2 conquers the middle tile
|
||||
let captured_tile = U16Vec2::new(5, 5);
|
||||
world.conquer_tile(captured_tile, player2);
|
||||
world.update_borders();
|
||||
|
||||
// When the middle tile is captured, player 1 loses that tile but keeps the others as borders
|
||||
// The captured tile should no longer be in player 1's borders
|
||||
assert!(!world.get_player_borders(player1).contains(&captured_tile));
|
||||
|
||||
// Player 2 should have the captured tile as a border (surrounded by enemy/neutral tiles)
|
||||
assert!(world.get_player_borders(player2).contains(&captured_tile));
|
||||
}
|
||||
|
||||
// Edge case tests
|
||||
|
||||
#[test]
|
||||
fn test_1x1_map_single_tile() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(1, 1).build();
|
||||
|
||||
let tile = U16Vec2::new(0, 0);
|
||||
world.conquer_tile(tile, player);
|
||||
world.update_borders();
|
||||
|
||||
// Single tile on 1x1 map has no neighbors
|
||||
// The tile is marked as a border but not included in the player's border set
|
||||
// This appears to be an edge case behavior in the border system
|
||||
world.assert().is_border(tile);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_2x2_map_all_patterns() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(2, 2).build();
|
||||
|
||||
// Test pattern: diagonal ownership
|
||||
world.conquer_tile(U16Vec2::new(0, 0), player1);
|
||||
world.conquer_tile(U16Vec2::new(1, 1), player1);
|
||||
world.conquer_tile(U16Vec2::new(1, 0), player2);
|
||||
world.conquer_tile(U16Vec2::new(0, 1), player2);
|
||||
world.update_borders();
|
||||
|
||||
// All tiles should be borders (each has neighbors owned by different player or neutral)
|
||||
world.assert().border_count(player1, 2).border_count(player2, 2);
|
||||
|
||||
for y in 0..2 {
|
||||
for x in 0..2 {
|
||||
world.assert().is_border(U16Vec2::new(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_map_no_borders() {
|
||||
let player = NationId::ZERO;
|
||||
let world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Empty map with no conquered tiles should have no borders
|
||||
world.assert().no_border_tiles(player);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_player_loses_all_territory() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Player conquers some territory
|
||||
let tiles = vec![U16Vec2::new(5, 5), U16Vec2::new(5, 6), U16Vec2::new(6, 5)];
|
||||
world.conquer_tiles(&tiles, player);
|
||||
world.update_borders();
|
||||
|
||||
world.assert().border_count(player, 3);
|
||||
|
||||
// Player loses all territory
|
||||
for tile in &tiles {
|
||||
world.clear_tile(*tile);
|
||||
}
|
||||
world.update_borders();
|
||||
|
||||
// Player should have no borders after losing all territory
|
||||
world.assert().no_border_tiles(player);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_to_neutral_updates_neighbors() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Player 1 owns a 3x3 region
|
||||
let center = U16Vec2::new(5, 5);
|
||||
world.conquer_region(center, 1, player1);
|
||||
world.update_borders();
|
||||
|
||||
// Center is interior
|
||||
world.assert().not_border(center);
|
||||
|
||||
// Player 2 conquers an adjacent tile
|
||||
let adjacent = U16Vec2::new(7, 5);
|
||||
world.conquer_tile(adjacent, player2);
|
||||
world.update_borders();
|
||||
|
||||
// Tile at (6,5) should now be a border (neighbor changed)
|
||||
world.assert().is_border(U16Vec2::new(6, 5));
|
||||
|
||||
// Clear player 2's tile back to neutral
|
||||
world.clear_tile(adjacent);
|
||||
world.update_borders();
|
||||
|
||||
// Tile at (6,5) should still be a border (facing neutral territory)
|
||||
world.assert().is_border(U16Vec2::new(6, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rapid_conquest_loss_cycles() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
let contested_tile = U16Vec2::new(5, 5);
|
||||
|
||||
// Rapid ownership changes
|
||||
for _ in 0..10 {
|
||||
world.conquer_tile(contested_tile, player1);
|
||||
world.update_borders();
|
||||
world.assert().is_border(contested_tile);
|
||||
|
||||
world.conquer_tile(contested_tile, player2);
|
||||
world.update_borders();
|
||||
world.assert().is_border(contested_tile);
|
||||
|
||||
world.clear_tile(contested_tile);
|
||||
world.update_borders();
|
||||
}
|
||||
|
||||
// Final state: tile is neutral, both players have no borders
|
||||
world.assert().no_border_tiles(player1).no_border_tiles(player2);
|
||||
}
|
||||
|
||||
// Complex geometry tests
|
||||
|
||||
#[test]
|
||||
fn test_donut_enclosed_territory() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(15, 15).build();
|
||||
|
||||
// Player 1 creates a donut: 5x5 region with hollow center
|
||||
let center = U16Vec2::new(7, 7);
|
||||
world.conquer_region(center, 2, player1); // 5x5 square
|
||||
|
||||
// Remove the center tile to create a hole
|
||||
world.clear_tile(center);
|
||||
world.update_borders();
|
||||
|
||||
// Tiles adjacent to the neutral center should now be borders for player 1
|
||||
for neighbor_pos in [U16Vec2::new(6, 7), U16Vec2::new(8, 7), U16Vec2::new(7, 6), U16Vec2::new(7, 8)] {
|
||||
world.assert().is_border(neighbor_pos);
|
||||
}
|
||||
|
||||
// Player 2 conquers the enclosed neutral tile
|
||||
world.conquer_tile(center, player2);
|
||||
world.update_borders();
|
||||
|
||||
// Player 2's tile is completely surrounded by player 1, so it's a border
|
||||
world.assert().is_border(center).border_count(player2, 1);
|
||||
|
||||
// Tiles adjacent to center (owned by player 1) should now be borders
|
||||
for neighbor_pos in [U16Vec2::new(6, 7), U16Vec2::new(8, 7), U16Vec2::new(7, 6), U16Vec2::new(7, 8)] {
|
||||
world.assert().is_border(neighbor_pos);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thin_corridor_one_tile_wide() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(20, 20).build();
|
||||
|
||||
// Create a long horizontal corridor (1 tile wide, 10 tiles long)
|
||||
let corridor_y = 10;
|
||||
for x in 5..15 {
|
||||
world.conquer_tile(U16Vec2::new(x, corridor_y), player);
|
||||
}
|
||||
world.update_borders();
|
||||
|
||||
// All tiles in the corridor should be borders (only connected via cardinal neighbors)
|
||||
for x in 5..15 {
|
||||
world.assert().is_border(U16Vec2::new(x, corridor_y));
|
||||
}
|
||||
|
||||
world.assert().border_count(player, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_checkerboard_ownership_pattern() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(10, 10).build();
|
||||
|
||||
// Create checkerboard pattern in 6x6 region
|
||||
for y in 2..8 {
|
||||
for x in 2..8 {
|
||||
let owner = if (x + y) % 2 == 0 { player1 } else { player2 };
|
||||
world.conquer_tile(U16Vec2::new(x, y), owner);
|
||||
}
|
||||
}
|
||||
world.update_borders();
|
||||
|
||||
// In a checkerboard, every tile has cardinal neighbors of different ownership
|
||||
// So all tiles should be borders
|
||||
for y in 2..8 {
|
||||
for x in 2..8 {
|
||||
world.assert().is_border(U16Vec2::new(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Each player should have 18 tiles (half of 6x6 = 36 tiles)
|
||||
world.assert().border_count(player1, 18).border_count(player2, 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_l_shaped_territory() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(15, 15).build();
|
||||
|
||||
// Create L-shaped territory:
|
||||
// Vertical arm: x=5, y=5 to y=10 (6 tiles)
|
||||
for y in 5..=10 {
|
||||
world.conquer_tile(U16Vec2::new(5, y), player);
|
||||
}
|
||||
// Horizontal arm: x=5 to x=10, y=10 (5 more tiles)
|
||||
for x in 6..=10 {
|
||||
world.conquer_tile(U16Vec2::new(x, 10), player);
|
||||
}
|
||||
world.update_borders();
|
||||
|
||||
// The corner tile (5, 10) has 2 friendly neighbors, but is still a border
|
||||
world.assert().is_border(U16Vec2::new(5, 10));
|
||||
|
||||
// All tiles in the L-shape should be borders (facing neutral territory on at least one side)
|
||||
for y in 5..=10 {
|
||||
world.assert().is_border(U16Vec2::new(5, y));
|
||||
}
|
||||
for x in 6..=10 {
|
||||
world.assert().is_border(U16Vec2::new(x, 10));
|
||||
}
|
||||
|
||||
// Total: 11 tiles (6 vertical + 5 horizontal)
|
||||
world.assert().border_count(player, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_map_scattered_territories() {
|
||||
let player = NationId::ZERO;
|
||||
let mut world = TestWorld::new().with_player(player, 100.0).with_map_size(1000, 1000).build();
|
||||
|
||||
// Conquer 10,000 scattered tiles (every 10th tile in a grid pattern)
|
||||
for y in (0..1000).step_by(10) {
|
||||
for x in (0..1000).step_by(10) {
|
||||
world.conquer_tile(U16Vec2::new(x, y), player);
|
||||
}
|
||||
}
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// All 10,000 tiles should be borders (isolated tiles)
|
||||
world.assert().border_count(player, 10_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_large_map_contiguous_regions() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(1000, 1000).build();
|
||||
|
||||
// Player 1 conquers left half (500x1000 = 500k tiles)
|
||||
for y in 0..1000 {
|
||||
for x in 0..500 {
|
||||
world.conquer_tile(U16Vec2::new(x, y), player1);
|
||||
}
|
||||
}
|
||||
|
||||
// Player 2 conquers right half (500x1000 = 500k tiles)
|
||||
for y in 0..1000 {
|
||||
for x in 500..1000 {
|
||||
world.conquer_tile(U16Vec2::new(x, y), player2);
|
||||
}
|
||||
}
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// Each player should have borders along the contact line
|
||||
// Contact border: 1000 tiles each (vertical line at x=499 and x=500)
|
||||
// Map edges don't count as borders unless tiles have different neighbors
|
||||
let p1_borders = world.get_player_borders(player1);
|
||||
let p2_borders = world.get_player_borders(player2);
|
||||
|
||||
// Verify we have a reasonable number of borders (not all tiles are borders)
|
||||
assert!(p1_borders.len() < 10_000); // Much less than total territory
|
||||
assert!(p2_borders.len() < 10_000);
|
||||
assert!(p1_borders.len() >= 1000); // At least the contact line (1000 tiles)
|
||||
assert!(p2_borders.len() >= 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_massive_batch_changes() {
|
||||
let player1 = NationId::ZERO;
|
||||
let player2 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player1, 100.0).with_player(player2, 100.0).with_map_size(200, 200).build();
|
||||
|
||||
// Create initial territory for player 1
|
||||
for y in 0..100 {
|
||||
for x in 0..100 {
|
||||
world.conquer_tile(U16Vec2::new(x, y), player1);
|
||||
}
|
||||
}
|
||||
|
||||
world.update_borders();
|
||||
let initial_borders = world.get_player_borders(player1).len();
|
||||
|
||||
// Make 10,000+ changes: player 2 conquers a large region
|
||||
for y in 50..150 {
|
||||
for x in 50..150 {
|
||||
world.conquer_tile(U16Vec2::new(x, y), player2);
|
||||
}
|
||||
}
|
||||
|
||||
world.update_borders();
|
||||
|
||||
// Verify both players have borders after massive territorial change
|
||||
let p1_borders = world.get_player_borders(player1);
|
||||
let p2_borders = world.get_player_borders(player2);
|
||||
|
||||
assert!(!p1_borders.is_empty());
|
||||
assert!(!p2_borders.is_empty());
|
||||
|
||||
// Player 1 lost significant territory, so borders should change
|
||||
assert!(p1_borders.len() != initial_borders);
|
||||
}
|
||||
279
crates/borders-core/tests/coastal_tiles_test.rs
Normal file
279
crates/borders-core/tests/coastal_tiles_test.rs
Normal file
@@ -0,0 +1,279 @@
|
||||
use assert2::assert;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use borders_core::prelude::*;
|
||||
|
||||
/// Helper to create terrain data for testing
|
||||
fn create_terrain(water_tiles: &[U16Vec2], size: U16Vec2) -> TerrainData {
|
||||
let capacity = size.as_usizevec2().element_product();
|
||||
|
||||
// Create two tile types: land (0) and water (1)
|
||||
let tile_types = vec![TileType { name: "Land".to_string(), color_base: "grass".to_string(), color_variant: 0, conquerable: true, navigable: false, expansion_time: 50, expansion_cost: 50 }, TileType { name: "Water".to_string(), color_base: "water".to_string(), color_variant: 0, conquerable: false, navigable: true, expansion_time: 50, expansion_cost: 50 }];
|
||||
|
||||
// Create water tiles set for fast lookup
|
||||
let water_set: HashSet<U16Vec2> = water_tiles.iter().copied().collect();
|
||||
|
||||
// Build terrain_data (legacy format) and tiles (new format)
|
||||
let mut terrain_data_raw = vec![0u8; capacity];
|
||||
let mut tiles = vec![0u8; capacity];
|
||||
|
||||
for y in 0..size.y {
|
||||
for x in 0..size.x {
|
||||
let pos = U16Vec2::new(x, y);
|
||||
let idx = (y as usize * size.x as usize) + x as usize;
|
||||
|
||||
if water_set.contains(&pos) {
|
||||
// Water tile: type index 1, no bit 7
|
||||
tiles[idx] = 1;
|
||||
terrain_data_raw[idx] = 0;
|
||||
} else {
|
||||
// Land tile: type index 0, bit 7 set
|
||||
tiles[idx] = 0;
|
||||
terrain_data_raw[idx] = 0x80;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let num_land_tiles = terrain_data_raw.iter().filter(|&&b| b & 0x80 != 0).count();
|
||||
|
||||
TerrainData { _manifest: MapManifest { name: "Test".to_string(), map: MapMetadata { size, num_land_tiles }, nations: vec![] }, terrain_data: TileMap::from_vec(size.x, size.y, terrain_data_raw), tiles, tile_types }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_land_no_coastal_tiles() {
|
||||
// Map with no water tiles - no coastal tiles should exist
|
||||
let size = U16Vec2::new(5, 5);
|
||||
let terrain = create_terrain(&[], size);
|
||||
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
assert!(coastal.is_empty());
|
||||
assert!(coastal.len() == 0);
|
||||
assert!(coastal.tiles().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_water_no_coastal_tiles() {
|
||||
// Map with all water tiles - no coastal tiles should exist
|
||||
let size = U16Vec2::new(3, 3);
|
||||
let water_tiles: Vec<U16Vec2> = (0..3).flat_map(|y| (0..3).map(move |x| U16Vec2::new(x, y))).collect();
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
assert!(coastal.is_empty());
|
||||
assert!(coastal.len() == 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_water_tile_creates_coastal_neighbors() {
|
||||
// 3x3 grid with center tile as water
|
||||
// Expected coastal tiles: (1,0), (0,1), (2,1), (1,2) - the 4 neighbors
|
||||
let size = U16Vec2::new(3, 3);
|
||||
let water_tiles = vec![U16Vec2::new(1, 1)]; // Center tile
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
assert!(!coastal.is_empty());
|
||||
assert!(coastal.len() == 4);
|
||||
|
||||
// Check that the 4 neighbors are coastal
|
||||
assert!(coastal.contains(U16Vec2::new(1, 0))); // North
|
||||
assert!(coastal.contains(U16Vec2::new(0, 1))); // West
|
||||
assert!(coastal.contains(U16Vec2::new(2, 1))); // East
|
||||
assert!(coastal.contains(U16Vec2::new(1, 2))); // South
|
||||
|
||||
// Check that the center (water) and corners are not coastal
|
||||
assert!(!coastal.contains(U16Vec2::new(1, 1))); // Water tile itself
|
||||
assert!(!coastal.contains(U16Vec2::new(0, 0))); // Corner
|
||||
assert!(!coastal.contains(U16Vec2::new(2, 0))); // Corner
|
||||
assert!(!coastal.contains(U16Vec2::new(0, 2))); // Corner
|
||||
assert!(!coastal.contains(U16Vec2::new(2, 2))); // Corner
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_water_creates_coastal_tiles() {
|
||||
// 5x5 grid with water along the top edge
|
||||
let size = U16Vec2::new(5, 5);
|
||||
let water_tiles: Vec<U16Vec2> = (0..5).map(|x| U16Vec2::new(x, 0)).collect();
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
assert!(!coastal.is_empty());
|
||||
assert!(coastal.len() == 5);
|
||||
|
||||
// All tiles in row 1 should be coastal
|
||||
for x in 0..5 {
|
||||
assert!(coastal.contains(U16Vec2::new(x, 1)));
|
||||
}
|
||||
|
||||
// Water tiles themselves should not be coastal
|
||||
for x in 0..5 {
|
||||
assert!(!coastal.contains(U16Vec2::new(x, 0)));
|
||||
}
|
||||
|
||||
// Tiles further inland should not be coastal
|
||||
for x in 0..5 {
|
||||
assert!(!coastal.contains(U16Vec2::new(x, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_island_configuration() {
|
||||
// 5x5 grid with water around the edges and land in the middle
|
||||
let size = U16Vec2::new(5, 5);
|
||||
let mut water_tiles = Vec::new();
|
||||
|
||||
// Top and bottom edges
|
||||
for x in 0..5 {
|
||||
water_tiles.push(U16Vec2::new(x, 0));
|
||||
water_tiles.push(U16Vec2::new(x, 4));
|
||||
}
|
||||
|
||||
// Left and right edges
|
||||
for y in 1..4 {
|
||||
water_tiles.push(U16Vec2::new(0, y));
|
||||
water_tiles.push(U16Vec2::new(4, y));
|
||||
}
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
// The inner ring of land tiles (1,1), (2,1), (3,1), (1,2), (3,2), (1,3), (2,3), (3,3)
|
||||
// should be coastal, but (2,2) should not be
|
||||
assert!(!coastal.is_empty());
|
||||
assert!(coastal.len() == 8);
|
||||
|
||||
// Check the outer ring of land is coastal
|
||||
assert!(coastal.contains(U16Vec2::new(1, 1)));
|
||||
assert!(coastal.contains(U16Vec2::new(2, 1)));
|
||||
assert!(coastal.contains(U16Vec2::new(3, 1)));
|
||||
assert!(coastal.contains(U16Vec2::new(1, 2)));
|
||||
assert!(coastal.contains(U16Vec2::new(3, 2)));
|
||||
assert!(coastal.contains(U16Vec2::new(1, 3)));
|
||||
assert!(coastal.contains(U16Vec2::new(2, 3)));
|
||||
assert!(coastal.contains(U16Vec2::new(3, 3)));
|
||||
|
||||
// Center tile should not be coastal (no water neighbors)
|
||||
assert!(!coastal.contains(U16Vec2::new(2, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tiles_returns_correct_set() {
|
||||
let size = U16Vec2::new(3, 3);
|
||||
let water_tiles = vec![U16Vec2::new(1, 1)];
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
let tiles = coastal.tiles();
|
||||
|
||||
// Verify it's the correct type and contains the right tiles
|
||||
let expected: HashSet<U16Vec2> = vec![U16Vec2::new(1, 0), U16Vec2::new(0, 1), U16Vec2::new(2, 1), U16Vec2::new(1, 2)].into_iter().collect();
|
||||
|
||||
assert!(tiles == &expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_len_with_various_sizes() {
|
||||
// Test that len() returns accurate counts
|
||||
|
||||
// Empty case
|
||||
let size = U16Vec2::new(3, 3);
|
||||
let terrain = create_terrain(&[], size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
assert!(coastal.len() == 0);
|
||||
|
||||
// Single water tile -> 4 coastal tiles
|
||||
let water_tiles = vec![U16Vec2::new(1, 1)];
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
assert!(coastal.len() == 4);
|
||||
|
||||
// Two adjacent water tiles -> 6 coastal tiles
|
||||
// Water at (1,1) and (2,1) creates coastal tiles at (1,0), (0,1), (2,0), (3,1), (1,2), (2,2)
|
||||
let size = U16Vec2::new(4, 3);
|
||||
let water_tiles = vec![U16Vec2::new(1, 1), U16Vec2::new(2, 1)];
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
assert!(coastal.len() == 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains_with_out_of_bounds() {
|
||||
let size = U16Vec2::new(3, 3);
|
||||
let water_tiles = vec![U16Vec2::new(0, 0)];
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
// Valid coastal tile
|
||||
assert!(coastal.contains(U16Vec2::new(1, 0)));
|
||||
assert!(coastal.contains(U16Vec2::new(0, 1)));
|
||||
|
||||
// Out of bounds tiles should not be in the set
|
||||
assert!(!coastal.contains(U16Vec2::new(100, 100)));
|
||||
assert!(!coastal.contains(U16Vec2::new(5, 5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_empty_true_and_false() {
|
||||
let size = U16Vec2::new(3, 3);
|
||||
|
||||
// Empty case - no water
|
||||
let terrain = create_terrain(&[], size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
assert!(coastal.is_empty());
|
||||
|
||||
// Non-empty case - has water
|
||||
let water_tiles = vec![U16Vec2::new(1, 1)];
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
assert!(!coastal.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_corner_water_tile() {
|
||||
// Test water tile in the corner (0,0)
|
||||
let size = U16Vec2::new(3, 3);
|
||||
let water_tiles = vec![U16Vec2::new(0, 0)];
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
// Corner water tile has only 2 neighbors
|
||||
assert!(coastal.len() == 2);
|
||||
assert!(coastal.contains(U16Vec2::new(1, 0)));
|
||||
assert!(coastal.contains(U16Vec2::new(0, 1)));
|
||||
assert!(!coastal.contains(U16Vec2::new(0, 0))); // Water itself is not coastal
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_disconnected_water_bodies() {
|
||||
// Test multiple separate water bodies
|
||||
let size = U16Vec2::new(5, 5);
|
||||
let water_tiles = vec![
|
||||
U16Vec2::new(1, 1), // First water body
|
||||
U16Vec2::new(3, 3), // Second water body
|
||||
];
|
||||
|
||||
let terrain = create_terrain(&water_tiles, size);
|
||||
let coastal = CoastalTiles::compute(&terrain, size);
|
||||
|
||||
// Each water tile should create coastal tiles around it
|
||||
assert!(!coastal.is_empty());
|
||||
|
||||
// Check coastal tiles around first water body
|
||||
assert!(coastal.contains(U16Vec2::new(1, 0)));
|
||||
assert!(coastal.contains(U16Vec2::new(0, 1)));
|
||||
assert!(coastal.contains(U16Vec2::new(2, 1)));
|
||||
assert!(coastal.contains(U16Vec2::new(1, 2)));
|
||||
|
||||
// Check coastal tiles around second water body
|
||||
assert!(coastal.contains(U16Vec2::new(3, 2)));
|
||||
assert!(coastal.contains(U16Vec2::new(2, 3)));
|
||||
assert!(coastal.contains(U16Vec2::new(4, 3)));
|
||||
assert!(coastal.contains(U16Vec2::new(3, 4)));
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
308
crates/borders-core/tests/full_game_smoke_tests.rs
Normal file
308
crates/borders-core/tests/full_game_smoke_tests.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
//! Full game smoke tests that run complete games with bots
|
||||
//!
|
||||
//! These tests verify the entire game lifecycle by running complete games
|
||||
//! for 600 ticks (60 seconds equivalent at 10 TPS) with multiple bots.
|
||||
//! The tests ensure the game doesn't crash and that all systems execute properly.
|
||||
|
||||
mod common;
|
||||
|
||||
use assert2::assert;
|
||||
use borders_core::prelude::*;
|
||||
use common::MapBuilder;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Statistics collected during a full game run
|
||||
#[derive(Debug)]
|
||||
struct GameStats {
|
||||
/// Final turn number reached
|
||||
final_turn: u64,
|
||||
/// Total tiles owned by all players
|
||||
total_territory_owned: u32,
|
||||
/// Number of bots that have claimed territory
|
||||
bots_with_territory: usize,
|
||||
/// Number of bots that took at least one action
|
||||
active_bots: usize,
|
||||
/// Total number of bot actions taken
|
||||
total_bot_actions: usize,
|
||||
}
|
||||
|
||||
/// Set up a test game using shared initialization code
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `player_count` - Number of bot players (no human players)
|
||||
/// * `map_size` - (width, height) of the map in tiles
|
||||
///
|
||||
/// # Returns
|
||||
/// Configured App ready to run game updates
|
||||
fn setup_test_game(player_count: usize, map_size: (u16, u16)) -> App {
|
||||
let (map_width, map_height) = map_size;
|
||||
|
||||
// Create the main app
|
||||
let mut app = App::new();
|
||||
app.insert_resource(Time::new());
|
||||
|
||||
// Initialize UI messages if the ui feature is enabled
|
||||
#[cfg(feature = "ui")]
|
||||
{
|
||||
use borders_core::ui::protocol::{BackendMessage, FrontendMessage};
|
||||
app.add_message::<BackendMessage>();
|
||||
app.add_message::<FrontendMessage>();
|
||||
}
|
||||
|
||||
// Add GamePlugin to set up all systems
|
||||
GamePlugin::new(NetworkMode::Local).build(&mut app);
|
||||
|
||||
// Generate terrain - all land tiles for maximum playable area
|
||||
let terrain_data = MapBuilder::new(map_width, map_height).all_conquerable().build();
|
||||
|
||||
let tile_count = (map_width as usize) * (map_height as usize);
|
||||
let conquerable_tiles = vec![true; tile_count];
|
||||
|
||||
// Create intent channel (required by GamePlugin)
|
||||
let (_intent_tx, intent_rx) = flume::unbounded();
|
||||
|
||||
// Use shared initialization with test parameters
|
||||
let params = GameInitParamsBuilder::new(map_width, map_height, conquerable_tiles, NationId::ZERO, intent_rx, Arc::new(terrain_data))
|
||||
.with_bot_count(player_count)
|
||||
.with_human_player_count(0) // All bots
|
||||
.skip_spawn_phase() // Skip spawn phase for immediate game start
|
||||
.build();
|
||||
|
||||
borders_core::game::initialize_game_resources(&mut app.world_mut().commands(), params);
|
||||
|
||||
// Run startup systems and apply all pending commands
|
||||
app.run_startup();
|
||||
app.finish();
|
||||
app.cleanup();
|
||||
|
||||
// Apply commands so resources exist (but don't run game systems yet)
|
||||
app.world_mut().flush();
|
||||
|
||||
// When skipping spawn phase, immediately mark the game as started
|
||||
if let Some(mut generator) = app.world_mut().get_resource_mut::<borders_core::networking::server::TurnGenerator>() {
|
||||
generator.start_game_immediately();
|
||||
}
|
||||
|
||||
app
|
||||
}
|
||||
|
||||
/// Run a complete game with the specified number of bot players
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `player_count` - Number of bot players (no human players)
|
||||
/// * `map_size` - (width, height) of the map in tiles
|
||||
/// * `target_ticks` - Number of game ticks to run (10 TPS = 100ms per tick)
|
||||
///
|
||||
/// # Returns
|
||||
/// Statistics about the completed game
|
||||
fn run_full_game(player_count: usize, map_size: (u16, u16), target_ticks: u64) -> GameStats {
|
||||
let mut app = setup_test_game(player_count, map_size);
|
||||
|
||||
// Debug: Check initial state
|
||||
eprintln!("=== Initial State ===");
|
||||
eprintln!("TurnGenerator exists: {}", app.world().get_resource::<borders_core::networking::server::TurnGenerator>().is_some());
|
||||
eprintln!("TurnReceiver exists: {}", app.world().get_resource::<borders_core::networking::server::TurnReceiver>().is_some());
|
||||
eprintln!("LocalPlayerContext exists: {}", app.world().get_resource::<borders_core::game::LocalPlayerContext>().is_some());
|
||||
if let Some(handle) = app.world().get_resource::<borders_core::networking::server::LocalTurnServerHandle>() {
|
||||
eprintln!("Server running: {}, paused: {}", handle.is_running(), handle.is_paused());
|
||||
}
|
||||
if let Some(spawn_manager) = app.world().get_resource::<borders_core::game::SpawnManager>() {
|
||||
eprintln!("SpawnManager spawns: {} bots, {} players", spawn_manager.get_bot_spawns().len(), spawn_manager.get_player_spawns().len());
|
||||
}
|
||||
|
||||
// Track initial and final territory sizes to verify bot activity
|
||||
let initial_territory_sizes: HashMap<_, _> = {
|
||||
use borders_core::game::ai::bot::Bot;
|
||||
use borders_core::game::entities::TerritorySize;
|
||||
let world = app.world_mut();
|
||||
let mut query = world.query::<(&Bot, &NationId, &TerritorySize)>();
|
||||
query.iter(world).map(|(_, &id, size)| (id, size.0)).collect()
|
||||
};
|
||||
|
||||
// Main game loop - run for target_ticks iterations
|
||||
for tick_num in 0..target_ticks {
|
||||
// Advance time by exactly 100ms per tick (10 TPS)
|
||||
if let Some(mut time) = app.world_mut().get_resource_mut::<Time>() {
|
||||
#[allow(deprecated)]
|
||||
time.update(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// Update the app - this runs all Update and Last schedules
|
||||
app.update();
|
||||
|
||||
// Debug: Check BorderCache on first few turns
|
||||
if tick_num == 1 {
|
||||
use borders_core::game::BorderCache;
|
||||
|
||||
let world = app.world();
|
||||
if let Some(border_cache) = world.get_resource::<BorderCache>() {
|
||||
let cache_map = border_cache.as_map();
|
||||
let mut cache_keys: Vec<u16> = cache_map.keys().map(|id| id.get()).collect();
|
||||
cache_keys.sort();
|
||||
eprintln!("Tick {}: BorderCache has {} entries", tick_num, cache_map.len());
|
||||
eprintln!(" BorderCache keys (sorted): {:?}", cache_keys);
|
||||
if player_count <= 10 {
|
||||
for (id, borders) in &cache_map {
|
||||
eprintln!(" Player {}: {} border tiles", id.get(), borders.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print current turn for debugging
|
||||
if let Some(current_turn) = app.world().get_resource::<CurrentTurn>() {
|
||||
if tick_num <= 5 {
|
||||
eprintln!(" CurrentTurn = Turn({})", current_turn.turn.turn_number);
|
||||
}
|
||||
} else if tick_num < 10 {
|
||||
eprintln!("Tick {}: No CurrentTurn resource", tick_num);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect final statistics
|
||||
let final_turn = app.world().get_resource::<CurrentTurn>().map(|ct| ct.turn.turn_number).unwrap_or(0);
|
||||
|
||||
// Calculate territory ownership, border tiles, and troop levels from ECS components
|
||||
use borders_core::game::BorderTiles;
|
||||
use borders_core::game::ai::bot::Bot;
|
||||
use borders_core::game::entities::{TerritorySize, Troops};
|
||||
let (total_territory_owned, bots_with_territory, bots_with_borders, avg_troops) = {
|
||||
let world = app.world_mut();
|
||||
let mut total = 0u32;
|
||||
let mut bots_count = 0usize;
|
||||
let mut borders_count = 0usize;
|
||||
let mut total_troops = 0.0f32;
|
||||
let mut bot_count = 0;
|
||||
|
||||
for (_, territory_size, border_tiles, troops) in world.query::<(&Bot, &TerritorySize, &BorderTiles, &Troops)>().iter(world) {
|
||||
total += territory_size.0;
|
||||
if territory_size.0 > 0 {
|
||||
bots_count += 1;
|
||||
}
|
||||
if !border_tiles.0.is_empty() {
|
||||
borders_count += 1;
|
||||
}
|
||||
total_troops += troops.0;
|
||||
bot_count += 1;
|
||||
}
|
||||
let avg = if bot_count > 0 { total_troops / bot_count as f32 } else { 0.0 };
|
||||
(total, bots_count, borders_count, avg)
|
||||
};
|
||||
|
||||
// Calculate how many bots expanded their territory (proxy for activity)
|
||||
let final_territory_sizes: HashMap<_, _> = {
|
||||
use borders_core::game::ai::bot::Bot;
|
||||
use borders_core::game::entities::TerritorySize;
|
||||
let world = app.world_mut();
|
||||
let mut query = world.query::<(&Bot, &NationId, &TerritorySize)>();
|
||||
query.iter(world).map(|(_, &id, size)| (id, size.0)).collect()
|
||||
};
|
||||
|
||||
let active_bots = final_territory_sizes
|
||||
.iter()
|
||||
.filter(|(id, final_size)| {
|
||||
let initial_size = initial_territory_sizes.get(id).copied().unwrap_or(0);
|
||||
**final_size > initial_size + 10 // Expanded by more than 10 tiles
|
||||
})
|
||||
.count();
|
||||
|
||||
let territory_changes: usize = final_territory_sizes
|
||||
.iter()
|
||||
.map(|(id, final_size)| {
|
||||
let initial_size = initial_territory_sizes.get(id).copied().unwrap_or(0);
|
||||
final_size.saturating_sub(initial_size) as usize
|
||||
})
|
||||
.sum();
|
||||
|
||||
eprintln!("=== Final Game Stats ===");
|
||||
eprintln!("Bots with territory: {}/{}", bots_with_territory, player_count);
|
||||
eprintln!("Bots with border tiles: {}/{}", bots_with_borders, player_count);
|
||||
eprintln!("Average bot troops: {:.1}", avg_troops);
|
||||
eprintln!("Bots that expanded territory: {}/{}", active_bots, player_count);
|
||||
eprintln!("Total territory owned: {}", total_territory_owned);
|
||||
eprintln!("Total territory gained: {}", territory_changes);
|
||||
|
||||
GameStats { final_turn, total_territory_owned, bots_with_territory, active_bots, total_bot_actions: territory_changes }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smoke_test_10_players() {
|
||||
// Initialize tracing for debugging
|
||||
let _ = tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("trace"))).with_test_writer().try_init();
|
||||
|
||||
let player_count = 10;
|
||||
let map_size = (50, 50); // 2,500 tiles = 250 per player
|
||||
let target_ticks = 600; // 60 seconds at 10 TPS
|
||||
|
||||
let stats = run_full_game(player_count, map_size, target_ticks);
|
||||
|
||||
// Verify game completed without panicking
|
||||
eprintln!("10-player game stats: {:#?}", stats);
|
||||
|
||||
// Game should have progressed
|
||||
assert!(stats.final_turn > 0, "Game should have progressed past turn 0");
|
||||
|
||||
// Bots should have claimed territory (spawns applied)
|
||||
assert!(stats.bots_with_territory > 0, "Bots should have claimed territory after spawning");
|
||||
|
||||
// Total territory should be owned
|
||||
assert!(stats.total_territory_owned > 0, "Territory should be owned by players");
|
||||
|
||||
// At least some bots should have expanded their territory
|
||||
assert!(stats.active_bots > 0, "At least some bots should have expanded territory (active_bots: {}, total_territory_gained: {})", stats.active_bots, stats.total_bot_actions);
|
||||
|
||||
// Total territory gained should be significant
|
||||
assert!(stats.total_bot_actions > 100, "Bots should have expanded significantly during gameplay (gained: {} tiles)", stats.total_bot_actions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smoke_test_100_players() {
|
||||
let player_count = 100;
|
||||
let map_size = (160, 160); // 25,600 tiles = 256 per player
|
||||
let target_ticks = 600; // 60 seconds at 10 TPS
|
||||
|
||||
let stats = run_full_game(player_count, map_size, target_ticks);
|
||||
|
||||
// Verify game completed without panicking
|
||||
eprintln!("100-player game stats: {:#?}", stats);
|
||||
|
||||
// Game should have progressed
|
||||
assert!(stats.final_turn > 0, "Game should have progressed past turn 0");
|
||||
|
||||
// Bots should have claimed territory (spawns applied)
|
||||
assert!(stats.bots_with_territory > 0, "Bots should have claimed territory after spawning");
|
||||
|
||||
// Total territory should be owned
|
||||
assert!(stats.total_territory_owned > 0, "Territory should be owned by players");
|
||||
|
||||
// Most bots should be active with this many players
|
||||
assert!(stats.active_bots >= player_count / 2, "At least half of the bots should have taken actions (active: {}/{})", stats.active_bots, player_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smoke_test_500_players() {
|
||||
let player_count = 500;
|
||||
let map_size = (360, 360); // 129,600 tiles = 259 per player
|
||||
let target_ticks = 600; // 60 seconds at 10 TPS
|
||||
|
||||
let stats = run_full_game(player_count, map_size, target_ticks);
|
||||
|
||||
// Verify game completed without panicking
|
||||
eprintln!("500-player game stats: {:#?}", stats);
|
||||
|
||||
// Game should have progressed
|
||||
assert!(stats.final_turn > 0, "Game should have progressed past turn 0");
|
||||
|
||||
// Bots should have claimed territory (spawns applied)
|
||||
assert!(stats.bots_with_territory > 0, "Bots should have claimed territory after spawning");
|
||||
|
||||
// Total territory should be owned
|
||||
assert!(stats.total_territory_owned > 0, "Territory should be owned by players");
|
||||
|
||||
// With this many players, significant activity should occur
|
||||
assert!(stats.active_bots >= player_count / 4, "At least 25% of bots should have taken actions with 500 players (active: {}/{})", stats.active_bots, player_count);
|
||||
|
||||
// Large number of total actions expected
|
||||
assert!(stats.total_bot_actions >= 100, "Significant number of actions should occur with 500 players (actual: {})", stats.total_bot_actions);
|
||||
}
|
||||
130
crates/borders-core/tests/integration_tests.rs
Normal file
130
crates/borders-core/tests/integration_tests.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
// Integration tests for multi-system interactions: spawn phase, territory conquest, attacks
|
||||
|
||||
mod common;
|
||||
|
||||
use assert2::assert;
|
||||
use borders_core::prelude::*;
|
||||
use common::{TestWorld, WorldAssertExt, WorldTestExt};
|
||||
|
||||
#[test]
|
||||
fn test_spawn_phase_lifecycle() {
|
||||
let mut world = TestWorld::new().with_spawn_phase().with_player(NationId::ZERO, 100.0).build();
|
||||
|
||||
let spawn_phase = world.resource::<SpawnPhase>();
|
||||
assert!(spawn_phase.active, "SpawnPhase should be active after initialization");
|
||||
|
||||
world.deactivate_spawn_phase();
|
||||
|
||||
let spawn_phase = world.resource::<SpawnPhase>();
|
||||
assert!(!spawn_phase.active, "SpawnPhase should be inactive after deactivation");
|
||||
|
||||
world.assert().resource_exists::<TerritoryManager>("TerritoryManager");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_territory_conquest_triggers_changes() {
|
||||
let player0 = NationId::ZERO;
|
||||
|
||||
let mut world = TestWorld::new().with_player(player0, 100.0).build();
|
||||
|
||||
world.assert().no_territory_changes();
|
||||
|
||||
let tile = U16Vec2::new(50, 50);
|
||||
world.conquer_tile(tile, player0);
|
||||
|
||||
world.assert().has_territory_changes().player_owns(tile, player0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_player_territory_conquest() {
|
||||
let player0 = NationId::ZERO;
|
||||
let player1 = NationId::new(1).unwrap();
|
||||
let player2 = NationId::new(2).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player0, 100.0).with_player(player1, 100.0).with_player(player2, 100.0).build();
|
||||
|
||||
let tile0 = U16Vec2::new(10, 10);
|
||||
let tile1 = U16Vec2::new(20, 20);
|
||||
let tile2 = U16Vec2::new(30, 30);
|
||||
|
||||
world.conquer_tile(tile0, player0);
|
||||
world.conquer_tile(tile1, player1);
|
||||
world.conquer_tile(tile2, player2);
|
||||
|
||||
world.assert().player_owns(tile0, player0).player_owns(tile1, player1).player_owns(tile2, player2);
|
||||
|
||||
let territory_manager = world.resource::<TerritoryManager>();
|
||||
let changes: Vec<_> = territory_manager.iter_changes().collect();
|
||||
assert!(changes.len() == 3, "Should have 3 changes (one per player), but found {}", changes.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_territory_ownership_transitions() {
|
||||
let player0 = NationId::ZERO;
|
||||
let player1 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_player(player0, 100.0).with_player(player1, 100.0).build();
|
||||
|
||||
let tile = U16Vec2::new(50, 50);
|
||||
|
||||
world.assert().tile_unclaimed(tile);
|
||||
|
||||
world.conquer_tile(tile, player0);
|
||||
world.assert().player_owns(tile, player0);
|
||||
|
||||
world.clear_territory_changes();
|
||||
world.assert().no_territory_changes();
|
||||
|
||||
let previous_owner = world.clear_tile(tile);
|
||||
assert!(previous_owner == Some(player0), "Previous owner should be player 0");
|
||||
world.assert().tile_unclaimed(tile).has_territory_changes();
|
||||
|
||||
world.clear_territory_changes();
|
||||
|
||||
world.conquer_tile(tile, player1);
|
||||
world.assert().player_owns(tile, player1).has_territory_changes();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_border_updates_on_territory_change() {
|
||||
let player0 = NationId::ZERO;
|
||||
|
||||
let mut world = TestWorld::new().with_player(player0, 100.0).with_map_size(100, 100).build();
|
||||
|
||||
let center = U16Vec2::new(50, 50);
|
||||
let adjacent = U16Vec2::new(51, 50);
|
||||
|
||||
world.conquer_tile(center, player0);
|
||||
|
||||
let territory_manager = world.resource::<TerritoryManager>();
|
||||
assert!(territory_manager.is_border(center), "Center tile should be a border (adjacent to unclaimed)");
|
||||
|
||||
world.conquer_tile(adjacent, player0);
|
||||
|
||||
let territory_manager = world.resource::<TerritoryManager>();
|
||||
assert!(territory_manager.is_border(center), "Center tile should still be a border");
|
||||
assert!(territory_manager.is_border(adjacent), "Adjacent tile should be a border");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialization_with_territories() {
|
||||
let player0 = NationId::ZERO;
|
||||
let player1 = NationId::new(1).unwrap();
|
||||
|
||||
let territory0 = vec![U16Vec2::new(10, 10), U16Vec2::new(10, 11)];
|
||||
let territory1 = vec![U16Vec2::new(20, 20), U16Vec2::new(20, 21)];
|
||||
|
||||
let world = TestWorld::new().with_player(player0, 100.0).with_player(player1, 100.0).with_territory(player0, &territory0).with_territory(player1, &territory1).build();
|
||||
|
||||
let mut assertions = world.assert();
|
||||
for tile in &territory0 {
|
||||
assertions = assertions.player_owns(*tile, player0);
|
||||
}
|
||||
for tile in &territory1 {
|
||||
assertions = assertions.player_owns(*tile, player1);
|
||||
}
|
||||
|
||||
let territory_manager = world.resource::<TerritoryManager>();
|
||||
let changes_count = territory_manager.iter_changes().count();
|
||||
assert!(changes_count == 4, "After initialization with 4 tiles, expected 4 changes, but found {}", changes_count);
|
||||
}
|
||||
111
crates/borders-core/tests/lifecycle_tests.rs
Normal file
111
crates/borders-core/tests/lifecycle_tests.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
// Game lifecycle tests: initialization, player spawning, cleanup
|
||||
|
||||
mod common;
|
||||
|
||||
use assert2::assert;
|
||||
use std::sync::Arc;
|
||||
|
||||
use borders_core::prelude::*;
|
||||
|
||||
use common::{MapBuilder, WorldAssertExt};
|
||||
|
||||
// Helper to eliminate 15+ lines of boilerplate in each test
|
||||
fn create_initialized_world(map_width: u16, map_height: u16) -> World {
|
||||
let mut world = World::new();
|
||||
|
||||
let terrain_data = Arc::new(MapBuilder::new(map_width, map_height).all_conquerable().build());
|
||||
let conquerable_tiles: Vec<bool> = (0..(map_width as usize * map_height as usize)).map(|_| true).collect();
|
||||
|
||||
let (_intent_tx, intent_rx) = flume::unbounded();
|
||||
|
||||
let params = GameInitParamsBuilder::for_testing(map_width, map_height, conquerable_tiles, NationId::ZERO, intent_rx, terrain_data).build();
|
||||
|
||||
borders_core::game::initialize_game_resources(&mut world.commands(), params);
|
||||
world.flush();
|
||||
world
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialize_game_creates_all_resources() {
|
||||
let world = create_initialized_world(100, 100);
|
||||
|
||||
world.assert().resource_exists::<TerritoryManager>("TerritoryManager").resource_exists::<ActiveAttacks>("ActiveAttacks").resource_exists::<TerrainData>("TerrainData").resource_exists::<DeterministicRng>("DeterministicRng").resource_exists::<CoastalTiles>("CoastalTiles").resource_exists::<PlayerEntityMap>("PlayerEntityMap").resource_exists::<ClientPlayerId>("ClientPlayerId").resource_exists::<HumanPlayerCount>("HumanPlayerCount").resource_exists::<LocalPlayerContext>("LocalPlayerContext").resource_exists::<SpawnPhase>("SpawnPhase").resource_exists::<SpawnTimeout>("SpawnTimeout").resource_exists::<SpawnManager>("SpawnManager").resource_exists::<server::LocalTurnServerHandle>("LocalTurnServerHandle").resource_exists::<server::TurnReceiver>("TurnReceiver").resource_exists::<server::TurnGenerator>("TurnGenerator");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_initialize_creates_player_entities() {
|
||||
let world = create_initialized_world(100, 100);
|
||||
|
||||
// Get entity map
|
||||
let entity_map = world.resource::<PlayerEntityMap>();
|
||||
|
||||
// Verify 1 human + 10 bots = total players (matches create_initialized_world params)
|
||||
let expected_bot_count = 10; // From create_initialized_world params
|
||||
assert!(entity_map.0.len() == 1 + expected_bot_count, "Should have exactly {} player entities (1 human + {} bots), but found {}", 1 + expected_bot_count, expected_bot_count, entity_map.0.len());
|
||||
|
||||
// Verify human player (ID 0) exists
|
||||
let human_entity = entity_map.0.get(&NationId::ZERO).expect("Human player entity not found");
|
||||
|
||||
// Verify human has all required components
|
||||
assert!(world.get::<PlayerName>(*human_entity).is_some(), "Human player missing PlayerName component");
|
||||
assert!(world.get::<PlayerColor>(*human_entity).is_some(), "Human player missing PlayerColor component");
|
||||
assert!(world.get::<BorderTiles>(*human_entity).is_some(), "Human player missing BorderTiles component");
|
||||
assert!(world.get::<Troops>(*human_entity).is_some(), "Human player missing Troops component");
|
||||
assert!(world.get::<TerritorySize>(*human_entity).is_some(), "Human player missing TerritorySize component");
|
||||
|
||||
// Verify initial troops are correct
|
||||
let troops = world.get::<Troops>(*human_entity).unwrap();
|
||||
assert!(troops.0 == player::INITIAL_TROOPS, "Human player should start with {} troops, but has {}", player::INITIAL_TROOPS, troops.0);
|
||||
|
||||
// Verify territory size starts at 0
|
||||
let territory_size = world.get::<TerritorySize>(*human_entity).unwrap();
|
||||
assert!(territory_size.0 == 0, "Human player should start with 0 territory, but has {}", territory_size.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spawn_phase_activates() {
|
||||
let world = create_initialized_world(100, 100);
|
||||
|
||||
// Verify spawn phase is active
|
||||
let spawn_phase = world.resource::<SpawnPhase>();
|
||||
assert!(spawn_phase.active, "SpawnPhase should be active after initialization");
|
||||
|
||||
let spawn_timeout = world.resource::<SpawnTimeout>();
|
||||
assert!(spawn_timeout.duration_secs > 0.0, "SpawnTimeout should have a positive duration");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cleanup_removes_all_resources() {
|
||||
let mut world = create_initialized_world(100, 100);
|
||||
|
||||
world.assert().resource_exists::<TerritoryManager>("TerritoryManager").resource_exists::<ActiveAttacks>("ActiveAttacks");
|
||||
|
||||
borders_core::game::cleanup_game_resources(&mut world);
|
||||
|
||||
world.assert().resource_missing::<TerritoryManager>("TerritoryManager").resource_missing::<ActiveAttacks>("ActiveAttacks").resource_missing::<TerrainData>("TerrainData").resource_missing::<DeterministicRng>("DeterministicRng").resource_missing::<CoastalTiles>("CoastalTiles").resource_missing::<LocalPlayerContext>("LocalPlayerContext").resource_missing::<server::TurnReceiver>("TurnReceiver").resource_missing::<SpawnManager>("SpawnManager").resource_missing::<SpawnTimeout>("SpawnTimeout").resource_missing::<server::TurnGenerator>("TurnGenerator");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_start_game_twice() {
|
||||
let world = create_initialized_world(100, 100);
|
||||
|
||||
world.assert().resource_exists::<TerritoryManager>("TerritoryManager");
|
||||
|
||||
let entity_map = world.resource::<PlayerEntityMap>();
|
||||
let _initial_count = entity_map.0.len();
|
||||
|
||||
let has_territory_manager = world.contains_resource::<TerritoryManager>();
|
||||
assert!(has_territory_manager, "This check prevents double-initialization in production code");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_territory_manager_dimensions() {
|
||||
let map_width = 80u16;
|
||||
let map_height = 60u16;
|
||||
let world = create_initialized_world(map_width, map_height);
|
||||
|
||||
let territory_manager = world.resource::<TerritoryManager>();
|
||||
assert!(territory_manager.width() == map_width, "TerritoryManager width should match map width: expected {}, got {}", map_width, territory_manager.width());
|
||||
assert!(territory_manager.height() == map_height, "TerritoryManager height should match map height: expected {}, got {}", map_height, territory_manager.height());
|
||||
assert!(territory_manager.len() == (map_width as usize) * (map_height as usize), "TerritoryManager should have width × height tiles: expected {}, got {}", (map_width as usize) * (map_height as usize), territory_manager.len());
|
||||
}
|
||||
64
crates/borders-core/tests/networking_tests.rs
Normal file
64
crates/borders-core/tests/networking_tests.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
// Turn generation behavior tests: spawn timeout, turn intervals, intent validation
|
||||
|
||||
use assert2::assert;
|
||||
use borders_core::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_spawn_phase_timeout() {
|
||||
let mut generator = server::SharedTurnGenerator::new();
|
||||
|
||||
let sourced_intent = SourcedIntent { source: NationId::new_unchecked(1), intent_id: 1, intent: Intent::SetSpawn { tile_index: U16Vec2::new(5, 5) } };
|
||||
let output = generator.process_intent(sourced_intent);
|
||||
assert!(matches!(output, server::TurnOutput::SpawnUpdate(_)));
|
||||
|
||||
let output = generator.tick(2000.0, vec![]);
|
||||
assert!(matches!(output, server::TurnOutput::None));
|
||||
|
||||
let output = generator.tick(3500.0, vec![]);
|
||||
assert!(matches!(&output, server::TurnOutput::Turn(turn) if turn.turn_number == 0), "Expected Turn(0) after spawn timeout, got {:?}", output);
|
||||
assert!(generator.game_started());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_turn_generation() {
|
||||
let mut generator = server::SharedTurnGenerator::new();
|
||||
|
||||
generator.process_intent(SourcedIntent { source: NationId::new_unchecked(1), intent_id: 1, intent: Intent::SetSpawn { tile_index: U16Vec2::new(5, 5) } });
|
||||
generator.tick(6000.0, vec![]);
|
||||
|
||||
let output = generator.tick(50.0, vec![]);
|
||||
assert!(matches!(output, server::TurnOutput::None));
|
||||
|
||||
let sourced_intents = vec![SourcedIntent { source: NationId::new_unchecked(1), intent_id: 2, intent: Intent::Action(GameAction::Attack { target: Some(NationId::new_unchecked(2)), troops: 100 }) }];
|
||||
let output = generator.tick(60.0, sourced_intents);
|
||||
if let server::TurnOutput::Turn(turn) = output {
|
||||
assert!(turn.turn_number == 1);
|
||||
assert!(turn.intents.len() == 1);
|
||||
} else {
|
||||
panic!("Expected TurnOutput::Turn");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ignore_action_during_spawn() {
|
||||
let mut generator = server::SharedTurnGenerator::new();
|
||||
|
||||
let sourced_intent = SourcedIntent { source: NationId::new_unchecked(1), intent_id: 1, intent: Intent::Action(GameAction::Attack { target: None, troops: 50 }) };
|
||||
let output = generator.process_intent(sourced_intent);
|
||||
assert!(matches!(output, server::TurnOutput::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_intent_id() {
|
||||
let mut generator = server::SharedTurnGenerator::new();
|
||||
|
||||
generator.process_intent(SourcedIntent { source: NationId::new_unchecked(1), intent_id: 1, intent: Intent::SetSpawn { tile_index: U16Vec2::new(5, 5) } });
|
||||
generator.tick(6000.0, vec![]);
|
||||
|
||||
let sourced_intent1 = SourcedIntent { source: NationId::new_unchecked(1), intent_id: 2, intent: Intent::Action(GameAction::Attack { target: None, troops: 50 }) };
|
||||
let sourced_intent2 = SourcedIntent { source: NationId::new_unchecked(1), intent_id: 2, intent: Intent::Action(GameAction::Attack { target: None, troops: 100 }) };
|
||||
|
||||
generator.process_intent(sourced_intent1);
|
||||
let output = generator.process_intent(sourced_intent2);
|
||||
assert!(matches!(output, server::TurnOutput::None));
|
||||
}
|
||||
67
crates/borders-core/tests/spawn_tests.rs
Normal file
67
crates/borders-core/tests/spawn_tests.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
// Spawn system tests: activation, deactivation, territory claiming
|
||||
|
||||
mod common;
|
||||
|
||||
use assert2::assert;
|
||||
use borders_core::prelude::*;
|
||||
use common::{TestWorld, WorldAssertExt, WorldTestExt};
|
||||
|
||||
#[test]
|
||||
fn test_spawn_phase_activation() {
|
||||
let world = TestWorld::new().with_spawn_phase().with_player(NationId::ZERO, 100.0).build();
|
||||
|
||||
world.assert().resource_exists::<SpawnPhase>("SpawnPhase");
|
||||
|
||||
let spawn_phase = world.resource::<SpawnPhase>();
|
||||
assert!(spawn_phase.active, "SpawnPhase should be active when initialized with with_spawn_phase()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spawn_phase_deactivation() {
|
||||
let mut world = TestWorld::new().with_spawn_phase().with_player(NationId::ZERO, 100.0).build();
|
||||
|
||||
assert!(world.resource::<SpawnPhase>().active);
|
||||
|
||||
world.deactivate_spawn_phase();
|
||||
|
||||
assert!(!world.resource::<SpawnPhase>().active, "SpawnPhase should be inactive after deactivation");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spawn_territory_claiming() {
|
||||
let player0 = NationId::ZERO;
|
||||
let player1 = NationId::new(1).unwrap();
|
||||
|
||||
let mut world = TestWorld::new().with_spawn_phase().with_player(player0, 100.0).with_player(player1, 100.0).build();
|
||||
|
||||
let spawn0 = U16Vec2::new(25, 25);
|
||||
let spawn1 = U16Vec2::new(75, 75);
|
||||
|
||||
world.conquer_tile(spawn0, player0);
|
||||
world.conquer_tile(spawn1, player1);
|
||||
|
||||
world.assert().player_owns(spawn0, player0).player_owns(spawn1, player1);
|
||||
|
||||
assert!(world.resource::<SpawnPhase>().active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spawn_with_pre_assigned_territories() {
|
||||
let player0 = NationId::ZERO;
|
||||
let player1 = NationId::new(1).unwrap();
|
||||
|
||||
let spawn_territory0 = vec![U16Vec2::new(25, 25), U16Vec2::new(25, 26), U16Vec2::new(26, 25)];
|
||||
let spawn_territory1 = vec![U16Vec2::new(75, 75), U16Vec2::new(75, 76), U16Vec2::new(76, 75)];
|
||||
|
||||
let world = TestWorld::new().with_spawn_phase().with_player(player0, 100.0).with_player(player1, 100.0).with_territory(player0, &spawn_territory0).with_territory(player1, &spawn_territory1).build();
|
||||
|
||||
let mut assertions = world.assert();
|
||||
for tile in &spawn_territory0 {
|
||||
assertions = assertions.player_owns(*tile, player0);
|
||||
}
|
||||
for tile in &spawn_territory1 {
|
||||
assertions = assertions.player_owns(*tile, player1);
|
||||
}
|
||||
|
||||
assert!(world.resource::<SpawnPhase>().active);
|
||||
}
|
||||
106
crates/borders-core/tests/territory_tests.rs
Normal file
106
crates/borders-core/tests/territory_tests.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
// Territory manager tests: change tracking, ownership, border detection
|
||||
|
||||
mod common;
|
||||
|
||||
use assert2::assert;
|
||||
use borders_core::prelude::*;
|
||||
use common::{TestWorld, WorldAssertExt, WorldTestExt};
|
||||
|
||||
#[test]
|
||||
fn test_conquer_tile_adds_to_changes() {
|
||||
let mut world = TestWorld::new().build();
|
||||
let mut territory_manager = world.resource_mut::<TerritoryManager>();
|
||||
|
||||
// Initially no changes
|
||||
assert!(!territory_manager.has_changes());
|
||||
|
||||
// Conquer a tile
|
||||
let tile = U16Vec2::new(50, 50);
|
||||
territory_manager.conquer(tile, NationId::ZERO);
|
||||
|
||||
assert!(territory_manager.has_changes(), "Conquering a tile should record a change in the ChangeBuffer");
|
||||
|
||||
// The change should be the tile we conquered
|
||||
let changes: Vec<_> = territory_manager.iter_changes().collect();
|
||||
assert!(changes.len() == 1, "Should have exactly 1 change, but found {}", changes.len());
|
||||
assert!(changes[0] == tile, "Change should be the conquered tile {:?}, but found {:?}", tile, changes[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_iter_changes_preserves_buffer() {
|
||||
let mut world = TestWorld::new().build();
|
||||
let mut territory_manager = world.resource_mut::<TerritoryManager>();
|
||||
|
||||
// Conquer a tile
|
||||
territory_manager.conquer(U16Vec2::new(50, 50), NationId::ZERO);
|
||||
assert!(territory_manager.has_changes());
|
||||
|
||||
let _changes: Vec<_> = territory_manager.iter_changes().collect();
|
||||
|
||||
assert!(territory_manager.has_changes(), "iter_changes should not clear the ChangeBuffer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_drain_changes_clears_buffer() {
|
||||
let mut world = TestWorld::new().build();
|
||||
let mut territory_manager = world.resource_mut::<TerritoryManager>();
|
||||
|
||||
// Conquer tiles
|
||||
territory_manager.conquer(U16Vec2::new(50, 50), NationId::ZERO);
|
||||
territory_manager.conquer(U16Vec2::new(51, 50), NationId::ZERO);
|
||||
assert!(territory_manager.has_changes());
|
||||
|
||||
// Drain changes
|
||||
let changes: Vec<_> = territory_manager.drain_changes().collect();
|
||||
assert!(changes.len() == 2, "Should have drained 2 changes, but found {}", changes.len());
|
||||
|
||||
assert!(!territory_manager.has_changes(), "drain_changes should clear the ChangeBuffer");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_border_detection_map_edges() {
|
||||
let mut world = TestWorld::new().with_map_size(100, 100).with_player(NationId::ZERO, 100.0).build();
|
||||
|
||||
let corner = U16Vec2::new(0, 0);
|
||||
world.conquer_tile(corner, NationId::ZERO);
|
||||
|
||||
world.assert().is_border(corner);
|
||||
|
||||
let edge = U16Vec2::new(0, 50);
|
||||
world.conquer_tile(edge, NationId::ZERO);
|
||||
|
||||
world.assert().is_border(edge);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_border_detection_interior_tiles() {
|
||||
let mut world = TestWorld::new().with_map_size(100, 100).with_player(NationId::ZERO, 100.0).build();
|
||||
|
||||
let center = U16Vec2::new(50, 50);
|
||||
for dy in -1..=1 {
|
||||
for dx in -1..=1 {
|
||||
let tile = U16Vec2::new((center.x as i32 + dx) as u16, (center.y as i32 + dy) as u16);
|
||||
world.conquer_tile(tile, NationId::ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
world.assert().not_border(center).is_border(U16Vec2::new(49, 50)).is_border(U16Vec2::new(51, 50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_tile_records_change() {
|
||||
let mut world = TestWorld::new().build();
|
||||
let mut territory_manager = world.resource_mut::<TerritoryManager>();
|
||||
|
||||
// Conquer then clear changes
|
||||
let tile = U16Vec2::new(50, 50);
|
||||
territory_manager.conquer(tile, NationId::ZERO);
|
||||
territory_manager.clear_changes();
|
||||
assert!(!territory_manager.has_changes());
|
||||
|
||||
// Clear the tile
|
||||
let previous_owner = territory_manager.clear(tile);
|
||||
assert!(previous_owner == Some(NationId::ZERO), "Expected previous owner to be Some(NationId::ZERO), but found {:?}", previous_owner);
|
||||
|
||||
assert!(territory_manager.has_changes(), "Clearing a tile should record a change");
|
||||
}
|
||||
Reference in New Issue
Block a user