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, Scenario, SimDuration,
6 UtilityProfile, 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 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}
30
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}