use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rustsim_core::interaction::{PositionedAgent, SpaceInteraction};
use rustsim_spaces::{
continuous::{ContinuousPos, ContinuousSpace2D},
continuous3d::ContinuousPos3D,
hybrid::{HybridPos, HybridSpace, ZoneExtent},
link::{LinkGeometry, LinkSpace},
prelude::HybridSpaceError,
};
#[test]
fn continuous_space_periodic_insert_and_move_always_normalize_into_extent() {
let mut rng = StdRng::seed_from_u64(42);
let mut space = ContinuousSpace2D::new(100.0, 50.0, true, 5.0).unwrap();
struct DummyAgent {
id: u64,
pos: ContinuousPos,
}
impl rustsim_core::agent::Agent for DummyAgent {
fn id(&self) -> u64 {
self.id
}
}
impl PositionedAgent for DummyAgent {
type Position = ContinuousPos;
fn position(&self) -> &Self::Position {
&self.pos
}
fn set_position(&mut self, pos: Self::Position) {
self.pos = pos;
}
}
for id in 1..=200u64 {
let pos = ContinuousPos::new(rng.gen_range(-500.0..500.0), rng.gen_range(-500.0..500.0));
let agent = DummyAgent { id, pos };
space.add_agent(&agent).unwrap();
let stored = *space.agent_position(id).unwrap();
assert!((0.0..100.0).contains(&stored.x));
assert!((0.0..50.0).contains(&stored.y));
let moved = space
.move_agent_pos(
id,
ContinuousPos::new(rng.gen_range(-500.0..500.0), rng.gen_range(-500.0..500.0)),
)
.unwrap();
assert!((0.0..100.0).contains(&moved.x));
assert!((0.0..50.0).contains(&moved.y));
}
}
#[test]
fn link_space_total_agents_matches_sum_of_link_populations_under_random_operations() {
let mut rng = StdRng::seed_from_u64(7);
let mut space: LinkSpace<()> = LinkSpace::new();
for _ in 0..5 {
space.add_node();
}
for i in 0..4 {
space
.add_link(i, i + 1, LinkGeometry::new(100.0).unwrap(), ())
.unwrap();
}
for id in 1..=50u64 {
let link = rng.gen_range(0..4usize);
let pos = rng.gen_range(0.0..100.0);
space.add_agent_to_link(id, link, pos).unwrap();
}
for _ in 0..500 {
let id = rng.gen_range(1..=50u64);
match rng.gen_range(0..3) {
0 => {
let _ = space.advance_agent(id, rng.gen_range(0.0..25.0));
}
1 => {
if let Some((link, _)) = space.agent_position(id) {
if link + 1 < space.num_links() && space.agent_at_link_end(id) {
let _ = space.transfer_agent(id, link + 1);
}
}
}
_ => {
if let Some((link, _)) = space.agent_position(id) {
space.set_link_exit_blocked(link, rng.gen_bool(0.5));
}
}
}
let sum: usize = (0..space.num_links())
.map(|link| space.agents_on_link(link))
.sum();
assert_eq!(space.total_agents(), sum);
for link in 0..space.num_links() {
let ids = space.agent_ids_on_link(link);
for &aid in ids {
let (actual_link, pos) = space.agent_position(aid).unwrap();
assert_eq!(actual_link, link);
assert!((0.0..=100.0).contains(&pos));
}
let positions: Vec<f64> = ids
.iter()
.map(|&aid| space.agent_position(aid).unwrap().1)
.collect();
assert!(positions.windows(2).all(|w| w[0] <= w[1]));
}
}
}
#[test]
fn hybrid_space_zone_assignments_remain_consistent_under_random_moves() {
let mut rng = StdRng::seed_from_u64(123);
let extent = ZoneExtent::cube(20.0).unwrap();
let mut space = HybridSpace::uniform(4, extent);
for i in 0..3 {
assert!(space.add_edge(i, i + 1));
}
for id in 1..=40u64 {
let zone = rng.gen_range(0..4usize);
space
.add_agent_internal(
id,
HybridPos::new(zone, ContinuousPos3D::new(1.0, 1.0, 1.0)),
)
.unwrap();
}
for _ in 0..400 {
let id = rng.gen_range(1..=40u64);
let current = *space.agent_position(id).unwrap();
let dx = rng.gen_range(-10.0..10.0);
let dy = rng.gen_range(-10.0..10.0);
let dz = rng.gen_range(-10.0..10.0);
let moved = space
.move_within_zone(id, ContinuousPos3D::new(dx, dy, dz))
.unwrap();
assert_eq!(moved.node, current.node);
assert!((0.0..20.0).contains(&moved.offset.x));
assert!((0.0..20.0).contains(&moved.offset.y));
assert!((0.0..20.0).contains(&moved.offset.z));
let updated = *space.agent_position(id).unwrap();
if rng.gen_bool(0.15) && updated.node + 1 < 4 {
let moved = space
.transition_to_zone(
id,
updated.node + 1,
ContinuousPos3D::new(50.0, -5.0, 100.0),
)
.unwrap();
assert_eq!(moved.node, updated.node + 1);
assert!((0.0..20.0).contains(&moved.offset.x));
assert!((0.0..20.0).contains(&moved.offset.y));
assert!((0.0..20.0).contains(&moved.offset.z));
}
}
for id in 1..=40u64 {
let pos = space.agent_position(id).unwrap();
assert!(pos.node < 4);
let occupants = space.agents_at_node(pos.node);
assert!(occupants.contains(&id));
}
}
#[test]
fn hybrid_space_rejects_non_adjacent_cross_zone_moves() {
let extent = ZoneExtent::cube(10.0).unwrap();
let mut space = HybridSpace::uniform(3, extent);
assert!(space.add_edge(0, 1));
space
.add_agent_internal(1, HybridPos::new(0, ContinuousPos3D::new(1.0, 1.0, 1.0)))
.unwrap();
let err = space
.transition_to_zone(1, 2, ContinuousPos3D::new(1.0, 1.0, 1.0))
.unwrap_err();
assert!(matches!(err, HybridSpaceError::NoEdge { .. }));
}