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>
288 lines
8.7 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|