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 parent_ticket: None,
80 },
81 },
82 ),
83 )?;
84 canwu.step_canonical()?.expect("decision intake boundary");
85 Ok(())
86}
87
88fn refresh_aid_options(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
89 canwu.enqueue_decision(
90 canwu.time(),
91 0,
92 DecisionIngressRequest::new(
93 DecisionRequestId::new(3),
94 canwu.revision(),
95 DecisionMutation::ReplaceOptions {
96 ticket_id: DecisionTicketId::new(1),
97 expected_version: 1,
98 context: DecisionContext::new(
99 "beiyang.aid-request.v1",
100 json!({
101 "requester": "warlord-a",
102 "battle": "ongoing-front",
103 "common_enemy": true,
104 "route_confirmed": true
105 }),
106 ),
107 options: vec![
108 DecisionOption {
109 action: DecisionAction::None,
110 utility_inputs: BTreeMap::from([
111 ("home_defense".to_owned(), 80),
112 ("alliance".to_owned(), -40),
113 ]),
114 ..DecisionOption::new("decline", "Decline aid")
115 },
116 DecisionOption {
117 action: DecisionAction::Command {
118 command: serde_json::to_value(Command::OrderMovement {
119 subject: EntityRef::Army(ids.army),
120 destination: ids.eastern_territory,
121 cargo: Vec::new(),
122 })?,
123 },
124 utility_inputs: BTreeMap::from([
125 ("home_defense".to_owned(), -20),
126 ("alliance".to_owned(), 90),
127 ]),
128 ..DecisionOption::new("send-aid", "Send the neighboring army")
129 },
130 ],
131 },
132 ),
133 )?;
134 canwu
135 .step_canonical()?
136 .expect("dynamic option refresh boundary");
137 Ok(())
138}
139
140fn resolve_aid_request(canwu: &mut Canwu) -> Result<(), Box<dyn std::error::Error>> {
141 let policy = WeightedUtilityPolicy::new(
142 "aid-utility",
143 "1",
144 UtilityProfile {
145 weights: BTreeMap::from([("alliance".to_owned(), 3), ("home_defense".to_owned(), 1)]),
146 },
147 );
148 let evaluation = canwu.drive_decision(
149 canwu.time(),
150 0,
151 DecisionRequestId::new(4),
152 Some(CommandRequestId::new(1)),
153 DecisionTicketId::new(1),
154 &policy,
155 )?;
156 assert!(matches!(evaluation, DecisionEvaluation::Prepared(_)));
157 canwu
158 .step_canonical()?
159 .expect("decision resolution boundary");
160 Ok(())
161}
162
163fn verify_persistence_and_replay(canwu: &Canwu) -> Result<(), Box<dyn std::error::Error>> {
164 let snapshot = canwu.snapshot();
165 verify_snapshot_round_trip(&snapshot)?;
166 verify_journal_replay(canwu, &snapshot)
167}
168
169fn verify_snapshot_round_trip(
170 snapshot: &canwu_api::SimulationSnapshot,
171) -> Result<(), Box<dyn std::error::Error>> {
172 let restored = Canwu::from_snapshot_json(&serde_json::to_string(&snapshot)?)?;
173 assert_eq!(restored.snapshot(), *snapshot);
174 Ok(())
175}
176
177fn verify_journal_replay(
178 canwu: &Canwu,
179 snapshot: &canwu_api::SimulationSnapshot,
180) -> Result<(), Box<dyn std::error::Error>> {
181 let replayed = Canwu::replay_from_journal(&[], &canwu.replay_journal())?;
182 assert_eq!(replayed.snapshot(), *snapshot);
183 Ok(())
184}