Skip to main content

WorldSnapshot

Struct WorldSnapshot 

Source
pub struct WorldSnapshot {
    pub people: Vec<Person>,
    pub governments: Vec<Government>,
    pub territories: Vec<Territory>,
    pub routes: Vec<Route>,
    pub armies: Vec<Army>,
}

Fields§

§people: Vec<Person>§governments: Vec<Government>§territories: Vec<Territory>§routes: Vec<Route>§armies: Vec<Army>

Implementations§

Source§

impl WorldSnapshot

Source

pub fn person(&self, id: PersonId) -> Option<&Person>

Source

pub fn government(&self, id: GovernmentId) -> Option<&Government>

Source

pub fn territory(&self, id: TerritoryId) -> Option<&Territory>

Source

pub fn route(&self, id: RouteId) -> Option<&Route>

Source

pub fn army(&self, id: ArmyId) -> Option<&Army>

Examples found in repository?
examples/continuous_game_loop.rs (line 121)
120    fn from_world(world: &WorldSnapshot, army: ArmyId) -> Self {
121        let army = world.army(army).expect("the demo army must exist");
122        Self {
123            authoritative_location: army.location,
124            last_authoritative_transit: army.transit.clone(),
125            visual_progress: None,
126        }
127    }
128
129    fn refresh_from_authority(&mut self, world: &WorldSnapshot, army: ArmyId) {
130        let army = world.army(army).expect("the demo army must exist");
131        self.authoritative_location = army.location;
132        if let Some(transit) = &army.transit {
133            self.last_authoritative_transit = Some(transit.clone());
134        }
135    }
136
137    fn render(&mut self, presentation_time: PresentationTime) -> Option<f64> {
138        self.visual_progress = self.last_authoritative_transit.as_ref().map(|transit| {
139            if presentation_time.canonical < transit.departed_at {
140                return 0.0;
141            }
142            let elapsed = std_duration(presentation_time.canonical - transit.departed_at)
143                .checked_add(presentation_time.sub_minute)
144                .expect("presentation elapsed time must remain representable");
145            let travel = std_duration(transit.arrives_at - transit.departed_at);
146            (elapsed.as_secs_f64() / travel.as_secs_f64()).clamp(0.0, 1.0)
147        });
148        self.visual_progress
149    }
150
151    fn describe(&self) -> String {
152        match (&self.last_authoritative_transit, self.visual_progress) {
153            (Some(transit), Some(progress)) => format!(
154                "army visual progress={:.1}% ({} -> {}, authoritative_location={})",
155                progress * 100.0,
156                transit.from,
157                transit.to,
158                self.authoritative_location,
159            ),
160            _ => format!("army stationary at {}", self.authoritative_location),
161        }
162    }
163}
164
165/// The authoritative simulation clock and the render clock deliberately live
166/// in different fields. Only whole simulation minutes cross the Canwu API.
167struct GameHost {
168    authority: Canwu,
169    ids: DemoIds,
170    presentation: PresentationState,
171    accumulated_sim_nanos: u128,
172    wall_elapsed: Duration,
173    frame: u64,
174    reported_events: usize,
175    next_progress_milestone: u8,
176    canonical_boundaries: usize,
177    verbose: bool,
178}
179
180impl GameHost {
181    fn new(verbose: bool) -> Result<Self, CanwuError> {
182        let authority = Canwu::demo(35)?;
183        let ids = Canwu::demo_ids();
184        let presentation = PresentationState::from_world(&authority.world(), ids.army);
185        Ok(Self {
186            authority,
187            ids,
188            presentation,
189            accumulated_sim_nanos: 0,
190            wall_elapsed: Duration::ZERO,
191            frame: 0,
192            reported_events: 0,
193            next_progress_milestone: 25,
194            canonical_boundaries: 0,
195            verbose,
196        })
197    }
198
199    fn run_phase(&mut self, phase: SpeedPhase, profile: FrameProfile) -> Result<(), CanwuError> {
200        let mut remaining_millis = phase.wall_millis;
201        let mut pattern_index = 0;
202        let mut first_frame = true;
203
204        while remaining_millis > 0 {
205            let frame_millis = profile.frame_millis[pattern_index % profile.frame_millis.len()]
206                .min(remaining_millis);
207            remaining_millis -= frame_millis;
208            self.render_frame(
209                Duration::from_millis(frame_millis),
210                phase.speed,
211                first_frame || remaining_millis == 0,
212            )?;
213            first_frame = false;
214            pattern_index += 1;
215        }
216        Ok(())
217    }
218
219    fn render_frame(
220        &mut self,
221        wall_dt: Duration,
222        speed: GameSpeed,
223        phase_edge: bool,
224    ) -> Result<(), CanwuError> {
225        self.frame += 1;
226        self.wall_elapsed += wall_dt;
227
228        let converted_wall_nanos = wall_dt.as_nanos() * BASE_SIM_SECONDS_PER_WALL_SECOND;
229        self.accumulated_sim_nanos += converted_wall_nanos * speed.multiplier();
230
231        let mut authority_changed = false;
232        while self.accumulated_sim_nanos >= SIMULATION_MINUTE_NANOS {
233            let receipts = self.authority.advance_canonical(SIMULATION_QUANTUM)?;
234            self.canonical_boundaries += receipts.len();
235            authority_changed |= !receipts.is_empty();
236            self.accumulated_sim_nanos -= SIMULATION_MINUTE_NANOS;
237        }
238
239        if authority_changed {
240            self.refresh_presentation_from_authority();
241        }
242
243        let progress = self.presentation.render(self.presentation_time());
244        let crossed_milestone = self.crossed_progress_milestone(progress);
245        let has_new_events = self.reported_events < self.authority.events().len();
246
247        if self.verbose && (phase_edge || crossed_milestone || has_new_events) {
248            self.print_frame(speed);
249        }
250        self.report_new_events();
251        Ok(())
252    }
253
254    /// This host never backdates mid-quantum input. It deterministically rounds
255    /// up to the next representable Canwu minute; another host may choose a
256    /// different policy, but it must apply the same policy across render FPS.
257    fn submit_player_command(&mut self) -> Result<(), CanwuError> {
258        let captured_at = self.presentation_time();
259        let due_at = if self.accumulated_sim_nanos == 0 {
260            self.authority.time()
261        } else {
262            self.authority
263                .time()
264                .checked_add(SIMULATION_QUANTUM)
265                .expect("the scripted command time must remain representable")
266        };
267        let envelope = CommandEnvelope::new(
268            Issuer::Actor(self.ids.commander),
269            Command::MoveArmy {
270                army: self.ids.army,
271                destination: self.ids.eastern_territory,
272            },
273        )
274        .at_time(due_at);
275        let request = CommandRequest::new(
276            CommandRequestId::new(1),
277            self.authority.revision(),
278            envelope,
279        );
280        let ingress = self.authority.enqueue_command(due_at, 0, request)?;
281
282        if self.verbose {
283            println!(
284                "INPUT: queued commander move as {:?}, captured_at={captured_at}, quantized_due_at={due_at}",
285                ingress.ingress_id,
286            );
287        }
288
289        // This drains ingress due at the current canonical time only. The
290        // mid-quantum command remains queued until normal accumulation reaches
291        // its quantized minute; future scheduled work is never jumped to.
292        let receipts = self.authority.advance_canonical(SimDuration::ZERO)?;
293        self.canonical_boundaries += receipts.len();
294        self.refresh_presentation_from_authority();
295        self.presentation.render(self.presentation_time());
296        if self.verbose {
297            self.print_frame(GameSpeed::X1);
298        }
299        self.report_new_events();
300        Ok(())
301    }
302
303    fn refresh_presentation_from_authority(&mut self) {
304        self.presentation
305            .refresh_from_authority(&self.authority.world(), self.ids.army);
306    }
307
308    fn presentation_time(&self) -> PresentationTime {
309        PresentationTime {
310            canonical: self.authority.time(),
311            sub_minute: Duration::from_nanos(
312                u64::try_from(self.accumulated_sim_nanos)
313                    .expect("the sub-minute accumulator must fit a Duration"),
314            ),
315        }
316    }
317
318    fn crossed_progress_milestone(&mut self, progress: Option<f64>) -> bool {
319        let Some(progress) = progress else {
320            return false;
321        };
322        let mut crossed = false;
323        while self.next_progress_milestone <= 100
324            && progress * 100.0 + f64::EPSILON >= f64::from(self.next_progress_milestone)
325        {
326            crossed = true;
327            self.next_progress_milestone += 25;
328        }
329        crossed
330    }
331
332    fn print_frame(&self, speed: GameSpeed) {
333        println!(
334            "frame={:>5} speed={:<6} wall={:>7.3}s visual_time={} canwu_time={} {}",
335            self.frame,
336            speed.label(),
337            self.wall_elapsed.as_secs_f64(),
338            self.presentation_time(),
339            self.authority.time(),
340            self.presentation.describe(),
341        );
342    }
343
344    fn report_new_events(&mut self) {
345        let events = self.authority.events();
346        if self.verbose {
347            for event in &events[self.reported_events..] {
348                println!(
349                    "EVENT: at={} type={} {}",
350                    event.timestamp,
351                    event.kind.qualified_event_type(),
352                    event.summary,
353                );
354            }
355        }
356        self.reported_events = events.len();
357    }
358
359    fn outcome(self, profile: FrameProfile) -> Result<RunOutcome, CanwuError> {
360        Ok(RunOutcome {
361            profile: profile.name,
362            frames: self.frame,
363            time: self.authority.time(),
364            world: self.authority.world(),
365            events: self.authority.events().to_vec(),
366            commands: self.authority.commands().to_vec(),
367            authoritative_state_hash: self.authority.authoritative_state_hash()?,
368            checkpoint_hash: self.authority.checkpoint_hash().to_owned(),
369            canonical_boundaries: self.canonical_boundaries,
370        })
371    }
372}
373
374struct RunOutcome {
375    profile: &'static str,
376    frames: u64,
377    time: SimTime,
378    world: WorldSnapshot,
379    events: Vec<SimEvent>,
380    commands: Vec<CommandRecord>,
381    authoritative_state_hash: String,
382    checkpoint_hash: String,
383    canonical_boundaries: usize,
384}
385
386fn run(profile: FrameProfile, verbose: bool) -> Result<RunOutcome, CanwuError> {
387    let mut host = GameHost::new(verbose)?;
388    if verbose {
389        println!("\n{} render loop", profile.name);
390    }
391
392    host.run_phase(SPEED_SCHEDULE[0], profile)?;
393    host.submit_player_command()?;
394    host.run_phase(SPEED_SCHEDULE[1], profile)?;
395    host.run_phase(SPEED_SCHEDULE[2], profile)?;
396
397    let time_before_pause = host.authority.time();
398    let remainder_before_pause = host.accumulated_sim_nanos;
399    let presentation_before_pause = host.presentation_time();
400    let frames_before_pause = host.frame;
401    assert!(
402        remainder_before_pause > 0,
403        "the pause fixture must preserve a fractional simulation minute"
404    );
405    host.run_phase(SPEED_SCHEDULE[3], profile)?;
406    assert!(
407        host.frame > frames_before_pause,
408        "pause must keep rendering"
409    );
410    assert_eq!(
411        host.authority.time(),
412        time_before_pause,
413        "pause must not advance Canwu"
414    );
415    assert_eq!(
416        host.accumulated_sim_nanos, remainder_before_pause,
417        "pause must not add desired simulation time"
418    );
419    assert_eq!(
420        host.presentation_time(),
421        presentation_before_pause,
422        "pause must freeze presentation time"
423    );
424
425    host.run_phase(SPEED_SCHEDULE[4], profile)?;
426    assert_eq!(
427        host.accumulated_sim_nanos, 0,
428        "the scripted schedule should end on a simulation-minute boundary"
429    );
430    host.outcome(profile)
431}
432
433fn std_duration(duration: SimDuration) -> Duration {
434    let minutes = u64::try_from(duration.as_minutes())
435        .expect("presentation interpolation requires a non-negative duration");
436    Duration::from_secs(
437        minutes
438            .checked_mul(60)
439            .expect("presentation duration must remain representable"),
440    )
441}
442
443fn assert_fps_independent(left: &RunOutcome, right: &RunOutcome) {
444    assert_ne!(
445        left.frames, right.frames,
446        "the render segmentations must differ"
447    );
448    assert_eq!(left.time, right.time, "Canwu time changed with render FPS");
449    assert_eq!(
450        left.world, right.world,
451        "world state changed with render FPS"
452    );
453    assert_eq!(left.events, right.events, "events changed with render FPS");
454    assert_eq!(
455        left.commands, right.commands,
456        "command ordering changed with render FPS"
457    );
458    assert_eq!(
459        left.authoritative_state_hash, right.authoritative_state_hash,
460        "authoritative state hash changed with render FPS"
461    );
462    assert_eq!(
463        left.checkpoint_hash, right.checkpoint_hash,
464        "checkpoint hash changed with render FPS"
465    );
466    assert_eq!(
467        left.canonical_boundaries, right.canonical_boundaries,
468        "canonical boundary count changed with render FPS"
469    );
470}
471
472fn assert_expected_outcome(outcome: &RunOutcome) {
473    let ids = Canwu::demo_ids();
474    let command_at = SimTime::from_minutes(1);
475    let arrival_at = SimTime::from_minutes(1_081);
476    let final_time = SimTime::from_minutes(1_082);
477
478    assert_eq!(outcome.time, final_time, "the scripted final time changed");
479    let army = outcome
480        .world
481        .army(ids.army)
482        .expect("the demo army must remain present");
483    assert_eq!(
484        army.location, ids.eastern_territory,
485        "the scheduled arrival must execute"
486    );
487    assert!(
488        army.transit.is_none(),
489        "the army must no longer be in authoritative transit"
490    );
491
492    assert_eq!(
493        outcome.commands.len(),
494        1,
495        "the move command must be accepted"
496    );
497    let command = &outcome.commands[0];
498    assert_eq!(command.accepted_at, command_at);
499    assert_eq!(command.envelope.expected_time, Some(command_at));
500    assert!(matches!(
501        &command.envelope.command,
502        Command::MoveArmy { army, destination }
503            if *army == ids.army && *destination == ids.eastern_territory
504    ));
505
506    let event_timeline = outcome
507        .events
508        .iter()
509        .map(|event| (event.kind.qualified_event_type(), event.timestamp))
510        .collect::<Vec<_>>();
511    assert_eq!(
512        event_timeline,
513        vec![
514            ("move_ordered".to_owned(), command_at),
515            ("army_arrived".to_owned(), arrival_at),
516            ("knowledge_updated".to_owned(), arrival_at),
517            ("report_dispatched".to_owned(), arrival_at),
518        ],
519        "the expected command and scheduled-arrival event timeline changed"
520    );
521    assert_eq!(
522        outcome.canonical_boundaries, 2,
523        "the command and arrival should settle at two canonical boundaries"
524    );
525}
Source

pub fn route_between( &self, from: TerritoryId, to: TerritoryId, ) -> Option<&Route>

Source

pub fn adjacent_territories( &self, territory: TerritoryId, ) -> impl Iterator<Item = TerritoryId>

Source

pub fn travel_duration( &self, from: TerritoryId, to: TerritoryId, ) -> Option<SimDuration>

Source

pub fn distance(&self, from: TerritoryId, to: TerritoryId) -> Option<f32>

Source

pub fn territories_within( &self, center: TerritoryId, radius: f32, ) -> impl Iterator<Item = &Territory>

Trait Implementations§

Source§

impl Clone for WorldSnapshot

Source§

fn clone(&self) -> WorldSnapshot

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for WorldSnapshot

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more
Source§

impl Default for WorldSnapshot

Source§

fn default() -> WorldSnapshot

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for WorldSnapshot

Source§

fn deserialize<__D>( __deserializer: __D, ) -> Result<WorldSnapshot, <__D as Deserializer<'de>>::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl PartialEq for WorldSnapshot

Source§

fn eq(&self, other: &WorldSnapshot) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for WorldSnapshot

Source§

fn serialize<__S>( &self, __serializer: __S, ) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for WorldSnapshot

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.