Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Dispatch Strategies

Dispatch is the brain of an elevator system – it decides which elevator goes where. This chapter covers imperative dispatch, the five built-in strategies, and how to choose between them.

How dispatch works

Each tick, the Dispatch phase runs four steps:

  1. Build manifest. The simulation collects per-stop demand (waiting riders, weights, wait times) and per-destination riding riders into a DispatchManifest.
  2. Collect idle elevators. Each group gathers its idle/stopped elevators and their current positions.
  3. Rank and match. The group’s DispatchStrategy scores every (car, stop) pair via rank(). The dispatch system feeds all scores into a Hungarian (Kuhn-Munkres) solver to produce the globally optimal assignment – one car per hall call, automatically.
  4. Apply decisions. Each elevator receives a DispatchDecision: either GoToStop(entity_id) to begin moving, or Idle to stay put.

Direction indicators (going_up/going_down) are set automatically from dispatch decisions, so downstream boarding gets direction-awareness with no extra work from the strategy. See Elevators for details.

Imperative dispatch with DestinationQueue

If you want to tell an elevator exactly where to go – bypassing strategy logic entirely – push directly to its DestinationQueue:

#![allow(unused)]
fn main() {
use elevator_core::prelude::*;
let mut sim: Simulation = todo!();
let elev: ElevatorId = todo!();
let stop_a: EntityId = todo!();
let stop_b: EntityId = todo!();
sim.push_destination(elev, stop_a).unwrap();        // enqueue at back
sim.push_destination_front(elev, stop_b).unwrap();  // jump to front of queue
sim.clear_destinations(elev).unwrap();              // cancel all pending stops
sim.abort_movement(elev).unwrap();                  // stop the current leg too
let queue: &[EntityId] = sim.destination_queue(elev).unwrap();
}

Adjacent duplicates are suppressed: pushing the same stop twice in a row is a no-op and emits a single DestinationQueued event.

clear_destinations is a soft clear – it only drains the pending queue. An elevator that is already mid-flight will finish its current leg and then go idle. To stop a moving car immediately, use abort_movement: it brakes the car along its normal deceleration profile, parks it at the nearest reachable stop (doors stay closed; onboard riders stay aboard), clears the queue, and emits MovementAborted so UI/metrics can react.

Between the Dispatch and Movement phases, the AdvanceQueue phase reconciles each elevator’s phase/target with the front of its queue. Idle elevators with a non-empty queue begin moving; elevators mid-flight whose queue front changed (because you called push_destination_front) are redirected. Movement pops the front on arrival.

You can mix imperative and strategy-driven dispatch freely. Dispatch keeps the queue in sync with its own decisions, so games can observe the queue for visualization and intervene only when needed.

Built-in strategies

StrategyAlgorithmBest forTrade-off
ScanDispatchSweep end-to-end, reversing at shaft extremesSingle elevator, uniform trafficSimple and fair; wastes time past the last request
LookDispatchLike SCAN, but reverses at the last request in the current directionSingle elevator, sparse trafficMore efficient than SCAN when requests cluster; slightly less predictable
NearestCarDispatchAssign each call to the closest idle elevatorMulti-elevator groupsLow average wait; can cause bunching when elevators cluster
EtdDispatchMinimize estimated time to destination across all ridersMulti-elevator groups with mixed trafficBest average performance; higher per-tick computation
DestinationDispatchSticky rider-to-car assignment via lobby kiosk inputDestination-dispatch systems (DCS)Requires HallCallMode::Destination; best with lobby kiosks

Choosing a strategy

                        +-- 1 elevator? -----------------> ScanDispatch
                        |                                   (or LookDispatch for bursty demand)
Does the group have  ---+-- 2+ elevators, simple ---------> NearestCarDispatch
                        |
                        +-- 2+ elevators, mixed traffic --> EtdDispatch
                        |   with SLA-sensitive riders
                        +-- DCS / lobby kiosks -----------> DestinationDispatch

Concrete guidance:

  • ScanDispatch – Start here. Deterministic, fair, easy to reason about. Good baseline for benchmarking custom strategies.
  • LookDispatch – Swap in when SCAN wastes obvious time at the extremes (sparse or clustered requests).
  • NearestCarDispatch – The default multi-car policy. Watch for bunching under heavy load.
  • EtdDispatch – Best average wait/ride time in most realistic mixes, at a higher per-tick cost. Use delay_weight to balance existing riders vs. new calls.

For priority, weight, fairness, or accessibility-aware dispatch, write a custom strategy – see Writing a Custom Dispatch Strategy.

Swapping strategies on the builder

The builder defaults to ScanDispatch. Call .dispatch() to use a different strategy:

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
use elevator_core::dispatch::look::LookDispatch;

fn main() -> Result<(), SimError> {
    let sim = SimulationBuilder::new()
        .stop(StopId(0), "Ground", 0.0)
        .stop(StopId(1), "Top", 10.0)
        .elevator(ElevatorConfig::default())
        .dispatch(LookDispatch::new())
        .build()?;
    Ok(())
}

All five strategies live in their respective modules:

#![allow(unused)]
fn main() {
use elevator_core::dispatch::scan::ScanDispatch;
use elevator_core::dispatch::look::LookDispatch;
use elevator_core::dispatch::nearest_car::NearestCarDispatch;
use elevator_core::dispatch::etd::EtdDispatch;
use elevator_core::dispatch::destination::DestinationDispatch;
}

The ETD strategy accepts a delay weight that controls how much it penalizes delays to existing riders when assigning a new call:

#![allow(unused)]
fn main() {
use elevator_core::dispatch::etd::EtdDispatch;

let etd = EtdDispatch::new();                         // default delay_weight = 1.0
let etd_conservative = EtdDispatch::with_delay_weight(1.5);  // favor existing riders
}

Multi-group dispatch

Large buildings often have separate elevator banks – a low-rise group serving floors 1-20 and a high-rise group serving floors 20-40. Each group can use its own dispatch strategy.

Use .dispatch_for_group() on the builder:

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::stop::StopId;
use elevator_core::dispatch::scan::ScanDispatch;
use elevator_core::dispatch::etd::EtdDispatch;

fn main() -> Result<(), SimError> {
    let sim = SimulationBuilder::new()
        .stop(StopId(0), "Ground", 0.0)
        .stop(StopId(1), "Top", 10.0)
        .elevator(ElevatorConfig::default())
        .dispatch_for_group(GroupId(0), ScanDispatch::new())
        .dispatch_for_group(GroupId(1), EtdDispatch::new())
        .build()?;
    Ok(())
}

Reposition strategies

After dispatch, idle elevators with no pending demand can be repositioned for better coverage. Configure a RepositionStrategy on the builder:

use elevator_core::prelude::*;
use elevator_core::config::ElevatorConfig;
use elevator_core::dispatch::BuiltinReposition;
use elevator_core::dispatch::reposition::SpreadEvenly;

fn main() -> Result<(), SimError> {
    let sim = SimulationBuilder::new()
        .stop(StopId(0), "Ground", 0.0)
        .stop(StopId(1), "Top", 10.0)
        .elevator(ElevatorConfig::default())
        .reposition(SpreadEvenly, BuiltinReposition::SpreadEvenly)
        .build()?;
    Ok(())
}

The second argument is a BuiltinReposition identifier used for snapshot serialization. Pass the variant that matches your strategy so snapshots can restore it correctly.

Four built-in strategies are available:

StrategyBehavior
SpreadEvenlyDistribute idle cars evenly across stops
ReturnToLobbySend idle cars to a configured home stop
DemandWeightedPosition near stops with historically high demand
NearestIdleKeep idle cars where they are (no-op)

Repositioning is optional. Groups without a registered strategy skip the reposition phase entirely.

DispatchManifest

Your strategy (or game code observing dispatch) receives a DispatchManifest with these convenience methods:

MethodReturnsDescription
waiting_count_at(stop)usizeNumber of riders waiting at a stop
total_weight_at(stop)f64Total weight of riders waiting at a stop
has_demand(stop)boolWhether a stop has any demand (waiting or riding-to)
riding_count_to(stop)usizeNumber of riders aboard elevators heading to a stop

For advanced dispatch (priority-aware, weight-aware, VIP-first), use manifest.waiting_riders_at(stop) to access per-stop rider lists, or manifest.iter_waiting_stops() to iterate all stops with waiting demand. Each entry provides a &[RiderInfo] with the rider’s id, destination, weight, and wait_ticks.

Next steps