Generalize the world to multiple sources and haulers with dispatch arbitration #22

Closed
opened 2026-07-21 03:02:52 +00:00 by coilyco-ops · 1 comment
Owner

Build the next core simulation step after #21. This is core Rust logic, not Bevy/viewer/deploy work.

Current state on main:

  • WorldState hardcodes exactly one source and one hauler, and NodeId is a closed three-variant enum (Source, Road, Factory).
  • Dispatch assignment is a special case: it requires the hauler to be at the source node and matches the single source intent against the single factory intent.
  • The C# DispatchComponent matched dispatchers to receivers with already-served filtering across multiple candidates - that arbitration axis is the conceptual heart of the game and is not yet represented.

Goal:

Generalize WorldState to N sources and N haulers with deterministic dispatch arbitration, so multiple haulers can serve factory demand without over-assignment. Keep the shared-hub topology (each source and the factory hang off one road node) - still no grid pathfinding.

Implementation guidance:

  • NodeId becomes Source(u8) | Road | Factory, displayed and serialized as source-N / road / factory.
  • Topology derives from the scenario source count: hub-shaped, step_toward stays a two-hop rule (anything to anything passes through road). Drop the linear route array and the hauler route_index from snapshots.
  • ScenarioDefinition gains sources: Vec<SourceSpec { item, deposit, mining_speed }> and hauler_count.
  • Assignment loop in hauler index order: an unassigned, empty hauler may be assigned from any position (the collect phase moves it to the source). Factory need accounts for in-flight assignments - collect-phase haulers count at carry limit, deliver-phase haulers at actual cargo - so demand is never double-served.
  • Events name the hauler (hauler-0) and source node (source-0) so multi-actor runs stay debuggable.
  • Add a starter scenario exercising arbitration: one source, three haulers, factory input buffer 6, carry limit 3 - exactly two haulers assigned on the first wave, the third only once demand reopens.

Acceptance criteria:

  • cargo test --workspace passes.
  • The iron-bars scenario still produces iron bars deterministically with one source and one hauler, on the same tick timeline.
  • At least one test proves two haulers get assigned concurrently and a third stays unassigned while demand is covered.
  • Deliveries never exceed the factory input buffer.
  • Snapshots expose all sources and haulers. docs/factory-sim.md and docs/FEATURES.md updated.

Out of scope:

  • Multi-ingredient recipes (next slice).
  • Grid pathfinding, obstacles, terrain.
  • Power/battery/deployment verbs.
  • Bevy, Wasm, viewer, or deploy work.
Build the next core simulation step after #21. This is core Rust logic, not Bevy/viewer/deploy work. Current state on `main`: * `WorldState` hardcodes exactly one source and one hauler, and `NodeId` is a closed three-variant enum (`Source`, `Road`, `Factory`). * Dispatch assignment is a special case: it requires the hauler to be at the source node and matches the single source intent against the single factory intent. * The C# `DispatchComponent` matched dispatchers to receivers with already-served filtering across multiple candidates - that arbitration axis is the conceptual heart of the game and is not yet represented. Goal: Generalize `WorldState` to N sources and N haulers with deterministic dispatch arbitration, so multiple haulers can serve factory demand without over-assignment. Keep the shared-hub topology (each source and the factory hang off one road node) - still no grid pathfinding. Implementation guidance: * `NodeId` becomes `Source(u8) | Road | Factory`, displayed and serialized as `source-N` / `road` / `factory`. * `Topology` derives from the scenario source count: hub-shaped, `step_toward` stays a two-hop rule (anything to anything passes through road). Drop the linear `route` array and the hauler `route_index` from snapshots. * `ScenarioDefinition` gains `sources: Vec<SourceSpec { item, deposit, mining_speed }>` and `hauler_count`. * Assignment loop in hauler index order: an unassigned, empty hauler may be assigned from any position (the collect phase moves it to the source). Factory need accounts for in-flight assignments - collect-phase haulers count at carry limit, deliver-phase haulers at actual cargo - so demand is never double-served. * Events name the hauler (`hauler-0`) and source node (`source-0`) so multi-actor runs stay debuggable. * Add a starter scenario exercising arbitration: one source, three haulers, factory input buffer 6, carry limit 3 - exactly two haulers assigned on the first wave, the third only once demand reopens. Acceptance criteria: * `cargo test --workspace` passes. * The iron-bars scenario still produces iron bars deterministically with one source and one hauler, on the same tick timeline. * At least one test proves two haulers get assigned concurrently and a third stays unassigned while demand is covered. * Deliveries never exceed the factory input buffer. * Snapshots expose all sources and haulers. `docs/factory-sim.md` and `docs/FEATURES.md` updated. Out of scope: * Multi-ingredient recipes (next slice). * Grid pathfinding, obstacles, terrain. * Power/battery/deployment verbs. * Bevy, Wasm, viewer, or deploy work.
Author
Owner

Landed on main in b68e7bb (feat: generalize world to multiple sources and haulers).

What shipped:

  • WorldState holds Vec<SourceNode> and Vec<Hauler> around one factory. NodeId is now Source(u8) | Road | Factory, serialized as source-N / road / factory.
  • Hub topology derived from the scenario source count: every source and the factory hang off the road node, step_toward is a two-hop rule, no search. The linear route array and hauler route_index are gone from snapshots.
  • Deterministic arbitration in assign_dispatch: factory demand minus in-flight cargo (collect-phase haulers counted at carry limit, deliver-phase at actual cargo) is handed to unassigned empty haulers in index order, assignable from any position.
  • ScenarioDefinition gains sources: Vec<SourceSpec> and hauler_count. New iron-bars-fleet starter scenario: one richer source, three haulers, six-unit input buffer. The CLI resolves scenario names from the content database instead of a hardcoded match.
  • Events name actors: hauler-0, source-0.

One behavior note: an unassigned empty hauler now stays put instead of drifting back to the source, so the single-hauler lifecycle picks up its next assignment at the factory and walks back under a collect assignment. Production timing for iron-bars is unchanged (bars at tick 5) and the 6-tick run metrics are identical to the #21 baseline.

Also worth noting: with index-order arbitration the third fleet hauler stays unassigned as long as haulers 0 and 1 cover demand - the test asserts the first wave is exactly [assigned, assigned, unassigned].

Verification:

  • cargo test --workspace passes - 12 sim tests plus the CLI test, including first-wave arbitration and a 16-tick invariant that factory input stock never exceeds the buffer.
  • ward exec test and pre-commit run --all-files green (one catalog code-comments cap violation caught and fixed before commit).
  • An 8-tick fleet run shows two haulers alternating deliveries, hauler-2 idle, 20 bars crafted.

WARD-OUTCOME: done

Landed on `main` in `b68e7bb` (`feat: generalize world to multiple sources and haulers`). What shipped: * `WorldState` holds `Vec<SourceNode>` and `Vec<Hauler>` around one factory. `NodeId` is now `Source(u8) | Road | Factory`, serialized as `source-N` / `road` / `factory`. * Hub topology derived from the scenario source count: every source and the factory hang off the road node, `step_toward` is a two-hop rule, no search. The linear `route` array and hauler `route_index` are gone from snapshots. * Deterministic arbitration in `assign_dispatch`: factory demand minus in-flight cargo (collect-phase haulers counted at carry limit, deliver-phase at actual cargo) is handed to unassigned empty haulers in index order, assignable from any position. * `ScenarioDefinition` gains `sources: Vec<SourceSpec>` and `hauler_count`. New `iron-bars-fleet` starter scenario: one richer source, three haulers, six-unit input buffer. The CLI resolves scenario names from the content database instead of a hardcoded match. * Events name actors: `hauler-0`, `source-0`. One behavior note: an unassigned empty hauler now stays put instead of drifting back to the source, so the single-hauler lifecycle picks up its next assignment at the factory and walks back under a collect assignment. Production timing for iron-bars is unchanged (bars at tick 5) and the 6-tick run metrics are identical to the #21 baseline. Also worth noting: with index-order arbitration the third fleet hauler stays unassigned as long as haulers 0 and 1 cover demand - the test asserts the first wave is exactly `[assigned, assigned, unassigned]`. Verification: * `cargo test --workspace` passes - 12 sim tests plus the CLI test, including first-wave arbitration and a 16-tick invariant that factory input stock never exceeds the buffer. * `ward exec test` and `pre-commit run --all-files` green (one catalog code-comments cap violation caught and fixed before commit). * An 8-tick fleet run shows two haulers alternating deliveries, hauler-2 idle, 20 bars crafted. `WARD-OUTCOME: done`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-gaming/factory-game-v3#22
No description provided.