Files
settled-reach/server/tests/shadowcast_bench.rs
T
jpmschweitzerandClaude Opus 4.7 8eb6c47f74 style(server): clear clippy-1.93 cfg(test)/test-target debt (#967)
Manual clippy-1.93 fixes that the prior machine-applicable sweep couldn't auto-
apply, all in cfg(test) modules and tests/ targets (invisible to the lib-only
pre-push clippy, hence accumulated unflagged):

- disallowed_types HashSet/HashMap → BTreeSet/BTreeMap (determinism rule):
  shadowcast_bench.rs (×8, (i32,i32) keys), mood.rs, sound.rs. SoundEventKind
  gains a PartialOrd/Ord derive (fieldless Copy enum) so it is BTree-usable.
- field_reassign_with_default → struct-init: disclosure.rs, monologue.rs (×2),
  save_io.rs (keeps `mut` for the deliberate last-write-wins overwrite).
- assertions_on_constants on the EAVESDROP_THRESHOLD invariant → compile-time
  `const _: () = assert!(...)`: listening.rs, cross_room_transitions.rs. This is
  stronger than the runtime assert and needs no #[allow].
- approx_constant: settings/types.rs round-trip literal 3.14 → 2.5 (the value is
  arbitrary test data, never meant to be PI — change avoids both the lint and a
  suppression).
- drop_non_drop: vision.rs early Mut<WalkabilityMap> release → scoped block.
- unnecessary_get_then_check → contains_key: information_boundaries.rs (×3).
- cloned_ref_to_slice_refs → std::slice::from_ref: triangle_validation.rs.
- unused_must_use: input.rs dropped the unused .id() on a spawn.

cargo clippy --all-targets -- -D warnings is clean; cargo test green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 11:03:15 +02:00

288 lines
8.7 KiB
Rust

//! Shadowcasting algorithm benchmarks
//!
//! Compares performance of symmetric vs recursive shadowcasting
//! Run with: cargo test --test shadowcast_bench -- --ignored --nocapture
use rand::Rng;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use settled_reach_server::perception::shadowcast::{recursive_shadowcast, symmetric_shadowcast};
use std::collections::BTreeSet;
use std::time::Instant;
/// Configuration for a benchmark run
struct BenchConfig {
map_size: i32,
wall_density: f64, // 0.0 to 1.0
vision_range: i32,
iterations: usize,
seed: u64,
}
/// Generate a random wall map with specified density
fn generate_wall_map(size: i32, density: f64, seed: u64) -> BTreeSet<(i32, i32)> {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut walls = BTreeSet::new();
for x in 0..size {
for y in 0..size {
if rng.random::<f64>() < density {
walls.insert((x, y));
}
}
}
walls
}
/// Run benchmark for a single configuration
fn bench_config(config: &BenchConfig) -> BenchResults {
let walls = generate_wall_map(config.map_size, config.wall_density, config.seed);
let is_opaque = |x: i32, y: i32| walls.contains(&(x, y));
// Pick random origin points (deterministic from same seed)
let mut rng = ChaCha8Rng::seed_from_u64(config.seed + 1000);
let origins: Vec<(i32, i32)> = (0..config.iterations)
.map(|_| {
let x = rng.random_range(0..config.map_size);
let y = rng.random_range(0..config.map_size);
(x, y)
})
.collect();
// Benchmark symmetric shadowcasting
let start = Instant::now();
let mut symmetric_total_tiles = 0;
for &(x, y) in &origins {
let visible = symmetric_shadowcast(&is_opaque, x, y, config.vision_range);
symmetric_total_tiles += visible.len();
}
let symmetric_duration = start.elapsed();
// Benchmark recursive shadowcasting
let start = Instant::now();
let mut recursive_total_tiles = 0;
for &(x, y) in &origins {
let visible = recursive_shadowcast(&is_opaque, x, y, config.vision_range);
recursive_total_tiles += visible.len();
}
let recursive_duration = start.elapsed();
BenchResults {
symmetric_ms: symmetric_duration.as_secs_f64() * 1000.0,
recursive_ms: recursive_duration.as_secs_f64() * 1000.0,
symmetric_avg_tiles: symmetric_total_tiles as f64 / config.iterations as f64,
recursive_avg_tiles: recursive_total_tiles as f64 / config.iterations as f64,
}
}
struct BenchResults {
symmetric_ms: f64,
recursive_ms: f64,
symmetric_avg_tiles: f64,
recursive_avg_tiles: f64,
}
#[test]
#[ignore]
fn benchmark_symmetric_vs_recursive() {
println!("\n=== Shadowcasting Algorithm Benchmark ===\n");
println!("Comparing Symmetric (Albert Ford) vs Traditional Recursive\n");
let configs = vec![
// 32x32 maps
BenchConfig {
map_size: 32,
wall_density: 0.0,
vision_range: 20,
iterations: 1000,
seed: 42,
},
BenchConfig {
map_size: 32,
wall_density: 0.1,
vision_range: 20,
iterations: 1000,
seed: 42,
},
BenchConfig {
map_size: 32,
wall_density: 0.3,
vision_range: 20,
iterations: 1000,
seed: 42,
},
// 64x64 maps
BenchConfig {
map_size: 64,
wall_density: 0.0,
vision_range: 20,
iterations: 1000,
seed: 42,
},
BenchConfig {
map_size: 64,
wall_density: 0.1,
vision_range: 20,
iterations: 1000,
seed: 42,
},
BenchConfig {
map_size: 64,
wall_density: 0.3,
vision_range: 20,
iterations: 1000,
seed: 42,
},
// 150x150 maps
BenchConfig {
map_size: 150,
wall_density: 0.0,
vision_range: 20,
iterations: 1000,
seed: 42,
},
BenchConfig {
map_size: 150,
wall_density: 0.1,
vision_range: 20,
iterations: 1000,
seed: 42,
},
BenchConfig {
map_size: 150,
wall_density: 0.3,
vision_range: 20,
iterations: 1000,
seed: 42,
},
];
for config in configs {
let density_str = match (config.wall_density * 100.0) as i32 {
0 => "open field",
10 => "moderate corridors",
30 => "dense rooms",
d => &format!("{}% walls", d),
};
println!(
"Map: {}x{}, Density: {}, Range: {}, Iterations: {}",
config.map_size, config.map_size, density_str, config.vision_range, config.iterations
);
let results = bench_config(&config);
println!(
" Symmetric: {:.2}ms total, {:.2}µs/call, {:.1} tiles avg",
results.symmetric_ms,
results.symmetric_ms * 1000.0 / config.iterations as f64,
results.symmetric_avg_tiles
);
println!(
" Recursive: {:.2}ms total, {:.2}µs/call, {:.1} tiles avg",
results.recursive_ms,
results.recursive_ms * 1000.0 / config.iterations as f64,
results.recursive_avg_tiles
);
let speedup = results.recursive_ms / results.symmetric_ms;
let comparison = if speedup > 1.0 {
format!("Symmetric is {:.2}x faster", speedup)
} else {
format!("Recursive is {:.2}x faster", 1.0 / speedup)
};
println!(" → {}\n", comparison);
}
}
#[test]
fn symmetric_algorithm_is_symmetric() {
// Verify that if A sees B, then B sees A (symmetric property)
// NOTE: Testing a subset of cases due to edge-case complexity in full grid testing
println!("\n=== Testing Symmetric Property (simplified) ===\n");
// Simple open field test - perfect symmetry should hold here
let no_walls: BTreeSet<(i32, i32)> = BTreeSet::new();
let is_opaque = |x: i32, y: i32| no_walls.contains(&(x, y));
let test_positions = vec![(0, 0), (3, 3), (5, 2), (1, 7)];
let range = 8;
let mut failures = 0;
for &(ax, ay) in &test_positions {
let a_visible = symmetric_shadowcast(&is_opaque, ax, ay, range);
for &(bx, by) in &test_positions {
if ax == bx && ay == by {
continue; // Skip self
}
let b_visible = symmetric_shadowcast(&is_opaque, bx, by, range);
// If A sees B, then B should see A
if a_visible.contains(&(bx, by)) && !b_visible.contains(&(ax, ay)) {
println!(
"SYMMETRY VIOLATION: ({}, {}) sees ({}, {}) but not vice versa",
ax, ay, bx, by
);
failures += 1;
}
}
}
if failures == 0 {
println!("✓ Symmetry verified for test cases\n");
} else {
println!("✗ Found {} symmetry violations\n", failures);
}
assert_eq!(failures, 0, "Symmetry property violated");
}
#[test]
fn both_algorithms_agree_on_basic_cases() {
// Verify both algorithms produce similar results on basic scenarios
println!("\n=== Comparing Algorithm Results ===\n");
let test_cases = vec![
("Open field", BTreeSet::new()),
("Single wall at (2,0)", {
let mut w = BTreeSet::new();
w.insert((2, 0));
w
}),
("L-shaped corridor", {
let mut w = BTreeSet::new();
for i in 0..5 {
w.insert((i, 2));
w.insert((2, i));
}
w
}),
];
for (name, walls) in test_cases {
let is_opaque = |x: i32, y: i32| walls.contains(&(x, y));
let origin = (0, 0);
let range = 10;
let symmetric = symmetric_shadowcast(&is_opaque, origin.0, origin.1, range);
let recursive = recursive_shadowcast(&is_opaque, origin.0, origin.1, range);
println!("Test case: {}", name);
println!(" Symmetric: {} tiles visible", symmetric.len());
println!(" Recursive: {} tiles visible", recursive.len());
// They may not match exactly due to algorithmic differences, but should be close
let diff = (symmetric.len() as i32 - recursive.len() as i32).abs();
let max_allowed_diff = (symmetric.len() as f64 * 0.1).ceil() as i32; // 10% tolerance
if diff <= max_allowed_diff {
println!(" ✓ Results within tolerance (diff: {})\n", diff);
} else {
println!(" ⚠ Large difference (diff: {})\n", diff);
}
}
}