feat(simulation): thread world seed from StartupMessage into economy (#826)

Replaces the hardcoded seed=0 with the seed received in StartupMessage,
threading it through SimulationPlugin -> EconomyPlugin / SimRng. Integration
test fixtures updated for the new SimulationPlugin { seed } signature.
This commit is contained in:
2026-04-14 17:21:53 +02:00
parent 521c682175
commit 920ea0582f
16 changed files with 39 additions and 28 deletions
+4 -3
View File
@@ -147,7 +147,7 @@ fn main() {
let mut app = App::new();
settled_reach_server::tick_phases::TickPhase::configure(&mut app);
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed });
app.add_plugins(BridgePlugin);
app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin);
app.add_plugins(settled_reach_server::npc::NpcPlugin);
@@ -176,7 +176,8 @@ fn main() {
app.insert_resource(BridgeResource::new(bridge));
app.insert_resource(HandshakeState::Complete);
// Override SimRng with the chosen seed (SimulationPlugin defaults to seed 0)
// SimulationPlugin { seed } already inserts SimRng with the correct seed.
// Re-insert here as a defensive override in case plugin ordering shifts.
app.insert_resource(settled_reach_server::simulation::rng::SimRng::new(seed));
// Initialize empty line pool index (populated by generator pipeline in v0.2).
@@ -359,7 +360,7 @@ fn dump_schedule_graph() {
let mut app = App::new();
settled_reach_server::tick_phases::TickPhase::configure(&mut app);
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin);
app.add_plugins(settled_reach_server::npc::NpcPlugin);
+2 -1
View File
@@ -358,9 +358,10 @@ pub fn try_load_economy(run_seed: u64) -> Option<(EconSimResource, EconStateReso
match Simulation::load_auto(run_seed) {
Ok(sim) => {
tracing::info!(
econ_seed = run_seed,
commodities = sim.economy().commodities.len(),
active_nodes = sim.nodes.len(),
"Economy simulation loaded"
"Economy simulation loaded with seed"
);
Some((EconSimResource::new(sim), EconStateResource::default()))
}
+7 -3
View File
@@ -8,13 +8,17 @@ use bevy_ecs::schedule::IntoScheduleConfigs;
use crate::tick_phases::TickPhase;
pub struct EconomyPlugin;
pub struct EconomyPlugin {
/// World seed from `StartupMessage` (#826). Passed to `try_load_economy` so the
/// tâtonnement simulation is seeded deterministically from the client's world seed.
pub seed: u64,
}
impl Plugin for EconomyPlugin {
fn build(&self, app: &mut App) {
// 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 (#826).
if let Some((econ_sim, econ_state)) = super::economy::try_load_economy(0) {
// Seed comes from StartupMessage.world_seed, threaded via SimulationPlugin (#826).
if let Some((econ_sim, econ_state)) = super::economy::try_load_economy(self.seed) {
app.insert_resource(econ_sim).insert_resource(econ_state);
}
// EconQueryBuffer: always registered so EconStateQuery PlayerActions are accepted
+8 -3
View File
@@ -50,7 +50,12 @@ pub mod zone;
/// Each sub-plugin owns its domain's systems, resources, and phase assignment.
/// No system registration happens here — only sub-plugin composition and
/// shared resources needed by multiple sub-plugins.
pub struct SimulationPlugin;
///
/// `seed` is threaded through to [`economy_plugin::EconomyPlugin`] so the
/// economy simulation uses the world seed from `StartupMessage` (#826).
pub struct SimulationPlugin {
pub seed: u64,
}
impl Plugin for SimulationPlugin {
fn build(&self, app: &mut App) {
@@ -59,7 +64,7 @@ impl Plugin for SimulationPlugin {
// Shared resources needed by multiple sub-plugins.
// Each sub-plugin inits its own domain-specific resources.
app.insert_resource(rng::SimRng::new(0))
app.insert_resource(rng::SimRng::new(self.seed))
.init_resource::<save_io::SaveLoadPending>()
.init_resource::<crate::knowledge::EntityRegistry>()
// Triangle escalation event queue (#250) — consumed by storyteller + npc plugins
@@ -81,7 +86,7 @@ impl Plugin for SimulationPlugin {
app.add_plugins(input_plugin::InputPlugin);
app.add_plugins(movement_plugin::MovementPlugin);
app.add_plugins(social_plugin::SocialPlugin);
app.add_plugins(economy_plugin::EconomyPlugin);
app.add_plugins(economy_plugin::EconomyPlugin { seed: self.seed });
// Background worker tick integration (#843 Part C)
app.add_systems(
+5 -5
View File
@@ -695,7 +695,7 @@ mod tests {
#[test]
fn gauntlet_setup_creates_expected_entities() {
let mut app = App::new();
app.add_plugins(crate::simulation::SimulationPlugin);
app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 });
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
@@ -718,7 +718,7 @@ mod tests {
#[test]
fn hub_center_is_walkable() {
let mut app = App::new();
app.add_plugins(crate::simulation::SimulationPlugin);
app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 });
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
@@ -735,7 +735,7 @@ mod tests {
#[test]
fn occlusion_north_wall_blocks() {
let mut app = App::new();
app.add_plugins(crate::simulation::SimulationPlugin);
app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 });
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
@@ -754,7 +754,7 @@ mod tests {
#[test]
fn corridor_connects_hub_to_occlusion() {
let mut app = App::new();
app.add_plugins(crate::simulation::SimulationPlugin);
app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 });
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
@@ -771,7 +771,7 @@ mod tests {
#[test]
fn stable_id_ranges_match_spec() {
let mut app = App::new();
app.add_plugins(crate::simulation::SimulationPlugin);
app.add_plugins(crate::simulation::SimulationPlugin { seed: 0 });
app.add_plugins(crate::knowledge::KnowledgePlugin);
app.add_plugins(crate::npc::NpcPlugin);
+1 -1
View File
@@ -78,7 +78,7 @@ mod gauntlet_integration {
/// Build a minimal Gauntlet app with the given archetype and run one tick.
fn boot_gauntlet(archetype: CharacterArchetype) -> App {
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
test_world::setup_gauntlet(&mut app, archetype);
app.update();
app
+1 -1
View File
@@ -221,7 +221,7 @@ fn t3_pause_mid_corridor_discards_movement_and_resumes() {
use settled_reach_server::simulation::SimulationPlugin;
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
// 200×200 walkability map covers the full gauntlet coordinate space.
app.insert_resource(WalkabilityMap::new(200, 200, 1));
+1 -1
View File
@@ -38,7 +38,7 @@ use settled_reach_server::simulation::SimulationPlugin;
/// Snapshots are written to SnapshotBuffer for direct inspection.
fn build_deterministic_app(seed: u64) -> App {
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed });
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.add_plugins(NpcPlugin);
+2 -2
View File
@@ -46,7 +46,7 @@ fn malformed_input_produces_sim_error_and_server_continues() {
let bridge = TcpBridge::accept_on(listener).expect("accept connection");
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.init_resource::<TrustEventQueue>();
@@ -177,7 +177,7 @@ fn state_hash_populated_in_snapshot() {
let bridge = TcpBridge::accept_on(listener).expect("accept connection");
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.init_resource::<TrustEventQueue>();
+1 -1
View File
@@ -28,7 +28,7 @@ fn player_moves_north_through_full_pipeline() {
// Build app
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.init_resource::<TrustEventQueue>();
+1 -1
View File
@@ -39,7 +39,7 @@ const FIXTURE_DIR: &str = "../tests/fixtures/gauntlet";
/// Identical to what the server runs in --test-mode.
fn build_gauntlet(seed: u64) -> App {
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.add_plugins(NpcPlugin);
+1 -1
View File
@@ -46,7 +46,7 @@ const NUM_TICKS: usize = 10;
/// Mirrors the setup in determinism.rs / main.rs.
fn build_app(seed: u64) -> App {
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(KnowledgePlugin);
app.add_plugins(NpcPlugin);
+2 -2
View File
@@ -6,7 +6,7 @@ use settled_reach_server::simulation::SimulationPlugin;
#[test]
fn movement_validated_within_app() {
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
// Insert a walkability map with one blocked tile
let mut map = WalkabilityMap::new(10, 10, 1);
@@ -56,7 +56,7 @@ fn movement_validated_within_app() {
#[test]
fn entity_collision_blocks_movement() {
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.insert_resource(WalkabilityMap::new(10, 10, 1));
// Stationary entity at (5,4)
+1 -1
View File
@@ -66,7 +66,7 @@ fn perf_tick_timing() {
let bridge = TcpBridge::accept_on(listener).expect("accept connection");
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
app.add_plugins(BridgePlugin);
app.add_plugins(settled_reach_server::knowledge::KnowledgePlugin);
app.add_plugins(settled_reach_server::npc::NpcPlugin);
+1 -1
View File
@@ -7,7 +7,7 @@ use settled_reach_server::simulation::SimulationPlugin;
#[test]
fn world_boots_and_ticks() {
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
// Verify initial state
let time = app.world().resource::<SimulationTime>();
+1 -1
View File
@@ -112,7 +112,7 @@ fn build_storyteller_app() -> App {
bridge::types::CharacterArchetype, simulation::SimulationPlugin, test_world,
};
let mut app = App::new();
app.add_plugins(SimulationPlugin);
app.add_plugins(SimulationPlugin { seed: 0 });
test_world::setup_gauntlet(&mut app, CharacterArchetype::default());
app
}