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_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}