Warnings fixed:
- contraband.rs: scan event now always emits even when NPC already
knows (was skipped by early `continue`). Contract matches doc.
- test_world/mod.rs: ScanEventBuffer added to player spawn bundle
so check_contraband_scan doesn't silently no-op in gauntlet mode.
- npc/mod.rs → simulation/mod.rs: moved check_contraband_scan
registration to SimulationPlugin (operates on player inventory and
snapshot pipeline, consistent with process_talk_interaction).
Suggestions addressed:
- cross_room_transitions.rs T1: clarified standalone position vs
constants.rs observer position in comment.
- dialogue.rs: Vec<&str> dedup replaced with BTreeSet<&str> for
deterministic iteration (project convention).
- contraband.rs: added test for multiple simultaneous ScanAuthority
NPCs in range (564 tests total).
- dialogue.rs: doc-comment on relationship_to_trust explaining
KnowledgeConfidence ordering and Suspects default.
- cross_room_transitions.rs T5: noted direct KG API usage vs full
perception system.
- sprint_gauntlet.rs: documented intentional Contentment { level: 0 }.
- content_scaling.rs: noted GAUNTLET_NPC_COUNT is manually maintained.
- contraband.rs: doc-comment on cross-plugin registration rationale.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
524 lines
18 KiB
Rust
524 lines
18 KiB
Rust
//! Content scaling test (#513, D-026).
|
|
//!
|
|
//! Verifies that adding extra NPCs doesn't degrade tick timing beyond
|
|
//! acceptable bounds. Runs the Gauntlet baseline, then adds additional
|
|
//! NPCs and compares:
|
|
//! 1. Tick timing stays within D-026 budget (100ms)
|
|
//! 2. Baseline entities still behave identically (deterministic)
|
|
//!
|
|
//! Sprint 11 adds two new tests (#513 deliverable):
|
|
//! - max_npc_pack_tick_budget: 80 NPCs (D-026 Active tier ceiling), 100 ticks,
|
|
//! per-tick budget assertion (every tick < 100ms, not just average).
|
|
//! - max_npc_pack_behavioral_regression: verifies that adding 46 extra NPCs to
|
|
//! hit the Active tier ceiling doesn't change original entity behavior at tick 100.
|
|
//!
|
|
//! Run with: cargo test --test content_scaling -- --nocapture
|
|
|
|
use bevy_app::prelude::*;
|
|
use std::collections::BTreeMap;
|
|
use std::time::Instant;
|
|
|
|
use settled_reach_server::bridge::types::*;
|
|
use settled_reach_server::bridge::BridgePlugin;
|
|
use settled_reach_server::knowledge::registry::{EntityRegistry, StableEntityId};
|
|
use settled_reach_server::knowledge::{KnowledgeConfidence, KnowledgeGraph, KnowledgePlugin, StableId};
|
|
use settled_reach_server::npc::{Contentment, Npc, NpcPlugin, ToleranceThreshold, Want, WantKind};
|
|
use settled_reach_server::simulation::interaction::Interactable;
|
|
use settled_reach_server::simulation::movement::TilePosition;
|
|
use settled_reach_server::simulation::path_follow::MovementSpeed;
|
|
use settled_reach_server::simulation::SimulationPlugin;
|
|
|
|
/// Number of ticks to run for timing measurements.
|
|
const TIMING_TICKS: usize = 50;
|
|
|
|
/// D-026 budget: 100ms per tick maximum.
|
|
const MAX_TICK_MS: f64 = 100.0;
|
|
|
|
/// Extra NPC counts for scaling tiers.
|
|
const EXTRA_NPC_COUNTS: &[usize] = &[0, 15, 50];
|
|
|
|
/// D-026 Active tier ceiling: maximum NPCs in full simulation.
|
|
const ACTIVE_TIER_NPC_CEILING: usize = 80;
|
|
|
|
/// Ticks for the full stress test (#513 spec: 100 ticks, 80 NPCs).
|
|
const STRESS_TICKS: usize = 100;
|
|
|
|
/// Known NPC count in the full Gauntlet world (all rooms, Sprint 11 included).
|
|
/// Fog Theater: 4, Occlusion Corridor: 4, Inventory Warehouse: 1, Pause Chamber: 1,
|
|
/// Dialogue Room: 4, Crowd Plaza: 15, Sprint Gauntlet: 1, Eavesdrop Alcove: 2,
|
|
/// Confrontation Stage: 2 = 34 total.
|
|
///
|
|
/// Manually maintained — update when rooms are added/changed. Future: derive
|
|
/// from StableId ranges in constants.rs to avoid manual sync.
|
|
const GAUNTLET_NPC_COUNT: usize = 34;
|
|
|
|
/// Extra NPCs to spawn on top of the Gauntlet baseline to reach Active tier ceiling.
|
|
const STRESS_EXTRA_NPCS: usize = ACTIVE_TIER_NPC_CEILING - GAUNTLET_NPC_COUNT;
|
|
|
|
/// Set up a Gauntlet world and return the app.
|
|
fn setup_baseline() -> App {
|
|
let mut app = App::new();
|
|
app.add_plugins(SimulationPlugin);
|
|
app.add_plugins(BridgePlugin);
|
|
app.add_plugins(KnowledgePlugin);
|
|
app.add_plugins(NpcPlugin);
|
|
|
|
#[cfg(feature = "gauntlet")]
|
|
settled_reach_server::test_world::setup_gauntlet(&mut app);
|
|
|
|
app
|
|
}
|
|
|
|
/// Spawn N extra NPCs spread across the Gauntlet hub area.
|
|
/// NPCs are placed in a grid starting at (40, 48) to stay within walkable space.
|
|
fn spawn_extra_npcs(app: &mut App, count: usize) {
|
|
// Remove registry from world so we can mutate it while also spawning entities.
|
|
let mut registry = app
|
|
.world_mut()
|
|
.remove_resource::<EntityRegistry>()
|
|
.expect("EntityRegistry should exist after setup_gauntlet");
|
|
let cols = 10;
|
|
|
|
for i in 0..count {
|
|
let x = 40 + (i % cols) as i32;
|
|
let y = 48 + (i / cols) as i32;
|
|
let pos = TilePosition::new(x, y, 0);
|
|
|
|
let entity = app
|
|
.world_mut()
|
|
.spawn((
|
|
Npc,
|
|
Interactable,
|
|
pos,
|
|
Want {
|
|
primary: WantKind::Safety,
|
|
intensity: 5,
|
|
description: format!("extra_npc_{}", i),
|
|
},
|
|
Contentment { level: 0 },
|
|
ToleranceThreshold {
|
|
current_stress: 0,
|
|
threshold: 50,
|
|
},
|
|
MovementSpeed::default(),
|
|
))
|
|
.id();
|
|
let sid = registry.register(entity);
|
|
app.world_mut()
|
|
.entity_mut(entity)
|
|
.insert(StableEntityId(sid));
|
|
}
|
|
|
|
app.insert_resource(registry);
|
|
}
|
|
|
|
/// Tick the app N times and return average milliseconds per tick.
|
|
fn measure_tick_timing(app: &mut App, ticks: usize) -> f64 {
|
|
// Warm-up tick (first tick has startup overhead)
|
|
app.update();
|
|
|
|
let start = Instant::now();
|
|
for _ in 0..ticks {
|
|
app.update();
|
|
}
|
|
let elapsed = start.elapsed();
|
|
elapsed.as_secs_f64() * 1000.0 / ticks as f64
|
|
}
|
|
|
|
/// Collect snapshot entity IDs from the VisibilityGeometry and entity count.
|
|
fn count_entities(app: &App) -> usize {
|
|
let registry = app.world().resource::<EntityRegistry>();
|
|
registry.len() as usize
|
|
}
|
|
|
|
/// Collect the player's KnowledgeGraph confidence levels for all Gauntlet entities
|
|
/// (StableIds 0..=max_id). Used to detect KG-level behavioral regression.
|
|
#[cfg(feature = "gauntlet")]
|
|
fn player_kg_snapshot(app: &App, max_id: u64) -> BTreeMap<u64, KnowledgeConfidence> {
|
|
let registry = app.world().resource::<EntityRegistry>();
|
|
let player_entity = registry
|
|
.to_entity(&StableId(0))
|
|
.expect("player entity at StableId 0");
|
|
match app.world().get::<KnowledgeGraph>(player_entity) {
|
|
Some(kg) => kg
|
|
.entities
|
|
.iter()
|
|
.filter(|(id, _)| id.0 <= max_id)
|
|
.map(|(id, entry)| (id.0, entry.confidence))
|
|
.collect(),
|
|
None => BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
/// Baseline tick timing: Gauntlet with default entities stays within D-026 budget.
|
|
#[test]
|
|
#[cfg(feature = "gauntlet")]
|
|
fn baseline_tick_timing_within_budget() {
|
|
let mut app = setup_baseline();
|
|
let entity_count = count_entities(&app);
|
|
let avg_ms = measure_tick_timing(&mut app, TIMING_TICKS);
|
|
|
|
eprintln!(
|
|
"Baseline: {} entities, avg {:.3}ms/tick over {} ticks",
|
|
entity_count, avg_ms, TIMING_TICKS
|
|
);
|
|
|
|
assert!(
|
|
avg_ms < MAX_TICK_MS,
|
|
"Baseline tick timing ({:.3}ms) exceeds D-026 budget ({}ms)",
|
|
avg_ms,
|
|
MAX_TICK_MS
|
|
);
|
|
}
|
|
|
|
/// Scaling test: adding NPCs keeps tick timing within D-026 budget.
|
|
/// Tests 0 (baseline), 15, and 50 extra NPCs.
|
|
#[test]
|
|
#[cfg(feature = "gauntlet")]
|
|
fn scaling_tick_timing_within_budget() {
|
|
let mut results: Vec<(usize, usize, f64)> = Vec::new();
|
|
|
|
for &extra_count in EXTRA_NPC_COUNTS {
|
|
let mut app = setup_baseline();
|
|
if extra_count > 0 {
|
|
spawn_extra_npcs(&mut app, extra_count);
|
|
}
|
|
let total_entities = count_entities(&app);
|
|
let avg_ms = measure_tick_timing(&mut app, TIMING_TICKS);
|
|
results.push((extra_count, total_entities, avg_ms));
|
|
}
|
|
|
|
eprintln!(
|
|
"\n=== Content Scaling Results (D-026: {}ms budget) ===",
|
|
MAX_TICK_MS
|
|
);
|
|
eprintln!("{:<12} {:<10} {:<15}", "Extra NPCs", "Total", "Avg ms/tick");
|
|
eprintln!("{:-<37}", "");
|
|
for &(extra, total, avg_ms) in &results {
|
|
let status = if avg_ms < MAX_TICK_MS { "OK" } else { "OVER" };
|
|
eprintln!("{:<12} {:<10} {:<15.3} {}", extra, total, avg_ms, status);
|
|
}
|
|
|
|
// Assert all tiers stay within budget
|
|
for &(extra, _total, avg_ms) in &results {
|
|
assert!(
|
|
avg_ms < MAX_TICK_MS,
|
|
"Tick timing with +{} NPCs ({:.3}ms) exceeds D-026 budget ({}ms)",
|
|
extra,
|
|
avg_ms,
|
|
MAX_TICK_MS
|
|
);
|
|
}
|
|
|
|
// Assert scaling is reasonable: +50 NPCs shouldn't more than 5x the baseline
|
|
if results.len() >= 2 {
|
|
let baseline_ms = results[0].2;
|
|
let max_extra_ms = results.last().unwrap().2;
|
|
let scaling_factor = max_extra_ms / baseline_ms;
|
|
eprintln!(
|
|
"\nScaling factor (baseline → +{} NPCs): {:.2}x",
|
|
results.last().unwrap().0,
|
|
scaling_factor
|
|
);
|
|
assert!(
|
|
scaling_factor < 5.0,
|
|
"Scaling factor {:.2}x exceeds 5x threshold — possible O(n^2) regression",
|
|
scaling_factor
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Determinism test: baseline entities produce identical snapshots regardless
|
|
/// of extra NPCs being present. The original Gauntlet entities (StableId 0
|
|
/// through RESET_PLATE_STABLE_IDS.1) should have the same positions and
|
|
/// visibility after the same number of ticks.
|
|
#[test]
|
|
#[cfg(feature = "gauntlet")]
|
|
fn extra_npcs_dont_affect_baseline_behavior() {
|
|
// Run baseline
|
|
let mut baseline_app = setup_baseline();
|
|
for _ in 0..10 {
|
|
baseline_app.update();
|
|
}
|
|
let baseline_buffer = baseline_app
|
|
.world()
|
|
.resource::<SnapshotBuffer>()
|
|
.snapshot
|
|
.clone();
|
|
|
|
// Run with extra NPCs
|
|
let mut scaled_app = setup_baseline();
|
|
spawn_extra_npcs(&mut scaled_app, 15);
|
|
for _ in 0..10 {
|
|
scaled_app.update();
|
|
}
|
|
let scaled_buffer = scaled_app
|
|
.world()
|
|
.resource::<SnapshotBuffer>()
|
|
.snapshot
|
|
.clone();
|
|
|
|
let baseline_snap = baseline_buffer.expect("baseline should produce a snapshot");
|
|
let scaled_snap = scaled_buffer.expect("scaled should produce a snapshot");
|
|
|
|
// Same tick
|
|
assert_eq!(
|
|
baseline_snap.tick, scaled_snap.tick,
|
|
"tick count should match"
|
|
);
|
|
|
|
// Same game time
|
|
assert_eq!(
|
|
baseline_snap.game_time.time_of_day, scaled_snap.game_time.time_of_day,
|
|
"game time should match"
|
|
);
|
|
|
|
// Player position should be identical
|
|
let baseline_player = baseline_snap
|
|
.entities
|
|
.iter()
|
|
.find(|e| e.kind == EntityKind::Player);
|
|
let scaled_player = scaled_snap
|
|
.entities
|
|
.iter()
|
|
.find(|e| e.kind == EntityKind::Player);
|
|
assert!(baseline_player.is_some(), "baseline should have player");
|
|
assert!(scaled_player.is_some(), "scaled should have player");
|
|
|
|
let bp = baseline_player.unwrap();
|
|
let sp = scaled_player.unwrap();
|
|
assert_eq!(bp.x, sp.x, "player x should match");
|
|
assert_eq!(bp.y, sp.y, "player y should match");
|
|
|
|
// Original entities (entity_id <= max Gauntlet StableId) visible in baseline
|
|
// should still be visible in scaled run. Extra NPCs may add to the visible
|
|
// set, but shouldn't remove baseline visibility.
|
|
let max_baseline_id = settled_reach_server::test_world::constants::RESET_PLATE_STABLE_IDS.1;
|
|
let baseline_original_ids: Vec<u64> = baseline_snap
|
|
.entities
|
|
.iter()
|
|
.filter(|e| e.entity_id <= max_baseline_id)
|
|
.map(|e| e.entity_id)
|
|
.collect();
|
|
|
|
let scaled_original_ids: Vec<u64> = scaled_snap
|
|
.entities
|
|
.iter()
|
|
.filter(|e| e.entity_id <= max_baseline_id)
|
|
.map(|e| e.entity_id)
|
|
.collect();
|
|
|
|
assert_eq!(
|
|
baseline_original_ids, scaled_original_ids,
|
|
"Original Gauntlet entities (id <= max_baseline_id) should be identical in both runs"
|
|
);
|
|
}
|
|
|
|
// =============================================================================
|
|
// Sprint 11 / #513 — Max-NPC Pack Stress Tests
|
|
// =============================================================================
|
|
|
|
/// Stress test: Active tier ceiling (80 NPCs), 100 ticks, per-tick budget check.
|
|
///
|
|
/// Spawns the full Gauntlet baseline ({GAUNTLET_NPC_COUNT} NPCs) plus
|
|
/// {STRESS_EXTRA_NPCS} extra NPCs to reach the D-026 Active tier ceiling (80).
|
|
/// Runs {STRESS_TICKS} ticks and asserts that EVERY individual tick (not just
|
|
/// the average) completes within the 100ms D-026 budget.
|
|
///
|
|
/// Outputs a PERF_RESULT JSON line compatible with the perf-baseline tooling
|
|
/// (same format as tests/perf/baseline.json) so CI can compare against the
|
|
/// stored baseline.
|
|
#[test]
|
|
#[cfg(feature = "gauntlet")]
|
|
fn max_npc_pack_tick_budget() {
|
|
let mut app = setup_baseline();
|
|
spawn_extra_npcs(&mut app, STRESS_EXTRA_NPCS);
|
|
let total_entities = count_entities(&app);
|
|
|
|
// Warm-up: first tick has bevy startup overhead.
|
|
app.update();
|
|
|
|
// Measure STRESS_TICKS, recording each tick individually.
|
|
let mut per_tick_us: Vec<u64> = Vec::with_capacity(STRESS_TICKS);
|
|
for _ in 0..STRESS_TICKS {
|
|
let start = Instant::now();
|
|
app.update();
|
|
per_tick_us.push(start.elapsed().as_micros() as u64);
|
|
}
|
|
|
|
// --- Statistics ---
|
|
let min_us = *per_tick_us.iter().min().unwrap();
|
|
let max_us = *per_tick_us.iter().max().unwrap();
|
|
let sum: u64 = per_tick_us.iter().sum();
|
|
let mean_us = sum / per_tick_us.len() as u64;
|
|
let mut sorted = per_tick_us.clone();
|
|
sorted.sort_unstable();
|
|
let p95_idx = ((sorted.len() - 1) as f64 * 0.95).floor() as usize;
|
|
let p95_us = sorted[p95_idx.min(sorted.len() - 1)];
|
|
|
|
eprintln!(
|
|
"\n=== Max-NPC Pack Stress Test — D-026 tick budget ({} NPCs, {} ticks) ===",
|
|
ACTIVE_TIER_NPC_CEILING, STRESS_TICKS
|
|
);
|
|
eprintln!(
|
|
"Entities in world: {} (Gauntlet NPCs: {} extra: {})",
|
|
total_entities, GAUNTLET_NPC_COUNT, STRESS_EXTRA_NPCS
|
|
);
|
|
eprintln!(
|
|
"Timing: min={:.3}ms mean={:.3}ms p95={:.3}ms max={:.3}ms budget={}ms",
|
|
min_us as f64 / 1000.0,
|
|
mean_us as f64 / 1000.0,
|
|
p95_us as f64 / 1000.0,
|
|
max_us as f64 / 1000.0,
|
|
MAX_TICK_MS
|
|
);
|
|
|
|
// Emit PERF_RESULT in the same format as tooling/perf-baseline so output
|
|
// can be diffed against tests/perf/baseline.json by CI tooling.
|
|
println!(
|
|
"PERF_RESULT:{}",
|
|
serde_json::json!({
|
|
"test": "max_npc_pack_tick_budget",
|
|
"spec": "D-026",
|
|
"tick_timing": {
|
|
"warmup_ticks": 1,
|
|
"measured_ticks": STRESS_TICKS,
|
|
"min_us": min_us,
|
|
"max_us": max_us,
|
|
"mean_us": mean_us,
|
|
"p95_us": p95_us,
|
|
},
|
|
"entities": {
|
|
"total_in_world": total_entities,
|
|
"active_tier_npcs": ACTIVE_TIER_NPC_CEILING,
|
|
"gauntlet_npcs": GAUNTLET_NPC_COUNT,
|
|
"extra_npcs": STRESS_EXTRA_NPCS,
|
|
},
|
|
})
|
|
);
|
|
|
|
// Core assertion: EVERY tick must be within the D-026 100ms budget.
|
|
// Average-only checks can mask spikes — verify each individual tick.
|
|
let budget_us = (MAX_TICK_MS * 1000.0) as u64;
|
|
let over_budget: Vec<(usize, u64)> = per_tick_us
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, &us)| us > budget_us)
|
|
.map(|(i, &us)| (i, us))
|
|
.collect();
|
|
|
|
assert!(
|
|
over_budget.is_empty(),
|
|
"D-026 tick budget exceeded with {} NPCs: {} of {} ticks over {}ms\n worst: tick {} at {:.3}ms",
|
|
ACTIVE_TIER_NPC_CEILING,
|
|
over_budget.len(),
|
|
STRESS_TICKS,
|
|
MAX_TICK_MS,
|
|
over_budget[0].0,
|
|
over_budget[0].1 as f64 / 1000.0
|
|
);
|
|
}
|
|
|
|
/// Behavioral regression: 80 NPCs must not disturb original entity state at tick 100.
|
|
///
|
|
/// Runs the pure Gauntlet (GAUNTLET_NPC_COUNT NPCs) and the full 80-NPC stress
|
|
/// pack for STRESS_TICKS ticks. Asserts:
|
|
/// 1. Snapshot entity IDs for all Gauntlet entities (StableId 0..=65) are identical.
|
|
/// 2. Player's KnowledgeGraph confidence entries for Gauntlet entity range are identical.
|
|
///
|
|
/// This validates D-010 determinism: extra Active-tier NPCs must not affect the
|
|
/// simulation of original entities via LOS, KG, or ECS phase ordering.
|
|
/// Spec: #513, D-026, D-010.
|
|
#[test]
|
|
#[cfg(feature = "gauntlet")]
|
|
fn max_npc_pack_behavioral_regression() {
|
|
use settled_reach_server::test_world::constants::SPRINT11_RESET_PLATE_STABLE_IDS;
|
|
|
|
// The highest StableId belonging to a Gauntlet entity (Sprint 11 reset plates).
|
|
let max_gauntlet_id = SPRINT11_RESET_PLATE_STABLE_IDS.1;
|
|
|
|
// --- Baseline run: pure Gauntlet, no extra NPCs ---
|
|
let mut baseline_app = setup_baseline();
|
|
for _ in 0..STRESS_TICKS {
|
|
baseline_app.update();
|
|
}
|
|
let baseline_snapshot = baseline_app
|
|
.world()
|
|
.resource::<SnapshotBuffer>()
|
|
.snapshot
|
|
.clone();
|
|
let baseline_kg = player_kg_snapshot(&baseline_app, max_gauntlet_id);
|
|
|
|
// --- Stress run: Gauntlet + extra NPCs to reach 80 NPC Active tier ceiling ---
|
|
let mut stress_app = setup_baseline();
|
|
spawn_extra_npcs(&mut stress_app, STRESS_EXTRA_NPCS);
|
|
for _ in 0..STRESS_TICKS {
|
|
stress_app.update();
|
|
}
|
|
let stress_snapshot = stress_app
|
|
.world()
|
|
.resource::<SnapshotBuffer>()
|
|
.snapshot
|
|
.clone();
|
|
let stress_kg = player_kg_snapshot(&stress_app, max_gauntlet_id);
|
|
|
|
let baseline_snap = baseline_snapshot
|
|
.expect("baseline Gauntlet should produce a snapshot");
|
|
let stress_snap = stress_snapshot
|
|
.expect("80-NPC stress run should produce a snapshot");
|
|
|
|
// Tick index must match (same number of updates).
|
|
assert_eq!(
|
|
baseline_snap.tick, stress_snap.tick,
|
|
"tick count should match between baseline and stress run"
|
|
);
|
|
|
|
// --- 1. Snapshot entity comparison ---
|
|
// Collect and sort entity IDs for original Gauntlet entities only.
|
|
// Extra NPCs (StableId > max_gauntlet_id) are excluded from comparison.
|
|
let mut baseline_ids: Vec<u64> = baseline_snap
|
|
.entities
|
|
.iter()
|
|
.filter(|e| e.entity_id <= max_gauntlet_id)
|
|
.map(|e| e.entity_id)
|
|
.collect();
|
|
let mut stress_ids: Vec<u64> = stress_snap
|
|
.entities
|
|
.iter()
|
|
.filter(|e| e.entity_id <= max_gauntlet_id)
|
|
.map(|e| e.entity_id)
|
|
.collect();
|
|
baseline_ids.sort_unstable();
|
|
stress_ids.sort_unstable();
|
|
|
|
assert_eq!(
|
|
baseline_ids, stress_ids,
|
|
"Gauntlet entity visibility at tick {} must be identical: baseline {} entities vs {} with {} extra NPCs",
|
|
STRESS_TICKS,
|
|
baseline_ids.len(),
|
|
stress_ids.len(),
|
|
STRESS_EXTRA_NPCS
|
|
);
|
|
|
|
// --- 2. Knowledge graph comparison ---
|
|
// Player's KG confidence levels for Gauntlet entities (StableId 0..=max_gauntlet_id)
|
|
// must be identical in both runs. Extra NPCs in the hub may be added to the
|
|
// player's KG (higher StableIds), but must not affect original entity entries.
|
|
assert_eq!(
|
|
baseline_kg, stress_kg,
|
|
"Player KG confidence entries for Gauntlet entities (id <= {}) differ at tick {}\n baseline: {} entries stress: {} entries",
|
|
max_gauntlet_id,
|
|
STRESS_TICKS,
|
|
baseline_kg.len(),
|
|
stress_kg.len()
|
|
);
|
|
|
|
eprintln!(
|
|
"Behavioral regression PASS: {} Gauntlet entities identical at tick {} ({} NPCs vs {} NPCs)",
|
|
baseline_ids.len(),
|
|
STRESS_TICKS,
|
|
GAUNTLET_NPC_COUNT,
|
|
ACTIVE_TIER_NPC_CEILING
|
|
);
|
|
}
|