Skip to main content

Canwu

Struct Canwu 

Source
pub struct Canwu { /* private fields */ }
Expand description

Main in-process API. All returned world values are detached snapshots.

Implementations§

Source§

impl Canwu

Source

pub const fn version() -> &'static str

Source

pub fn new(seed: u64, scenario: Scenario) -> Result<Self, CanwuError>

Source

pub fn into_compacted(self) -> Result<CompactedCanwu, CanwuError>

Enters the explicit compact-journal interface without discarding evidence.

Source

pub fn new_with_plugins( seed: u64, scenario: Scenario, plugins: &[&dyn SimulationPlugin], ) -> Result<Self, CanwuError>

Source

pub fn new_with_manifest( seed: u64, scenario: Scenario, run_manifest: RunManifest, ) -> Result<Self, CanwuError>

Source

pub fn new_with_manifest_and_plugins( seed: u64, scenario: Scenario, run_manifest: RunManifest, plugins: &[&dyn SimulationPlugin], ) -> Result<Self, CanwuError>

Source

pub fn new_with_run_configuration( seed: u64, scenario: Scenario, run_manifest: RunManifest, run_configuration: RunConfiguration, ) -> Result<Self, CanwuError>

Source

pub fn new_with_run_configuration_and_plugins( seed: u64, scenario: Scenario, run_manifest: RunManifest, run_configuration: RunConfiguration, plugins: &[&dyn SimulationPlugin], ) -> Result<Self, CanwuError>

Source

pub fn demo(seed: u64) -> Result<Self, CanwuError>

Examples found in repository?
examples/phased_boundary.rs (line 150)
149fn main() -> Result<(), CanwuError> {
150    let mut canwu = Canwu::demo(35)?;
151    canwu.register_plugin(&SupplyPlugin)?;
152    canwu.register_plugin(&DemandPlugin)?;
153
154    let receipt = canwu
155        .settle_boundary(BoundaryRequest::at(canwu.time()).with_cadence(SystemCadence::Daily))?;
156    assert_eq!(receipt.allocations[0].granted, 6);
157    assert_eq!(canwu.boundaries()[0].emissions.len(), 2);
158    Ok(())
159}
More examples
Hide additional examples
examples/plugin.rs (line 89)
88fn main() -> Result<(), CanwuError> {
89    let mut canwu = Canwu::demo(35)?;
90    let ids = Canwu::demo_ids();
91    canwu.register_plugin(&StancePlugin)?;
92    canwu.submit(CommandEnvelope::new(
93        Issuer::Actor(ids.commander),
94        Command::Plugin {
95            plugin: "example-stance".to_owned(),
96            command: "set_stance".to_owned(),
97            payload: json!({ "army": ids.army, "stance": "hold" }),
98        },
99    ))?;
100    Ok(())
101}
examples/continuous_game_loop.rs (line 182)
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    }
examples/decision_ticket.rs (line 12)
11fn main() -> Result<(), Box<dyn std::error::Error>> {
12    let mut canwu = Canwu::demo(1918)?;
13    let replay_scenario = replay_scenario(&canwu);
14    let ids = Canwu::demo_ids();
15
16    open_aid_request(&mut canwu, ids)?;
17    refresh_aid_options(&mut canwu, ids)?;
18    resolve_aid_request(&mut canwu)?;
19
20    let trace = canwu
21        .decision_traces()
22        .last()
23        .expect("utility decision trace");
24    println!("{}", serde_json::to_string_pretty(trace)?);
25
26    verify_persistence_and_replay(&canwu, replay_scenario)?;
27    println!("snapshot_restore=ok exact_replay=ok");
28    Ok(())
29}
examples/move_army.rs (line 4)
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4    let mut canwu = Canwu::demo(35)?;
5    let ids = Canwu::demo_ids();
6
7    canwu.act(
8        ids.commander,
9        SemanticAction::MoveEntity {
10            subject: EntityRef::Army(ids.army),
11            destination: ids.eastern_territory,
12            cargo: Vec::new(),
13        },
14    )?;
15    let arrival_events = canwu.advance(SimDuration::days(1))?;
16
17    let commander = canwu.observe(ids.commander, &ObserveRequest::default())?;
18    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
19    println!("time: {}", canwu.time());
20    println!("arrival events: {}", arrival_events.len());
21    println!(
22        "commander knows location: {:?}",
23        commander.known_armies[0].known_location
24    );
25    println!(
26        "observer still believes location: {:?}",
27        observer.known_armies[0].known_location
28    );
29
30    canwu.advance(SimDuration::days(3))?;
31    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
32    println!(
33        "observer after report: {:?}",
34        observer.known_armies[0].known_location
35    );
36    Ok(())
37}
Source

pub fn demo_ids() -> DemoIds

Examples found in repository?
examples/plugin.rs (line 90)
88fn main() -> Result<(), CanwuError> {
89    let mut canwu = Canwu::demo(35)?;
90    let ids = Canwu::demo_ids();
91    canwu.register_plugin(&StancePlugin)?;
92    canwu.submit(CommandEnvelope::new(
93        Issuer::Actor(ids.commander),
94        Command::Plugin {
95            plugin: "example-stance".to_owned(),
96            command: "set_stance".to_owned(),
97            payload: json!({ "army": ids.army, "stance": "hold" }),
98        },
99    ))?;
100    Ok(())
101}
More examples
Hide additional examples
examples/continuous_game_loop.rs (line 183)
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}
examples/decision_ticket.rs (line 14)
11fn main() -> Result<(), Box<dyn std::error::Error>> {
12    let mut canwu = Canwu::demo(1918)?;
13    let replay_scenario = replay_scenario(&canwu);
14    let ids = Canwu::demo_ids();
15
16    open_aid_request(&mut canwu, ids)?;
17    refresh_aid_options(&mut canwu, ids)?;
18    resolve_aid_request(&mut canwu)?;
19
20    let trace = canwu
21        .decision_traces()
22        .last()
23        .expect("utility decision trace");
24    println!("{}", serde_json::to_string_pretty(trace)?);
25
26    verify_persistence_and_replay(&canwu, replay_scenario)?;
27    println!("snapshot_restore=ok exact_replay=ok");
28    Ok(())
29}
examples/move_army.rs (line 5)
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4    let mut canwu = Canwu::demo(35)?;
5    let ids = Canwu::demo_ids();
6
7    canwu.act(
8        ids.commander,
9        SemanticAction::MoveEntity {
10            subject: EntityRef::Army(ids.army),
11            destination: ids.eastern_territory,
12            cargo: Vec::new(),
13        },
14    )?;
15    let arrival_events = canwu.advance(SimDuration::days(1))?;
16
17    let commander = canwu.observe(ids.commander, &ObserveRequest::default())?;
18    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
19    println!("time: {}", canwu.time());
20    println!("arrival events: {}", arrival_events.len());
21    println!(
22        "commander knows location: {:?}",
23        commander.known_armies[0].known_location
24    );
25    println!(
26        "observer still believes location: {:?}",
27        observer.known_armies[0].known_location
28    );
29
30    canwu.advance(SimDuration::days(3))?;
31    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
32    println!(
33        "observer after report: {:?}",
34        observer.known_armies[0].known_location
35    );
36    Ok(())
37}
Source

pub const fn time(&self) -> SimTime

Examples found in repository?
examples/phased_boundary.rs (line 155)
149fn main() -> Result<(), CanwuError> {
150    let mut canwu = Canwu::demo(35)?;
151    canwu.register_plugin(&SupplyPlugin)?;
152    canwu.register_plugin(&DemandPlugin)?;
153
154    let receipt = canwu
155        .settle_boundary(BoundaryRequest::at(canwu.time()).with_cadence(SystemCadence::Daily))?;
156    assert_eq!(receipt.allocations[0].granted, 6);
157    assert_eq!(canwu.boundaries()[0].emissions.len(), 2);
158    Ok(())
159}
More examples
Hide additional examples
examples/move_army.rs (line 19)
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4    let mut canwu = Canwu::demo(35)?;
5    let ids = Canwu::demo_ids();
6
7    canwu.act(
8        ids.commander,
9        SemanticAction::MoveEntity {
10            subject: EntityRef::Army(ids.army),
11            destination: ids.eastern_territory,
12            cargo: Vec::new(),
13        },
14    )?;
15    let arrival_events = canwu.advance(SimDuration::days(1))?;
16
17    let commander = canwu.observe(ids.commander, &ObserveRequest::default())?;
18    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
19    println!("time: {}", canwu.time());
20    println!("arrival events: {}", arrival_events.len());
21    println!(
22        "commander knows location: {:?}",
23        commander.known_armies[0].known_location
24    );
25    println!(
26        "observer still believes location: {:?}",
27        observer.known_armies[0].known_location
28    );
29
30    canwu.advance(SimDuration::days(3))?;
31    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
32    println!(
33        "observer after report: {:?}",
34        observer.known_armies[0].known_location
35    );
36    Ok(())
37}
examples/continuous_game_loop.rs (line 260)
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}
examples/decision_ticket.rs (line 42)
41fn open_aid_request(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
42    let now = canwu.time();
43    let controller = DecisionControllerBinding::new(
44        "warlord-b-ai",
45        DecisionPolicyIdentity::new(DecisionPolicyKind::Utility, "aid-utility", "1"),
46        DecisionAuthority::Actor {
47            actor: ids.commander,
48        },
49    )
50    .with_command_subject(EntityRef::Army(ids.army));
51
52    canwu.enqueue_decision(
53        now,
54        0,
55        DecisionIngressRequest::new(
56            DecisionRequestId::new(1),
57            canwu.revision(),
58            DecisionMutation::RegisterController { controller },
59        ),
60    )?;
61    canwu.enqueue_decision(
62        now,
63        0,
64        DecisionIngressRequest::new(
65            DecisionRequestId::new(2),
66            canwu.revision(),
67            DecisionMutation::Open {
68                ticket: DecisionTicketDraft {
69                    id: DecisionTicketId::new(1),
70                    definition: "beiyang.request-military-aid".to_owned(),
71                    decision_maker: EntityRef::Person(ids.commander),
72                    assigned_controller: "warlord-b-ai".to_owned(),
73                    summary: "Neighboring warlord requests immediate military aid".to_owned(),
74                    context: DecisionContext::new(
75                        "beiyang.aid-request.v1",
76                        json!({
77                            "requester": "warlord-a",
78                            "battle": "ongoing-front",
79                            "common_enemy": true
80                        }),
81                    ),
82                    options: vec![DecisionOption {
83                        action: DecisionAction::None,
84                        utility_inputs: BTreeMap::from([
85                            ("home_defense".to_owned(), 80),
86                            ("alliance".to_owned(), -40),
87                        ]),
88                        ..DecisionOption::new("decline", "Decline aid")
89                    }],
90                    deadline: Some(now + SimDuration::days(2)),
91                },
92            },
93        ),
94    )?;
95    canwu.step_canonical()?.expect("decision intake boundary");
96    Ok(())
97}
98
99fn refresh_aid_options(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
100    canwu.enqueue_decision(
101        canwu.time(),
102        0,
103        DecisionIngressRequest::new(
104            DecisionRequestId::new(3),
105            canwu.revision(),
106            DecisionMutation::ReplaceOptions {
107                ticket_id: DecisionTicketId::new(1),
108                expected_version: 1,
109                context: DecisionContext::new(
110                    "beiyang.aid-request.v1",
111                    json!({
112                        "requester": "warlord-a",
113                        "battle": "ongoing-front",
114                        "common_enemy": true,
115                        "route_confirmed": true
116                    }),
117                ),
118                options: vec![
119                    DecisionOption {
120                        action: DecisionAction::None,
121                        utility_inputs: BTreeMap::from([
122                            ("home_defense".to_owned(), 80),
123                            ("alliance".to_owned(), -40),
124                        ]),
125                        ..DecisionOption::new("decline", "Decline aid")
126                    },
127                    DecisionOption {
128                        action: DecisionAction::Command {
129                            command: serde_json::to_value(Command::OrderMovement {
130                                subject: EntityRef::Army(ids.army),
131                                destination: ids.eastern_territory,
132                                cargo: Vec::new(),
133                            })?,
134                        },
135                        utility_inputs: BTreeMap::from([
136                            ("home_defense".to_owned(), -20),
137                            ("alliance".to_owned(), 90),
138                        ]),
139                        ..DecisionOption::new("send-aid", "Send the neighboring army")
140                    },
141                ],
142            },
143        ),
144    )?;
145    canwu
146        .step_canonical()?
147        .expect("dynamic option refresh boundary");
148    Ok(())
149}
150
151fn resolve_aid_request(canwu: &mut Canwu) -> Result<(), Box<dyn std::error::Error>> {
152    let policy = WeightedUtilityPolicy::new(
153        "aid-utility",
154        "1",
155        UtilityProfile {
156            weights: BTreeMap::from([("alliance".to_owned(), 3), ("home_defense".to_owned(), 1)]),
157        },
158    );
159    let evaluation = canwu.drive_decision(
160        canwu.time(),
161        0,
162        DecisionRequestId::new(4),
163        Some(CommandRequestId::new(1)),
164        DecisionTicketId::new(1),
165        &policy,
166    )?;
167    assert!(matches!(evaluation, DecisionEvaluation::Prepared(_)));
168    canwu
169        .step_canonical()?
170        .expect("decision resolution boundary");
171    Ok(())
172}
Source

pub const fn run_manifest(&self) -> &RunManifest

Source

pub const fn run_configuration(&self) -> &RunConfigurationSnapshot

Source

pub fn revision(&self) -> u64

Returns the persisted authoritative transaction revision.

Accepted commands, persisted expected rejections, and completed settlement boundaries each advance it exactly once. Failed work, exact retries, bare clock movement, queued but unadmitted ingress, and plugin setup do not advance it; combine it with command expected-time guards.

Examples found in repository?
examples/continuous_game_loop.rs (line 278)
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    }
More examples
Hide additional examples
examples/decision_ticket.rs (line 57)
41fn open_aid_request(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
42    let now = canwu.time();
43    let controller = DecisionControllerBinding::new(
44        "warlord-b-ai",
45        DecisionPolicyIdentity::new(DecisionPolicyKind::Utility, "aid-utility", "1"),
46        DecisionAuthority::Actor {
47            actor: ids.commander,
48        },
49    )
50    .with_command_subject(EntityRef::Army(ids.army));
51
52    canwu.enqueue_decision(
53        now,
54        0,
55        DecisionIngressRequest::new(
56            DecisionRequestId::new(1),
57            canwu.revision(),
58            DecisionMutation::RegisterController { controller },
59        ),
60    )?;
61    canwu.enqueue_decision(
62        now,
63        0,
64        DecisionIngressRequest::new(
65            DecisionRequestId::new(2),
66            canwu.revision(),
67            DecisionMutation::Open {
68                ticket: DecisionTicketDraft {
69                    id: DecisionTicketId::new(1),
70                    definition: "beiyang.request-military-aid".to_owned(),
71                    decision_maker: EntityRef::Person(ids.commander),
72                    assigned_controller: "warlord-b-ai".to_owned(),
73                    summary: "Neighboring warlord requests immediate military aid".to_owned(),
74                    context: DecisionContext::new(
75                        "beiyang.aid-request.v1",
76                        json!({
77                            "requester": "warlord-a",
78                            "battle": "ongoing-front",
79                            "common_enemy": true
80                        }),
81                    ),
82                    options: vec![DecisionOption {
83                        action: DecisionAction::None,
84                        utility_inputs: BTreeMap::from([
85                            ("home_defense".to_owned(), 80),
86                            ("alliance".to_owned(), -40),
87                        ]),
88                        ..DecisionOption::new("decline", "Decline aid")
89                    }],
90                    deadline: Some(now + SimDuration::days(2)),
91                },
92            },
93        ),
94    )?;
95    canwu.step_canonical()?.expect("decision intake boundary");
96    Ok(())
97}
98
99fn refresh_aid_options(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
100    canwu.enqueue_decision(
101        canwu.time(),
102        0,
103        DecisionIngressRequest::new(
104            DecisionRequestId::new(3),
105            canwu.revision(),
106            DecisionMutation::ReplaceOptions {
107                ticket_id: DecisionTicketId::new(1),
108                expected_version: 1,
109                context: DecisionContext::new(
110                    "beiyang.aid-request.v1",
111                    json!({
112                        "requester": "warlord-a",
113                        "battle": "ongoing-front",
114                        "common_enemy": true,
115                        "route_confirmed": true
116                    }),
117                ),
118                options: vec![
119                    DecisionOption {
120                        action: DecisionAction::None,
121                        utility_inputs: BTreeMap::from([
122                            ("home_defense".to_owned(), 80),
123                            ("alliance".to_owned(), -40),
124                        ]),
125                        ..DecisionOption::new("decline", "Decline aid")
126                    },
127                    DecisionOption {
128                        action: DecisionAction::Command {
129                            command: serde_json::to_value(Command::OrderMovement {
130                                subject: EntityRef::Army(ids.army),
131                                destination: ids.eastern_territory,
132                                cargo: Vec::new(),
133                            })?,
134                        },
135                        utility_inputs: BTreeMap::from([
136                            ("home_defense".to_owned(), -20),
137                            ("alliance".to_owned(), 90),
138                        ]),
139                        ..DecisionOption::new("send-aid", "Send the neighboring army")
140                    },
141                ],
142            },
143        ),
144    )?;
145    canwu
146        .step_canonical()?
147        .expect("dynamic option refresh boundary");
148    Ok(())
149}
Source

pub fn run_manifest_hash(&self) -> &str

Source

pub fn checkpoint_hash(&self) -> &str

Examples found in repository?
examples/continuous_game_loop.rs (line 369)
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    }
Source

pub fn authoritative_state_hash(&self) -> Result<String, CanwuError>

Examples found in repository?
examples/continuous_game_loop.rs (line 368)
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    }
Source

pub fn world(&self) -> WorldSnapshot

Examples found in repository?
examples/continuous_game_loop.rs (line 184)
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    }
Source

pub fn knowledge(&self) -> &KnowledgeSnapshot

Trusted host/admin access to the complete knowledge snapshot.

Do not expose this facade to player, agent, observer, or remote clients; use Canwu::viewer or Canwu::viewer_for_actor instead.

Source

pub fn events(&self) -> &[SimEvent]

Examples found in repository?
examples/continuous_game_loop.rs (line 245)
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    }
Source

pub fn commands(&self) -> &[CommandRecord]

Examples found in repository?
examples/continuous_game_loop.rs (line 367)
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    }
Source

pub fn boundaries(&self) -> &[BoundaryRecord]

Examples found in repository?
examples/phased_boundary.rs (line 157)
149fn main() -> Result<(), CanwuError> {
150    let mut canwu = Canwu::demo(35)?;
151    canwu.register_plugin(&SupplyPlugin)?;
152    canwu.register_plugin(&DemandPlugin)?;
153
154    let receipt = canwu
155        .settle_boundary(BoundaryRequest::at(canwu.time()).with_cadence(SystemCadence::Daily))?;
156    assert_eq!(receipt.allocations[0].granted, 6);
157    assert_eq!(canwu.boundaries()[0].emissions.len(), 2);
158    Ok(())
159}
Source

pub fn command_attempts(&self) -> &[CommandAttemptRecord]

Source

pub fn ingress_log(&self) -> &[IngressRecord]

Source

pub fn domain_record( &self, reference: &DomainRecordRef, ) -> Option<&DomainRecord>

Source

pub fn typed_domain_record<T: DomainRecordType>( &self, reference: &TypedDomainRecordRef<T>, ) -> Option<&DomainRecord>

Source

pub fn domain_records(&self) -> impl Iterator<Item = &DomainRecord>

Source

pub const fn decision_state(&self) -> &DecisionState

Source

pub fn decision_ticket(&self, id: DecisionTicketId) -> Option<&DecisionTicket>

Source

pub fn decision_traces(&self) -> &[DecisionTrace]

Examples found in repository?
examples/decision_ticket.rs (line 21)
11fn main() -> Result<(), Box<dyn std::error::Error>> {
12    let mut canwu = Canwu::demo(1918)?;
13    let replay_scenario = replay_scenario(&canwu);
14    let ids = Canwu::demo_ids();
15
16    open_aid_request(&mut canwu, ids)?;
17    refresh_aid_options(&mut canwu, ids)?;
18    resolve_aid_request(&mut canwu)?;
19
20    let trace = canwu
21        .decision_traces()
22        .last()
23        .expect("utility decision trace");
24    println!("{}", serde_json::to_string_pretty(trace)?);
25
26    verify_persistence_and_replay(&canwu, replay_scenario)?;
27    println!("snapshot_restore=ok exact_replay=ok");
28    Ok(())
29}
Source

pub fn decision_attempts(&self) -> &[DecisionAttemptRecord]

Source

pub fn random_draws(&self) -> &[RandomDrawRecord]

Source

pub fn boundary_head_hash(&self) -> Option<&str>

Source

pub const fn schema(&self) -> &SchemaRegistry

Source

pub fn plugin_descriptors(&self) -> impl Iterator<Item = &PluginDescriptor>

Source

pub fn replay_journal(&self) -> ReplayJournal

Examples found in repository?
examples/decision_ticket.rs (line 181)
174fn verify_persistence_and_replay(
175    canwu: &Canwu,
176    replay_scenario: Scenario,
177) -> Result<(), Box<dyn std::error::Error>> {
178    let snapshot = canwu.snapshot();
179    let restored = Canwu::from_snapshot_json(&serde_json::to_string(&snapshot)?)?;
180    assert_eq!(restored.snapshot(), snapshot);
181    let replayed = Canwu::replay_from_journal(replay_scenario, &[], &canwu.replay_journal())?;
182    assert_eq!(replayed.snapshot(), snapshot);
183    Ok(())
184}
Source

pub fn evidence_cursor(&self) -> Result<EvidenceCursor, CanwuError>

Source

pub fn checkpoint(&self) -> Result<SimulationCheckpoint, CanwuError>

Source

pub fn journal_segment_since( &self, start: EvidenceCursor, ) -> Result<EvidenceJournalSegment, CanwuError>

Source

pub fn checkpoint_journal(&self) -> Result<CheckpointJournal, CanwuError>

Source

pub fn checkpoint_journal_json(&self) -> Result<String, CanwuError>

Source

pub fn register_plugin<P: SimulationPlugin + ?Sized>( &mut self, plugin: &P, ) -> Result<(), CanwuError>

Examples found in repository?
examples/phased_boundary.rs (line 151)
149fn main() -> Result<(), CanwuError> {
150    let mut canwu = Canwu::demo(35)?;
151    canwu.register_plugin(&SupplyPlugin)?;
152    canwu.register_plugin(&DemandPlugin)?;
153
154    let receipt = canwu
155        .settle_boundary(BoundaryRequest::at(canwu.time()).with_cadence(SystemCadence::Daily))?;
156    assert_eq!(receipt.allocations[0].granted, 6);
157    assert_eq!(canwu.boundaries()[0].emissions.len(), 2);
158    Ok(())
159}
More examples
Hide additional examples
examples/plugin.rs (line 91)
88fn main() -> Result<(), CanwuError> {
89    let mut canwu = Canwu::demo(35)?;
90    let ids = Canwu::demo_ids();
91    canwu.register_plugin(&StancePlugin)?;
92    canwu.submit(CommandEnvelope::new(
93        Issuer::Actor(ids.commander),
94        Command::Plugin {
95            plugin: "example-stance".to_owned(),
96            command: "set_stance".to_owned(),
97            payload: json!({ "army": ids.army, "stance": "hold" }),
98        },
99    ))?;
100    Ok(())
101}
Source

pub fn submit( &mut self, command: CommandEnvelope, ) -> Result<CommandReceipt, CanwuError>

Examples found in repository?
examples/plugin.rs (lines 92-99)
88fn main() -> Result<(), CanwuError> {
89    let mut canwu = Canwu::demo(35)?;
90    let ids = Canwu::demo_ids();
91    canwu.register_plugin(&StancePlugin)?;
92    canwu.submit(CommandEnvelope::new(
93        Issuer::Actor(ids.commander),
94        Command::Plugin {
95            plugin: "example-stance".to_owned(),
96            command: "set_stance".to_owned(),
97            payload: json!({ "army": ids.army, "stance": "hold" }),
98        },
99    ))?;
100    Ok(())
101}
Source

pub fn process_command( &mut self, request: CommandRequest, ) -> Result<CommandOutcome, CanwuError>

Source

pub fn enqueue_command( &mut self, due_at: SimTime, priority: i32, request: CommandRequest, ) -> Result<IngressReceipt, CanwuError>

Examples found in repository?
examples/continuous_game_loop.rs (line 281)
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    }
Source

pub fn enqueue_plugin_ingress( &mut self, request: PluginIngressRequest, ) -> Result<IngressReceipt, CanwuError>

Source

pub fn prepare_decision( &self, decision_request_id: DecisionRequestId, command_request_id: Option<CommandRequestId>, ticket_id: DecisionTicketId, policy: &dyn DecisionPolicy, ) -> Result<DecisionEvaluation, CanwuError>

Source

pub fn prepare_decision_at( &self, due_at: SimTime, decision_request_id: DecisionRequestId, command_request_id: Option<CommandRequestId>, ticket_id: DecisionTicketId, policy: &dyn DecisionPolicy, ) -> Result<DecisionEvaluation, CanwuError>

Source

pub fn enqueue_decision( &mut self, due_at: SimTime, priority: i32, request: DecisionIngressRequest, ) -> Result<IngressReceipt, CanwuError>

Examples found in repository?
examples/decision_ticket.rs (lines 52-60)
41fn open_aid_request(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
42    let now = canwu.time();
43    let controller = DecisionControllerBinding::new(
44        "warlord-b-ai",
45        DecisionPolicyIdentity::new(DecisionPolicyKind::Utility, "aid-utility", "1"),
46        DecisionAuthority::Actor {
47            actor: ids.commander,
48        },
49    )
50    .with_command_subject(EntityRef::Army(ids.army));
51
52    canwu.enqueue_decision(
53        now,
54        0,
55        DecisionIngressRequest::new(
56            DecisionRequestId::new(1),
57            canwu.revision(),
58            DecisionMutation::RegisterController { controller },
59        ),
60    )?;
61    canwu.enqueue_decision(
62        now,
63        0,
64        DecisionIngressRequest::new(
65            DecisionRequestId::new(2),
66            canwu.revision(),
67            DecisionMutation::Open {
68                ticket: DecisionTicketDraft {
69                    id: DecisionTicketId::new(1),
70                    definition: "beiyang.request-military-aid".to_owned(),
71                    decision_maker: EntityRef::Person(ids.commander),
72                    assigned_controller: "warlord-b-ai".to_owned(),
73                    summary: "Neighboring warlord requests immediate military aid".to_owned(),
74                    context: DecisionContext::new(
75                        "beiyang.aid-request.v1",
76                        json!({
77                            "requester": "warlord-a",
78                            "battle": "ongoing-front",
79                            "common_enemy": true
80                        }),
81                    ),
82                    options: vec![DecisionOption {
83                        action: DecisionAction::None,
84                        utility_inputs: BTreeMap::from([
85                            ("home_defense".to_owned(), 80),
86                            ("alliance".to_owned(), -40),
87                        ]),
88                        ..DecisionOption::new("decline", "Decline aid")
89                    }],
90                    deadline: Some(now + SimDuration::days(2)),
91                },
92            },
93        ),
94    )?;
95    canwu.step_canonical()?.expect("decision intake boundary");
96    Ok(())
97}
98
99fn refresh_aid_options(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
100    canwu.enqueue_decision(
101        canwu.time(),
102        0,
103        DecisionIngressRequest::new(
104            DecisionRequestId::new(3),
105            canwu.revision(),
106            DecisionMutation::ReplaceOptions {
107                ticket_id: DecisionTicketId::new(1),
108                expected_version: 1,
109                context: DecisionContext::new(
110                    "beiyang.aid-request.v1",
111                    json!({
112                        "requester": "warlord-a",
113                        "battle": "ongoing-front",
114                        "common_enemy": true,
115                        "route_confirmed": true
116                    }),
117                ),
118                options: vec![
119                    DecisionOption {
120                        action: DecisionAction::None,
121                        utility_inputs: BTreeMap::from([
122                            ("home_defense".to_owned(), 80),
123                            ("alliance".to_owned(), -40),
124                        ]),
125                        ..DecisionOption::new("decline", "Decline aid")
126                    },
127                    DecisionOption {
128                        action: DecisionAction::Command {
129                            command: serde_json::to_value(Command::OrderMovement {
130                                subject: EntityRef::Army(ids.army),
131                                destination: ids.eastern_territory,
132                                cargo: Vec::new(),
133                            })?,
134                        },
135                        utility_inputs: BTreeMap::from([
136                            ("home_defense".to_owned(), -20),
137                            ("alliance".to_owned(), 90),
138                        ]),
139                        ..DecisionOption::new("send-aid", "Send the neighboring army")
140                    },
141                ],
142            },
143        ),
144    )?;
145    canwu
146        .step_canonical()?
147        .expect("dynamic option refresh boundary");
148    Ok(())
149}
Source

pub fn drive_decision( &mut self, due_at: SimTime, priority: i32, decision_request_id: DecisionRequestId, command_request_id: Option<CommandRequestId>, ticket_id: DecisionTicketId, policy: &dyn DecisionPolicy, ) -> Result<DecisionEvaluation, CanwuError>

Examples found in repository?
examples/decision_ticket.rs (lines 159-166)
151fn resolve_aid_request(canwu: &mut Canwu) -> Result<(), Box<dyn std::error::Error>> {
152    let policy = WeightedUtilityPolicy::new(
153        "aid-utility",
154        "1",
155        UtilityProfile {
156            weights: BTreeMap::from([("alliance".to_owned(), 3), ("home_defense".to_owned(), 1)]),
157        },
158    );
159    let evaluation = canwu.drive_decision(
160        canwu.time(),
161        0,
162        DecisionRequestId::new(4),
163        Some(CommandRequestId::new(1)),
164        DecisionTicketId::new(1),
165        &policy,
166    )?;
167    assert!(matches!(evaluation, DecisionEvaluation::Prepared(_)));
168    canwu
169        .step_canonical()?
170        .expect("decision resolution boundary");
171    Ok(())
172}
Source

pub fn schedule_calendar_boundary( &mut self, due_at: SimTime, cadences: Vec<SystemCadence>, ) -> Result<IngressReceipt, CanwuError>

Source

pub fn advance_canonical( &mut self, duration: SimDuration, ) -> Result<Vec<BoundaryReceipt>, CanwuError>

Examples found in repository?
examples/continuous_game_loop.rs (line 233)
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    }
Source

pub fn step_canonical(&mut self) -> Result<Option<BoundaryReceipt>, CanwuError>

Examples found in repository?
examples/decision_ticket.rs (line 95)
41fn open_aid_request(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
42    let now = canwu.time();
43    let controller = DecisionControllerBinding::new(
44        "warlord-b-ai",
45        DecisionPolicyIdentity::new(DecisionPolicyKind::Utility, "aid-utility", "1"),
46        DecisionAuthority::Actor {
47            actor: ids.commander,
48        },
49    )
50    .with_command_subject(EntityRef::Army(ids.army));
51
52    canwu.enqueue_decision(
53        now,
54        0,
55        DecisionIngressRequest::new(
56            DecisionRequestId::new(1),
57            canwu.revision(),
58            DecisionMutation::RegisterController { controller },
59        ),
60    )?;
61    canwu.enqueue_decision(
62        now,
63        0,
64        DecisionIngressRequest::new(
65            DecisionRequestId::new(2),
66            canwu.revision(),
67            DecisionMutation::Open {
68                ticket: DecisionTicketDraft {
69                    id: DecisionTicketId::new(1),
70                    definition: "beiyang.request-military-aid".to_owned(),
71                    decision_maker: EntityRef::Person(ids.commander),
72                    assigned_controller: "warlord-b-ai".to_owned(),
73                    summary: "Neighboring warlord requests immediate military aid".to_owned(),
74                    context: DecisionContext::new(
75                        "beiyang.aid-request.v1",
76                        json!({
77                            "requester": "warlord-a",
78                            "battle": "ongoing-front",
79                            "common_enemy": true
80                        }),
81                    ),
82                    options: vec![DecisionOption {
83                        action: DecisionAction::None,
84                        utility_inputs: BTreeMap::from([
85                            ("home_defense".to_owned(), 80),
86                            ("alliance".to_owned(), -40),
87                        ]),
88                        ..DecisionOption::new("decline", "Decline aid")
89                    }],
90                    deadline: Some(now + SimDuration::days(2)),
91                },
92            },
93        ),
94    )?;
95    canwu.step_canonical()?.expect("decision intake boundary");
96    Ok(())
97}
98
99fn refresh_aid_options(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
100    canwu.enqueue_decision(
101        canwu.time(),
102        0,
103        DecisionIngressRequest::new(
104            DecisionRequestId::new(3),
105            canwu.revision(),
106            DecisionMutation::ReplaceOptions {
107                ticket_id: DecisionTicketId::new(1),
108                expected_version: 1,
109                context: DecisionContext::new(
110                    "beiyang.aid-request.v1",
111                    json!({
112                        "requester": "warlord-a",
113                        "battle": "ongoing-front",
114                        "common_enemy": true,
115                        "route_confirmed": true
116                    }),
117                ),
118                options: vec![
119                    DecisionOption {
120                        action: DecisionAction::None,
121                        utility_inputs: BTreeMap::from([
122                            ("home_defense".to_owned(), 80),
123                            ("alliance".to_owned(), -40),
124                        ]),
125                        ..DecisionOption::new("decline", "Decline aid")
126                    },
127                    DecisionOption {
128                        action: DecisionAction::Command {
129                            command: serde_json::to_value(Command::OrderMovement {
130                                subject: EntityRef::Army(ids.army),
131                                destination: ids.eastern_territory,
132                                cargo: Vec::new(),
133                            })?,
134                        },
135                        utility_inputs: BTreeMap::from([
136                            ("home_defense".to_owned(), -20),
137                            ("alliance".to_owned(), 90),
138                        ]),
139                        ..DecisionOption::new("send-aid", "Send the neighboring army")
140                    },
141                ],
142            },
143        ),
144    )?;
145    canwu
146        .step_canonical()?
147        .expect("dynamic option refresh boundary");
148    Ok(())
149}
150
151fn resolve_aid_request(canwu: &mut Canwu) -> Result<(), Box<dyn std::error::Error>> {
152    let policy = WeightedUtilityPolicy::new(
153        "aid-utility",
154        "1",
155        UtilityProfile {
156            weights: BTreeMap::from([("alliance".to_owned(), 3), ("home_defense".to_owned(), 1)]),
157        },
158    );
159    let evaluation = canwu.drive_decision(
160        canwu.time(),
161        0,
162        DecisionRequestId::new(4),
163        Some(CommandRequestId::new(1)),
164        DecisionTicketId::new(1),
165        &policy,
166    )?;
167    assert!(matches!(evaluation, DecisionEvaluation::Prepared(_)));
168    canwu
169        .step_canonical()?
170        .expect("decision resolution boundary");
171    Ok(())
172}
Source

pub fn advance( &mut self, duration: SimDuration, ) -> Result<Vec<SimEvent>, CanwuError>

Examples found in repository?
examples/move_army.rs (line 15)
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4    let mut canwu = Canwu::demo(35)?;
5    let ids = Canwu::demo_ids();
6
7    canwu.act(
8        ids.commander,
9        SemanticAction::MoveEntity {
10            subject: EntityRef::Army(ids.army),
11            destination: ids.eastern_territory,
12            cargo: Vec::new(),
13        },
14    )?;
15    let arrival_events = canwu.advance(SimDuration::days(1))?;
16
17    let commander = canwu.observe(ids.commander, &ObserveRequest::default())?;
18    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
19    println!("time: {}", canwu.time());
20    println!("arrival events: {}", arrival_events.len());
21    println!(
22        "commander knows location: {:?}",
23        commander.known_armies[0].known_location
24    );
25    println!(
26        "observer still believes location: {:?}",
27        observer.known_armies[0].known_location
28    );
29
30    canwu.advance(SimDuration::days(3))?;
31    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
32    println!(
33        "observer after report: {:?}",
34        observer.known_armies[0].known_location
35    );
36    Ok(())
37}
Source

pub fn settle_boundary( &mut self, request: BoundaryRequest, ) -> Result<BoundaryReceipt, CanwuError>

Examples found in repository?
examples/phased_boundary.rs (line 155)
149fn main() -> Result<(), CanwuError> {
150    let mut canwu = Canwu::demo(35)?;
151    canwu.register_plugin(&SupplyPlugin)?;
152    canwu.register_plugin(&DemandPlugin)?;
153
154    let receipt = canwu
155        .settle_boundary(BoundaryRequest::at(canwu.time()).with_cadence(SystemCadence::Daily))?;
156    assert_eq!(receipt.allocations[0].granted, 6);
157    assert_eq!(canwu.boundaries()[0].emissions.len(), 2);
158    Ok(())
159}
Source

pub fn wait( &mut self, duration: SimDuration, ) -> Result<Vec<SimEvent>, CanwuError>

Source

pub fn step(&mut self) -> Result<Vec<SimEvent>, CanwuError>

Source

pub fn snapshot(&self) -> SimulationSnapshot

Examples found in repository?
examples/decision_ticket.rs (line 32)
31fn replay_scenario(canwu: &Canwu) -> Scenario {
32    let initial = canwu.snapshot();
33    Scenario {
34        start_time: initial.initial_time,
35        world: initial.world,
36        knowledge: initial.knowledge,
37        domain_records: Vec::new(),
38    }
39}
40
41fn open_aid_request(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
42    let now = canwu.time();
43    let controller = DecisionControllerBinding::new(
44        "warlord-b-ai",
45        DecisionPolicyIdentity::new(DecisionPolicyKind::Utility, "aid-utility", "1"),
46        DecisionAuthority::Actor {
47            actor: ids.commander,
48        },
49    )
50    .with_command_subject(EntityRef::Army(ids.army));
51
52    canwu.enqueue_decision(
53        now,
54        0,
55        DecisionIngressRequest::new(
56            DecisionRequestId::new(1),
57            canwu.revision(),
58            DecisionMutation::RegisterController { controller },
59        ),
60    )?;
61    canwu.enqueue_decision(
62        now,
63        0,
64        DecisionIngressRequest::new(
65            DecisionRequestId::new(2),
66            canwu.revision(),
67            DecisionMutation::Open {
68                ticket: DecisionTicketDraft {
69                    id: DecisionTicketId::new(1),
70                    definition: "beiyang.request-military-aid".to_owned(),
71                    decision_maker: EntityRef::Person(ids.commander),
72                    assigned_controller: "warlord-b-ai".to_owned(),
73                    summary: "Neighboring warlord requests immediate military aid".to_owned(),
74                    context: DecisionContext::new(
75                        "beiyang.aid-request.v1",
76                        json!({
77                            "requester": "warlord-a",
78                            "battle": "ongoing-front",
79                            "common_enemy": true
80                        }),
81                    ),
82                    options: vec![DecisionOption {
83                        action: DecisionAction::None,
84                        utility_inputs: BTreeMap::from([
85                            ("home_defense".to_owned(), 80),
86                            ("alliance".to_owned(), -40),
87                        ]),
88                        ..DecisionOption::new("decline", "Decline aid")
89                    }],
90                    deadline: Some(now + SimDuration::days(2)),
91                },
92            },
93        ),
94    )?;
95    canwu.step_canonical()?.expect("decision intake boundary");
96    Ok(())
97}
98
99fn refresh_aid_options(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
100    canwu.enqueue_decision(
101        canwu.time(),
102        0,
103        DecisionIngressRequest::new(
104            DecisionRequestId::new(3),
105            canwu.revision(),
106            DecisionMutation::ReplaceOptions {
107                ticket_id: DecisionTicketId::new(1),
108                expected_version: 1,
109                context: DecisionContext::new(
110                    "beiyang.aid-request.v1",
111                    json!({
112                        "requester": "warlord-a",
113                        "battle": "ongoing-front",
114                        "common_enemy": true,
115                        "route_confirmed": true
116                    }),
117                ),
118                options: vec![
119                    DecisionOption {
120                        action: DecisionAction::None,
121                        utility_inputs: BTreeMap::from([
122                            ("home_defense".to_owned(), 80),
123                            ("alliance".to_owned(), -40),
124                        ]),
125                        ..DecisionOption::new("decline", "Decline aid")
126                    },
127                    DecisionOption {
128                        action: DecisionAction::Command {
129                            command: serde_json::to_value(Command::OrderMovement {
130                                subject: EntityRef::Army(ids.army),
131                                destination: ids.eastern_territory,
132                                cargo: Vec::new(),
133                            })?,
134                        },
135                        utility_inputs: BTreeMap::from([
136                            ("home_defense".to_owned(), -20),
137                            ("alliance".to_owned(), 90),
138                        ]),
139                        ..DecisionOption::new("send-aid", "Send the neighboring army")
140                    },
141                ],
142            },
143        ),
144    )?;
145    canwu
146        .step_canonical()?
147        .expect("dynamic option refresh boundary");
148    Ok(())
149}
150
151fn resolve_aid_request(canwu: &mut Canwu) -> Result<(), Box<dyn std::error::Error>> {
152    let policy = WeightedUtilityPolicy::new(
153        "aid-utility",
154        "1",
155        UtilityProfile {
156            weights: BTreeMap::from([("alliance".to_owned(), 3), ("home_defense".to_owned(), 1)]),
157        },
158    );
159    let evaluation = canwu.drive_decision(
160        canwu.time(),
161        0,
162        DecisionRequestId::new(4),
163        Some(CommandRequestId::new(1)),
164        DecisionTicketId::new(1),
165        &policy,
166    )?;
167    assert!(matches!(evaluation, DecisionEvaluation::Prepared(_)));
168    canwu
169        .step_canonical()?
170        .expect("decision resolution boundary");
171    Ok(())
172}
173
174fn verify_persistence_and_replay(
175    canwu: &Canwu,
176    replay_scenario: Scenario,
177) -> Result<(), Box<dyn std::error::Error>> {
178    let snapshot = canwu.snapshot();
179    let restored = Canwu::from_snapshot_json(&serde_json::to_string(&snapshot)?)?;
180    assert_eq!(restored.snapshot(), snapshot);
181    let replayed = Canwu::replay_from_journal(replay_scenario, &[], &canwu.replay_journal())?;
182    assert_eq!(replayed.snapshot(), snapshot);
183    Ok(())
184}
Source

pub fn snapshot_json(&self) -> Result<String, CanwuError>

Source

pub fn from_snapshot_json(json: &str) -> Result<Self, CanwuError>

Examples found in repository?
examples/decision_ticket.rs (line 179)
174fn verify_persistence_and_replay(
175    canwu: &Canwu,
176    replay_scenario: Scenario,
177) -> Result<(), Box<dyn std::error::Error>> {
178    let snapshot = canwu.snapshot();
179    let restored = Canwu::from_snapshot_json(&serde_json::to_string(&snapshot)?)?;
180    assert_eq!(restored.snapshot(), snapshot);
181    let replayed = Canwu::replay_from_journal(replay_scenario, &[], &canwu.replay_journal())?;
182    assert_eq!(replayed.snapshot(), snapshot);
183    Ok(())
184}
Source

pub fn from_snapshot_json_with_plugins( json: &str, plugins: &[&dyn SimulationPlugin], ) -> Result<Self, CanwuError>

Source

pub fn from_checkpoint_and_journal( checkpoint: SimulationCheckpoint, segments: Vec<EvidenceJournalSegment>, ) -> Result<Self, CanwuError>

Source

pub fn from_checkpoint_journal( bundle: CheckpointJournal, ) -> Result<Self, CanwuError>

Source

pub fn from_checkpoint_journal_with_plugins( bundle: CheckpointJournal, plugins: &[&dyn SimulationPlugin], ) -> Result<Self, CanwuError>

Source

pub fn from_checkpoint_journal_json(json: &str) -> Result<Self, CanwuError>

Source

pub fn from_checkpoint_journal_json_with_plugins( json: &str, plugins: &[&dyn SimulationPlugin], ) -> Result<Self, CanwuError>

Source

pub fn replay( seed: u64, scenario: Scenario, commands: &[CommandRecord], final_time: SimTime, ) -> Result<Self, CanwuError>

Source

pub fn replay_with_plugins( seed: u64, scenario: Scenario, plugins: &[&dyn SimulationPlugin], commands: &[CommandRecord], final_time: SimTime, ) -> Result<Self, CanwuError>

Source

pub fn replay_with_boundaries( seed: u64, scenario: Scenario, plugins: &[&dyn SimulationPlugin], commands: &[CommandRecord], boundaries: &[BoundaryRecord], final_time: SimTime, ) -> Result<Self, CanwuError>

Source

pub fn replay_with_run_manifest( seed: u64, scenario: Scenario, run_manifest: RunManifest, plugins: &[&dyn SimulationPlugin], commands: &[CommandRecord], boundaries: &[BoundaryRecord], final_time: SimTime, ) -> Result<Self, CanwuError>

Source

pub fn replay_with_run_configuration( seed: u64, scenario: Scenario, run_manifest: RunManifest, run_configuration: RunConfiguration, plugins: &[&dyn SimulationPlugin], commands: &[CommandRecord], command_attempts: &[CommandAttemptRecord], boundaries: &[BoundaryRecord], final_time: SimTime, ) -> Result<Self, CanwuError>

Source

pub fn replay_from_journal( scenario: Scenario, plugins: &[&dyn SimulationPlugin], journal: &ReplayJournal, ) -> Result<Self, CanwuError>

Examples found in repository?
examples/decision_ticket.rs (line 181)
174fn verify_persistence_and_replay(
175    canwu: &Canwu,
176    replay_scenario: Scenario,
177) -> Result<(), Box<dyn std::error::Error>> {
178    let snapshot = canwu.snapshot();
179    let restored = Canwu::from_snapshot_json(&serde_json::to_string(&snapshot)?)?;
180    assert_eq!(restored.snapshot(), snapshot);
181    let replayed = Canwu::replay_from_journal(replay_scenario, &[], &canwu.replay_journal())?;
182    assert_eq!(replayed.snapshot(), snapshot);
183    Ok(())
184}
Source

pub fn fork(&self) -> Self

Source

pub fn diff(&self, other: &Self) -> WorldDiff

Source

pub fn query(&self, query: &Query) -> QueryResult

Source

pub fn query_as( &self, actor: PersonId, query: &Query, ) -> Result<QueryResult, CanwuError>

Source

pub fn admin_query_knowledge( &self, holder: KnowledgeHolderRef, query: &KnowledgeQuery, ) -> Result<KnowledgeQueryResult, CanwuError>

Trusted host/admin holder query. Player-facing callers must use a restricted CanwuViewer.

Source

pub fn viewer(&self) -> Result<CanwuViewer<'_>, CanwuError>

Creates the restricted viewer dictated entirely by the persisted run policy and seat binding.

Source

pub fn viewer_for_actor( &self, actor: PersonId, ) -> Result<CanwuViewer<'_>, CanwuError>

Character-seat and compatibility convenience. It never upgrades an institution, public, research, or developer policy to a person.

Source

pub fn viewer_context( &self, actor: PersonId, ) -> Result<ViewerContext, CanwuError>

Builds an authorized viewer context from the persisted run policy.

Callers cannot select a stronger observation policy through an observation request; the run configuration is the input-control boundary for actor-relative versus research projections.

Source

pub fn observe( &self, actor: PersonId, request: &ObserveRequest, ) -> Result<AgentContext, CanwuError>

Examples found in repository?
examples/move_army.rs (line 17)
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4    let mut canwu = Canwu::demo(35)?;
5    let ids = Canwu::demo_ids();
6
7    canwu.act(
8        ids.commander,
9        SemanticAction::MoveEntity {
10            subject: EntityRef::Army(ids.army),
11            destination: ids.eastern_territory,
12            cargo: Vec::new(),
13        },
14    )?;
15    let arrival_events = canwu.advance(SimDuration::days(1))?;
16
17    let commander = canwu.observe(ids.commander, &ObserveRequest::default())?;
18    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
19    println!("time: {}", canwu.time());
20    println!("arrival events: {}", arrival_events.len());
21    println!(
22        "commander knows location: {:?}",
23        commander.known_armies[0].known_location
24    );
25    println!(
26        "observer still believes location: {:?}",
27        observer.known_armies[0].known_location
28    );
29
30    canwu.advance(SimDuration::days(3))?;
31    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
32    println!(
33        "observer after report: {:?}",
34        observer.known_armies[0].known_location
35    );
36    Ok(())
37}
Source

pub fn observe_with_viewer( &self, viewer: &ViewerContext, request: &ObserveRequest, ) -> Result<AgentContext, CanwuError>

Projects the simulation for a previously authorized viewer context.

The context controls only the player-facing projection. Plugin system subscriptions and state read permissions remain enforced by the simulation runtime and are not widened by this method.

Source

pub fn inspect( &self, actor: PersonId, entity: &EntityRef, detail: DetailLevel, ) -> Result<Inspection, CanwuError>

Source

pub fn available_actions( &self, actor: PersonId, ) -> Result<Vec<AvailableAction>, CanwuError>

Source

pub fn act( &mut self, actor: PersonId, action: SemanticAction, ) -> Result<CommandReceipt, CanwuError>

Examples found in repository?
examples/move_army.rs (lines 7-14)
3fn main() -> Result<(), Box<dyn std::error::Error>> {
4    let mut canwu = Canwu::demo(35)?;
5    let ids = Canwu::demo_ids();
6
7    canwu.act(
8        ids.commander,
9        SemanticAction::MoveEntity {
10            subject: EntityRef::Army(ids.army),
11            destination: ids.eastern_territory,
12            cargo: Vec::new(),
13        },
14    )?;
15    let arrival_events = canwu.advance(SimDuration::days(1))?;
16
17    let commander = canwu.observe(ids.commander, &ObserveRequest::default())?;
18    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
19    println!("time: {}", canwu.time());
20    println!("arrival events: {}", arrival_events.len());
21    println!(
22        "commander knows location: {:?}",
23        commander.known_armies[0].known_location
24    );
25    println!(
26        "observer still believes location: {:?}",
27        observer.known_armies[0].known_location
28    );
29
30    canwu.advance(SimDuration::days(3))?;
31    let observer = canwu.observe(ids.observer, &ObserveRequest::default())?;
32    println!(
33        "observer after report: {:?}",
34        observer.known_armies[0].known_location
35    );
36    Ok(())
37}
Source

pub fn explain(&self, request: &ExplanationRequest) -> Explanation

Source

pub fn describe_capabilities(&self) -> CapabilityDescription

Auto Trait Implementations§

§

impl Freeze for Canwu

§

impl RefUnwindSafe for Canwu

§

impl Send for Canwu

§

impl Sync for Canwu

§

impl Unpin for Canwu

§

impl UnsafeUnpin for Canwu

§

impl UnwindSafe for Canwu

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> 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, 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.