canwu-api 0.5.1

Public Rust facade for the Canwu deterministic historical simulation engine
Documentation
use canwu_api::{
    Canwu, Command, CommandRequestId, DecisionAction, DecisionAuthority, DecisionContext,
    DecisionControllerBinding, DecisionEvaluation, DecisionIngressRequest, DecisionMutation,
    DecisionOption, DecisionPolicyIdentity, DecisionPolicyKind, DecisionRequestId,
    DecisionTicketDraft, DecisionTicketId, DemoIds, EntityRef, Scenario, SimDuration,
    UtilityProfile, WeightedUtilityPolicy,
};
use serde_json::json;
use std::collections::BTreeMap;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut canwu = Canwu::demo(1918)?;
    let replay_scenario = replay_scenario(&canwu);
    let ids = Canwu::demo_ids();

    open_aid_request(&mut canwu, ids)?;
    refresh_aid_options(&mut canwu, ids)?;
    resolve_aid_request(&mut canwu)?;

    let trace = canwu
        .decision_traces()
        .last()
        .expect("utility decision trace");
    println!("{}", serde_json::to_string_pretty(trace)?);

    verify_persistence_and_replay(&canwu, replay_scenario)?;
    println!("snapshot_restore=ok exact_replay=ok");
    Ok(())
}

fn replay_scenario(canwu: &Canwu) -> Scenario {
    let initial = canwu.snapshot();
    Scenario {
        start_time: initial.initial_time,
        world: initial.world,
        knowledge: initial.knowledge,
        domain_records: Vec::new(),
    }
}

fn open_aid_request(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
    let now = canwu.time();
    let controller = DecisionControllerBinding::new(
        "warlord-b-ai",
        DecisionPolicyIdentity::new(DecisionPolicyKind::Utility, "aid-utility", "1"),
        DecisionAuthority::Actor {
            actor: ids.commander,
        },
    )
    .with_command_subject(EntityRef::Army(ids.army));

    canwu.enqueue_decision(
        now,
        0,
        DecisionIngressRequest::new(
            DecisionRequestId::new(1),
            canwu.revision(),
            DecisionMutation::RegisterController { controller },
        ),
    )?;
    canwu.enqueue_decision(
        now,
        0,
        DecisionIngressRequest::new(
            DecisionRequestId::new(2),
            canwu.revision(),
            DecisionMutation::Open {
                ticket: DecisionTicketDraft {
                    id: DecisionTicketId::new(1),
                    definition: "beiyang.request-military-aid".to_owned(),
                    decision_maker: EntityRef::Person(ids.commander),
                    assigned_controller: "warlord-b-ai".to_owned(),
                    summary: "Neighboring warlord requests immediate military aid".to_owned(),
                    context: DecisionContext::new(
                        "beiyang.aid-request.v1",
                        json!({
                            "requester": "warlord-a",
                            "battle": "ongoing-front",
                            "common_enemy": true
                        }),
                    ),
                    options: vec![DecisionOption {
                        action: DecisionAction::None,
                        utility_inputs: BTreeMap::from([
                            ("home_defense".to_owned(), 80),
                            ("alliance".to_owned(), -40),
                        ]),
                        ..DecisionOption::new("decline", "Decline aid")
                    }],
                    deadline: Some(now + SimDuration::days(2)),
                },
            },
        ),
    )?;
    canwu.step_canonical()?.expect("decision intake boundary");
    Ok(())
}

fn refresh_aid_options(canwu: &mut Canwu, ids: DemoIds) -> Result<(), Box<dyn std::error::Error>> {
    canwu.enqueue_decision(
        canwu.time(),
        0,
        DecisionIngressRequest::new(
            DecisionRequestId::new(3),
            canwu.revision(),
            DecisionMutation::ReplaceOptions {
                ticket_id: DecisionTicketId::new(1),
                expected_version: 1,
                context: DecisionContext::new(
                    "beiyang.aid-request.v1",
                    json!({
                        "requester": "warlord-a",
                        "battle": "ongoing-front",
                        "common_enemy": true,
                        "route_confirmed": true
                    }),
                ),
                options: vec![
                    DecisionOption {
                        action: DecisionAction::None,
                        utility_inputs: BTreeMap::from([
                            ("home_defense".to_owned(), 80),
                            ("alliance".to_owned(), -40),
                        ]),
                        ..DecisionOption::new("decline", "Decline aid")
                    },
                    DecisionOption {
                        action: DecisionAction::Command {
                            command: serde_json::to_value(Command::OrderMovement {
                                subject: EntityRef::Army(ids.army),
                                destination: ids.eastern_territory,
                                cargo: Vec::new(),
                            })?,
                        },
                        utility_inputs: BTreeMap::from([
                            ("home_defense".to_owned(), -20),
                            ("alliance".to_owned(), 90),
                        ]),
                        ..DecisionOption::new("send-aid", "Send the neighboring army")
                    },
                ],
            },
        ),
    )?;
    canwu
        .step_canonical()?
        .expect("dynamic option refresh boundary");
    Ok(())
}

fn resolve_aid_request(canwu: &mut Canwu) -> Result<(), Box<dyn std::error::Error>> {
    let policy = WeightedUtilityPolicy::new(
        "aid-utility",
        "1",
        UtilityProfile {
            weights: BTreeMap::from([("alliance".to_owned(), 3), ("home_defense".to_owned(), 1)]),
        },
    );
    let evaluation = canwu.drive_decision(
        canwu.time(),
        0,
        DecisionRequestId::new(4),
        Some(CommandRequestId::new(1)),
        DecisionTicketId::new(1),
        &policy,
    )?;
    assert!(matches!(evaluation, DecisionEvaluation::Prepared(_)));
    canwu
        .step_canonical()?
        .expect("decision resolution boundary");
    Ok(())
}

fn verify_persistence_and_replay(
    canwu: &Canwu,
    replay_scenario: Scenario,
) -> Result<(), Box<dyn std::error::Error>> {
    let snapshot = canwu.snapshot();
    let restored = Canwu::from_snapshot_json(&serde_json::to_string(&snapshot)?)?;
    assert_eq!(restored.snapshot(), snapshot);
    let replayed = Canwu::replay_from_journal(replay_scenario, &[], &canwu.replay_journal())?;
    assert_eq!(replayed.snapshot(), snapshot);
    Ok(())
}