fix(simulation): address PR #125 review — tick truncation, ordering, perf, protocol test

- Widen EventPort tick methods from u32 to u64 (prevents overflow)
- Add is_identity() guard on hot-path String allocation in modifiers
- Replace Vec::remove(0) with VecDeque::pop_front() in price history
- Add .after(tick_economy_simulation) ordering for debug commands
- Fix stale PROTOCOL_VERSION assertion (20 → 21) in serialization test
- Add D-181 Phase 2 visibility scope comment on serve_econ_state_query
- Eliminate double lookup in rebuild_signals via single-pass extraction
- Track economy seed TODO with backlog ticket reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 14:06:54 +02:00
co-authored by Claude Opus 4.6
parent 77232434f4
commit 1498115aba
8 changed files with 37 additions and 32 deletions
+16 -23
View File
@@ -31,7 +31,7 @@
//! 6. `production_vs_baseline` — supply ÷ initial baseline supply (Private)
//! 7. `official_coverage_ratio` — 1 shadow_intensity (Meta-signal)
use std::collections::BTreeMap;
use std::collections::{BTreeMap, VecDeque};
use bevy_ecs::prelude::*;
use econ_sim::Simulation;
@@ -61,7 +61,7 @@ pub struct EconSimResource {
pub sim: Simulation,
/// Price history for signal 2 (price_trend) computation.
/// Ring-buffer keyed by (system_id, commodity_id) → last TREND_WINDOW prices.
price_history: BTreeMap<(String, String), Vec<f64>>,
price_history: BTreeMap<(String, String), VecDeque<f64>>,
/// Baseline supply from first economy tick for signal 6 (production_vs_baseline).
baseline_supply: BTreeMap<(String, String), f64>,
}
@@ -188,15 +188,17 @@ fn rebuild_signals(
// Snapshot current node states into the price_history and baseline_supply maps,
// then build signals. We need to separate the borrow from the iteration.
let node_snapshots: Vec<(String, Vec<(String, f64, f64)>)> = econ_sim
let node_snapshots: Vec<(String, Vec<(String, f64, f64, f64, f64)>)> = econ_sim
.sim
.nodes
.iter()
.map(|(system_id, node)| {
let commodities: Vec<(String, f64, f64)> = node
let commodities: Vec<(String, f64, f64, f64, f64)> = node
.commodities
.iter()
.map(|(commodity_id, state)| (commodity_id.clone(), state.price, state.supply))
.map(|(commodity_id, state)| {
(commodity_id.clone(), state.price, state.supply, state.stockpile, state.demand)
})
.collect();
(system_id.clone(), commodities)
})
@@ -206,7 +208,7 @@ fn rebuild_signals(
let shadow_intensity = shadow_intensities.get(system_id).copied().unwrap_or(0.0);
let corp_count = corp_counts.get(system_id).copied().unwrap_or(0);
for (commodity_id, price, supply) in commodities {
for (commodity_id, price, supply, stockpile, demand) in commodities {
let key = (system_id.clone(), commodity_id.clone());
// Signal 6 baseline: record first-tick supply
@@ -218,9 +220,9 @@ fn rebuild_signals(
// Signal 2: price trend via ring buffer
let history = econ_sim.price_history.entry(key.clone()).or_default();
history.push(*price);
history.push_back(*price);
if history.len() > TREND_WINDOW {
history.remove(0);
history.pop_front();
}
let price_trend = if history.len() >= 2 {
price - history[0]
@@ -229,23 +231,9 @@ fn rebuild_signals(
};
// Signal 5: stockpile in weeks (7 economy ticks per week approximation)
let stockpile = econ_sim
.sim
.nodes
.get(system_id)
.and_then(|n| n.commodities.get(commodity_id))
.map(|s| s.stockpile)
.unwrap_or(0.0);
let demand = econ_sim
.sim
.nodes
.get(system_id)
.and_then(|n| n.commodities.get(commodity_id))
.map(|s| s.demand)
.unwrap_or(0.0);
let weekly_demand = demand * 7.0; // 7 econ ticks ≈ 1 week
let stockpile_weeks = if weekly_demand > 1e-9 {
stockpile / weekly_demand
*stockpile / weekly_demand
} else {
0.0
};
@@ -297,6 +285,11 @@ pub struct EconQueryBuffer {
/// Runs after `tick_economy_simulation` (signals must be fresh) and before
/// `compute_observer_snapshot` (which consumes the response).
/// No-op when `EconStateResource` is absent or no query is pending.
///
/// **D-181 visibility (Phase 2):** All 7 signals are sent unfiltered.
/// Phase 3 will gate signals 37 behind the D-181 visibility ladder
/// (Observable → Semi-private → Private → Meta) based on the player's
/// information access at the queried node.
pub fn serve_econ_state_query(
mut query_buf: ResMut<EconQueryBuffer>,
econ_state: Option<Res<EconStateResource>>,
+1 -1
View File
@@ -168,7 +168,7 @@ impl Plugin for SimulationPlugin {
app.init_resource::<ticker::TickerPool>();
// Economy simulation (#821, D-031) — loaded once at startup, no-op when DB absent.
// Uses seed 0 for now; will be threaded through StartupMessage world seed (#TODO).
// Uses seed 0 for now; will be threaded through StartupMessage world seed (#826).
if let Some((econ_sim, econ_state)) = economy::try_load_economy(0) {
app.insert_resource(econ_sim).insert_resource(econ_state);
}