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