use crate::force_3d::build_octree;
use crate::{Simulation, SimulationConfig, Vec3};
#[test]
fn triangle_converges() {
let mut sim = Simulation::new(
3,
SimulationConfig {
seed: 99,
link_strength: 0.8,
link_distance: 30.0,
repulsion_strength: -30.0,
center_strength: 0.0,
velocity_decay: 0.9,
dimensions: 3,
..Default::default()
},
);
sim.set_edges(&[(0, 1, 1.0), (1, 2, 1.0), (2, 0, 1.0)]);
for _ in 0..2000 {
sim.tick(0.02);
}
let positions = sim.positions_3d();
let d01 = (positions[0] - positions[1]).length();
let d12 = (positions[1] - positions[2]).length();
let d20 = (positions[2] - positions[0]).length();
let target = 30.0_f32;
for (label, d) in [("d01", d01), ("d12", d12), ("d20", d20)] {
assert!(
d > target * 0.75 && d < target * 1.25,
"expected triangle side ~30, {label}={d}",
);
}
let mean = (d01 + d12 + d20) / 3.0;
let max_dev = [d01, d12, d20]
.iter()
.map(|d| (d - mean).abs() / mean)
.fold(0.0_f32, f32::max);
assert!(
max_dev < 0.2,
"triangle sides should be within 20% of the mean, got {max_dev} deviation",
);
}
#[test]
fn star_topology_stabilizes_3d() {
let n = 9u32;
let mut sim = Simulation::new(
n,
SimulationConfig {
seed: 1,
link_strength: 0.2,
link_distance: 30.0,
repulsion_strength: -30.0,
center_strength: 0.1,
velocity_decay: 0.5,
dimensions: 3,
..Default::default()
},
);
let edges: Vec<_> = (1..n).map(|i| (0u32, i, 1.0)).collect();
sim.set_edges(&edges);
for _ in 0..1500 {
sim.tick(0.02);
}
let before: Vec<Vec3> = sim.positions_3d().to_vec();
for _ in 0..10 {
sim.tick(0.02);
}
let after = sim.positions_3d();
let max_delta = before
.iter()
.zip(after.iter())
.map(|(a, b)| (*b - *a).length())
.fold(0.0_f32, f32::max);
assert!(
max_delta < 0.5,
"star topology should settle; max delta over 10 ticks = {max_delta}",
);
}
#[test]
fn deterministic_tick_sequence_3d() {
let config = SimulationConfig {
seed: 12345,
dimensions: 3,
..Default::default()
};
let mut a = Simulation::new(50, config);
let mut b = Simulation::new(50, config);
let edges: Vec<_> = (0..49u32).map(|i| (i, i + 1, 1.0)).collect();
a.set_edges(&edges);
b.set_edges(&edges);
for _ in 0..100 {
a.tick(0.02);
b.tick(0.02);
}
for (pa, pb) in a.positions_3d().iter().zip(b.positions_3d().iter()) {
assert_eq!(pa, pb, "deterministic 3D run diverged");
}
let first = a.positions_3d()[0];
assert!(
first.length().is_finite(),
"first position must be finite: {first:?}",
);
}
#[test]
fn octree_handles_empty_input() {
let mut arena = Vec::new();
build_octree(&mut arena, &[], 1.0);
assert!(arena.is_empty());
}
#[test]
fn octree_single_body() {
let mut arena = Vec::new();
let p = Vec3::new(1.0, 2.0, 3.0);
build_octree(&mut arena, &[p], 1.0);
assert_eq!(arena.len(), 1);
assert_eq!(arena[0].mass(), 1.0);
assert_eq!(arena[0].com(), p);
}
#[test]
fn two_body_repulsion_pushes_apart() {
let mut sim = Simulation::new(
2,
SimulationConfig {
seed: 42,
repulsion_strength: -80.0,
link_strength: 0.0,
center_strength: 0.0,
velocity_decay: 0.6,
dimensions: 3,
..Default::default()
},
);
let initial_distance = (sim.positions_3d()[0] - sim.positions_3d()[1]).length();
for _ in 0..500 {
sim.tick(0.02);
}
let final_distance = (sim.positions_3d()[0] - sim.positions_3d()[1]).length();
assert!(
final_distance > initial_distance,
"repulsion should push bodies apart: initial={initial_distance}, final={final_distance}",
);
}