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
impl WorldSnapshot
pub fn person(&self, id: PersonId) -> Option<&Person>
pub fn government(&self, id: GovernmentId) -> Option<&Government>
pub fn territory(&self, id: TerritoryId) -> Option<&Territory>
pub fn route(&self, id: RouteId) -> Option<&Route>
Sourcepub fn army(&self, id: ArmyId) -> Option<&Army>
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}pub fn route_between( &self, from: TerritoryId, to: TerritoryId, ) -> Option<&Route>
pub fn adjacent_territories( &self, territory: TerritoryId, ) -> impl Iterator<Item = TerritoryId>
pub fn travel_duration( &self, from: TerritoryId, to: TerritoryId, ) -> Option<SimDuration>
pub fn distance(&self, from: TerritoryId, to: TerritoryId) -> Option<f32>
pub fn territories_within( &self, center: TerritoryId, radius: f32, ) -> impl Iterator<Item = &Territory>
Trait Implementations§
Source§impl Clone for WorldSnapshot
impl Clone for WorldSnapshot
Source§fn clone(&self) -> WorldSnapshot
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)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Debug for WorldSnapshot
impl Debug for WorldSnapshot
Source§impl Default for WorldSnapshot
impl Default for WorldSnapshot
Source§fn default() -> WorldSnapshot
fn default() -> WorldSnapshot
Returns the “default value” for a type. Read more
Source§impl<'de> Deserialize<'de> for WorldSnapshot
impl<'de> Deserialize<'de> for WorldSnapshot
Source§fn deserialize<__D>(
__deserializer: __D,
) -> Result<WorldSnapshot, <__D as Deserializer<'de>>::Error>where
__D: Deserializer<'de>,
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
impl PartialEq for WorldSnapshot
Source§impl Serialize for WorldSnapshot
impl Serialize for WorldSnapshot
Source§fn serialize<__S>(
&self,
__serializer: __S,
) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>where
__S: Serializer,
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
impl StructuralPartialEq for WorldSnapshot
Auto Trait Implementations§
impl Freeze for WorldSnapshot
impl RefUnwindSafe for WorldSnapshot
impl Send for WorldSnapshot
impl Sync for WorldSnapshot
impl Unpin for WorldSnapshot
impl UnsafeUnpin for WorldSnapshot
impl UnwindSafe for WorldSnapshot
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more