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, 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_traces()
21        .last()
22        .expect("utility decision trace");
23    println!("{}", serde_json::to_string_pretty(trace)?);
24
25    verify_persistence_and_replay(&canwu)?;
26    println!("snapshot_restore=ok exact_replay=ok");
27    Ok(())
28}
29
30fn open_aid_request(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
31    let now = canwu.time();
32    let controller = DecisionControllerBinding::new(
33        "warlord-b-ai",
34        DecisionPolicyIdentity::new(DecisionPolicyKind::Utility, "aid-utility", "1"),
35        DecisionAuthority::Actor {
36            actor: ids.commander,
37        },
38    )
39    .with_command_subject(EntityRef::Army(ids.army));
40
41    canwu.enqueue_decision(
42        now,
43        0,
44        DecisionIngressRequest::new(
45            DecisionRequestId::new(1),
46            canwu.revision(),
47            DecisionMutation::RegisterController { controller },
48        ),
49    )?;
50    canwu.enqueue_decision(
51        now,
52        0,
53        DecisionIngressRequest::new(
54            DecisionRequestId::new(2),
55            canwu.revision(),
56            DecisionMutation::Open {
57                ticket: DecisionTicketDraft {
58                    id: DecisionTicketId::new(1),
59                    definition: "beiyang.request-military-aid".to_owned(),
60                    decision_maker: EntityRef::Person(ids.commander),
61                    assigned_controller: "warlord-b-ai".to_owned(),
62                    summary: "Neighboring warlord requests immediate military aid".to_owned(),
63                    context: DecisionContext::new(
64                        "beiyang.aid-request.v1",
65                        json!({
66                            "requester": "warlord-a",
67                            "battle": "ongoing-front",
68                            "common_enemy": true
69                        }),
70                    ),
71                    options: vec![DecisionOption {
72                        action: DecisionAction::None,
73                        utility_inputs: BTreeMap::from([
74                            ("home_defense".to_owned(), 80),
75                            ("alliance".to_owned(), -40),
76                        ]),
77                        ..DecisionOption::new("decline", "Decline aid")
78                    }],
79                    deadline: Some(now + SimDuration::days(2)),
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    let restored = Canwu::from_snapshot_json(&serde_json::to_string(&snapshot)?)?;
166    assert_eq!(restored.snapshot(), snapshot);
167    let replayed = Canwu::replay_from_journal(&[], &canwu.replay_journal())?;
168    assert_eq!(replayed.snapshot(), snapshot);
169    Ok(())
170}