Skip to main content

decision_ticket/
decision_ticket.rs

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}