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>,
    pub letters: Vec<LetterCargo>,
}

Fields§

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

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::OrderMovement {
270                subject: EntityRef::Army(self.ids.army),
271                destination: self.ids.eastern_territory,
272                cargo: Vec::new(),
273            },
274        )
275        .at_time(due_at);
276        let request = CommandRequest::new(
277            CommandRequestId::new(1),
278            self.authority.revision(),
279            envelope,
280        );
281        let ingress = self.authority.enqueue_command(due_at, 0, request)?;
282
283        if self.verbose {
284            println!(
285                "INPUT: queued commander move as {:?}, captured_at={captured_at}, quantized_due_at={due_at}",
286                ingress.ingress_id,
287            );
288        }
289
290        // This drains ingress due at the current canonical time only. The
291        // mid-quantum command remains queued until normal accumulation reaches
292        // its quantized minute; future scheduled work is never jumped to.
293        let receipts = self.authority.advance_canonical(SimDuration::ZERO)?;
294        self.canonical_boundaries += receipts.len();
295        self.refresh_presentation_from_authority();
296        self.presentation.render(self.presentation_time());
297        if self.verbose {
298            self.print_frame(GameSpeed::X1);
299        }
300        self.report_new_events();
301        Ok(())
302    }
303
304    fn refresh_presentation_from_authority(&mut self) {
305        self.presentation
306            .refresh_from_authority(&self.authority.world(), self.ids.army);
307    }
308
309    fn presentation_time(&self) -> PresentationTime {
310        PresentationTime {
311            canonical: self.authority.time(),
312            sub_minute: Duration::from_nanos(
313                u64::try_from(self.accumulated_sim_nanos)
314                    .expect("the sub-minute accumulator must fit a Duration"),
315            ),
316        }
317    }
318
319    fn crossed_progress_milestone(&mut self, progress: Option<f64>) -> bool {
320        let Some(progress) = progress else {
321            return false;
322        };
323        let mut crossed = false;
324        while self.next_progress_milestone <= 100
325            && progress * 100.0 + f64::EPSILON >= f64::from(self.next_progress_milestone)
326        {
327            crossed = true;
328            self.next_progress_milestone += 25;
329        }
330        crossed
331    }
332
333    fn print_frame(&self, speed: GameSpeed) {
334        println!(
335            "frame={:>5} speed={:<6} wall={:>7.3}s visual_time={} canwu_time={} {}",
336            self.frame,
337            speed.label(),
338            self.wall_elapsed.as_secs_f64(),
339            self.presentation_time(),
340            self.authority.time(),
341            self.presentation.describe(),
342        );
343    }
344
345    fn report_new_events(&mut self) {
346        let events = self.authority.events();
347        if self.verbose {
348            for event in &events[self.reported_events..] {
349                println!(
350                    "EVENT: at={} type={} {}",
351                    event.timestamp,
352                    event.kind.qualified_event_type(),
353                    event.summary,
354                );
355            }
356        }
357        self.reported_events = events.len();
358    }
359
360    fn outcome(self, profile: FrameProfile) -> Result<RunOutcome, CanwuError> {
361        Ok(RunOutcome {
362            profile: profile.name,
363            frames: self.frame,
364            time: self.authority.time(),
365            world: self.authority.world(),
366            events: self.authority.events().to_vec(),
367            commands: self.authority.commands().to_vec(),
368            authoritative_state_hash: self.authority.authoritative_state_hash()?,
369            checkpoint_hash: self.authority.checkpoint_hash().to_owned(),
370            canonical_boundaries: self.canonical_boundaries,
371        })
372    }
373}
374
375struct RunOutcome {
376    profile: &'static str,
377    frames: u64,
378    time: SimTime,
379    world: WorldSnapshot,
380    events: Vec<SimEvent>,
381    commands: Vec<CommandRecord>,
382    authoritative_state_hash: String,
383    checkpoint_hash: String,
384    canonical_boundaries: usize,
385}
386
387fn run(profile: FrameProfile, verbose: bool) -> Result<RunOutcome, CanwuError> {
388    let mut host = GameHost::new(verbose)?;
389    if verbose {
390        println!("\n{} render loop", profile.name);
391    }
392
393    host.run_phase(SPEED_SCHEDULE[0], profile)?;
394    host.submit_player_command()?;
395    host.run_phase(SPEED_SCHEDULE[1], profile)?;
396    host.run_phase(SPEED_SCHEDULE[2], profile)?;
397
398    let time_before_pause = host.authority.time();
399    let remainder_before_pause = host.accumulated_sim_nanos;
400    let presentation_before_pause = host.presentation_time();
401    let frames_before_pause = host.frame;
402    assert!(
403        remainder_before_pause > 0,
404        "the pause fixture must preserve a fractional simulation minute"
405    );
406    host.run_phase(SPEED_SCHEDULE[3], profile)?;
407    assert!(
408        host.frame > frames_before_pause,
409        "pause must keep rendering"
410    );
411    assert_eq!(
412        host.authority.time(),
413        time_before_pause,
414        "pause must not advance Canwu"
415    );
416    assert_eq!(
417        host.accumulated_sim_nanos, remainder_before_pause,
418        "pause must not add desired simulation time"
419    );
420    assert_eq!(
421        host.presentation_time(),
422        presentation_before_pause,
423        "pause must freeze presentation time"
424    );
425
426    host.run_phase(SPEED_SCHEDULE[4], profile)?;
427    assert_eq!(
428        host.accumulated_sim_nanos, 0,
429        "the scripted schedule should end on a simulation-minute boundary"
430    );
431    host.outcome(profile)
432}
433
434fn std_duration(duration: SimDuration) -> Duration {
435    let minutes = u64::try_from(duration.as_minutes())
436        .expect("presentation interpolation requires a non-negative duration");
437    Duration::from_secs(
438        minutes
439            .checked_mul(60)
440            .expect("presentation duration must remain representable"),
441    )
442}
443
444fn assert_fps_independent(left: &RunOutcome, right: &RunOutcome) {
445    assert_ne!(
446        left.frames, right.frames,
447        "the render segmentations must differ"
448    );
449    assert_eq!(left.time, right.time, "Canwu time changed with render FPS");
450    assert_eq!(
451        left.world, right.world,
452        "world state changed with render FPS"
453    );
454    assert_eq!(left.events, right.events, "events changed with render FPS");
455    assert_eq!(
456        left.commands, right.commands,
457        "command ordering changed with render FPS"
458    );
459    assert_eq!(
460        left.authoritative_state_hash, right.authoritative_state_hash,
461        "authoritative state hash changed with render FPS"
462    );
463    assert_eq!(
464        left.checkpoint_hash, right.checkpoint_hash,
465        "checkpoint hash changed with render FPS"
466    );
467    assert_eq!(
468        left.canonical_boundaries, right.canonical_boundaries,
469        "canonical boundary count changed with render FPS"
470    );
471}
472
473fn assert_expected_outcome(outcome: &RunOutcome) {
474    let ids = Canwu::demo_ids();
475    let command_at = SimTime::from_minutes(1);
476    let arrival_at = SimTime::from_minutes(1_081);
477    let final_time = SimTime::from_minutes(1_082);
478
479    assert_eq!(outcome.time, final_time, "the scripted final time changed");
480    let army = outcome
481        .world
482        .army(ids.army)
483        .expect("the demo army must remain present");
484    assert_eq!(
485        army.location, ids.eastern_territory,
486        "the scheduled arrival must execute"
487    );
488    assert!(
489        army.transit.is_none(),
490        "the army must no longer be in authoritative transit"
491    );
492
493    assert_eq!(
494        outcome.commands.len(),
495        1,
496        "the move command must be accepted"
497    );
498    let command = &outcome.commands[0];
499    assert_eq!(command.accepted_at, command_at);
500    assert_eq!(command.envelope.expected_time, Some(command_at));
501    assert!(matches!(
502        &command.envelope.command,
503        Command::OrderMovement {
504            subject: EntityRef::Army(army),
505            destination,
506            cargo,
507        } if *army == ids.army
508            && *destination == ids.eastern_territory
509            && cargo.is_empty()
510    ));
511
512    let event_timeline = outcome
513        .events
514        .iter()
515        .map(|event| (event.kind.qualified_event_type(), event.timestamp))
516        .collect::<Vec<_>>();
517    assert_eq!(
518        event_timeline,
519        vec![
520            ("move_ordered".to_owned(), command_at),
521            ("army_arrived".to_owned(), arrival_at),
522            ("knowledge_updated".to_owned(), arrival_at),
523            ("report_dispatched".to_owned(), arrival_at),
524        ],
525        "the expected command and scheduled-arrival event timeline changed"
526    );
527    assert_eq!(
528        outcome.canonical_boundaries, 2,
529        "the command and arrival should settle at two canonical boundaries"
530    );
531}
Source

pub fn letter(&self, id: LetterId) -> Option<&LetterCargo>

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.