radip/
lib.rs

1//! DATC-compliant\* diplomacy adjucator.
2//!
3//! Use [`adjudicate`] to adjudicate a movement phase, and [`utils::apply_adjudication`] to update the map.
4//! This crate does not support build phases, as those are fairly easy to implement on your own, and
5//! build phases may differ for different variants.
6
7use std::{
8    any::Any,
9    collections::{HashMap, HashSet, VecDeque},
10    fmt::Debug,
11    ops::Deref,
12};
13
14pub mod base;
15pub mod core;
16pub mod utils;
17
18mod test;
19mod paradox;
20
21/// Abbreviation for a province (e.g. NTH, Lvn).
22pub type ProvinceAbbr = String;
23
24/// A fleet location. The first element of the tuple is the province, the second is the coast.
25pub type FleetLoc = (ProvinceAbbr, String);
26
27/// An army location.
28pub type ArmyLoc = ProvinceAbbr;
29
30use serde::{Deserialize, Serialize};
31
32/// Province metadata stored in a [`Map`].
33#[derive(Clone, Serialize, Deserialize)]
34pub struct Province {
35    pub coasts: HashSet<String>,
36    pub is_sea: bool,
37}
38
39/// A variant map.
40#[derive(Clone, Serialize, Deserialize)]
41pub struct Map {
42    pub provinces: HashMap<ProvinceAbbr, Province>,
43    pub fleet_adj: HashSet<(FleetLoc, FleetLoc)>,
44    pub army_adj: HashSet<(ArmyLoc, ArmyLoc)>,
45}
46
47impl Map {
48    /// The classic map.
49    pub fn classic() -> Self {
50        let default = include_str!("../data/classic.json");
51
52        serde_json::from_str(default).unwrap()
53    }
54}
55
56/// Stores the units present on a diplomacy board.
57#[derive(Clone, Debug, Serialize, Deserialize)]
58pub struct MapState {
59    pub units: HashMap<String, Unit>,
60
61    /// Tracks SC ownership.
62    /// This field is not used in the core adjudicator;
63    /// it is included for convenience.
64    pub ownership: HashMap<ProvinceAbbr, String>,
65}
66
67#[derive(Clone, Debug, Serialize, Deserialize)]
68#[serde(tag = "type", content = "data", rename_all = "lowercase")]
69pub enum Unit {
70    Army(String),
71    Fleet(String, String),
72}
73
74impl Unit {
75    pub fn nationality(&self) -> String {
76        match self {
77            Unit::Army(s) => s.clone(),
78            Unit::Fleet(s, _) => s.clone(),
79        }
80    }
81}
82
83/// Map of orders. Each province occupied by a unit have
84/// an associated order.
85pub type Orders = HashMap<String, Box<dyn Order>>;
86
87/// Helper trait.
88pub trait AsAny {
89    fn as_any(&self) -> &dyn Any;
90}
91
92impl<T: Any> AsAny for T {
93    fn as_any(&self) -> &dyn Any {
94        self
95    }
96}
97
98/// Represents an order object.
99///
100/// Downcasting is supported via [`Any::downcast_ref`](std::any::Any::downcast_ref) and [`Any::is`](std::any::Any::is).
101#[typetag::serde(tag = "type")]
102pub trait Order: 'static + Debug + AsAny + Send + Sync {
103    /// Return orders (identified by source province) that this order depends on
104    /// for resolution.
105    fn deps(
106        &self,
107        map: &Map,
108        state: &MapState,
109        orders: &Orders,
110        this_prov: &str,
111    ) -> HashSet<String>;
112
113    /// Based on the given `order_status` information, determine whether
114    /// this order succeeds or fails.
115    fn adjudicate(
116        &self,
117        map: &Map,
118        state: &MapState,
119        orders: &Orders,
120        this_prov: &str,
121        order_status: &HashMap<String, bool>,
122    ) -> Option<bool>;
123
124    fn as_owned(&self) -> Box<dyn Order>;
125}
126
127impl Deref for dyn Order {
128    type Target = dyn Any;
129    fn deref(&self) -> &Self::Target {
130        self.as_any()
131    }
132}
133
134impl Clone for Box<dyn Order> {
135    fn clone(&self) -> Self {
136        self.as_owned()
137    }
138}
139
140/// Adjudicate a movement phase.
141pub fn adjudicate(map: &Map, state: &MapState, orders: &Orders) -> HashMap<String, bool> {
142    let mut order_status: HashMap<String, bool> = HashMap::new();
143    loop {
144        if order_status.len() == orders.len() {
145            break;
146        }
147
148        let num_resolved = order_status.len();
149
150        for (prov, order) in orders.iter() {
151            if order_status.contains_key(prov) {
152                continue;
153            }
154
155            let deps = order.deps(&map, &state, &orders, prov);
156            let mut restricted_order_status = HashMap::new();
157            for dep_prov in deps {
158                if order_status.contains_key(&dep_prov) {
159                    let status = order_status[&dep_prov];
160                    restricted_order_status.insert(dep_prov, status);
161                }
162            }
163
164            match order.adjudicate(&map, &state, &orders, prov, &restricted_order_status) {
165                Some(status) => {
166                    order_status.insert(prov.to_string(), status);
167                }
168                None => {}
169            }
170        }
171
172        if order_status.len() != num_resolved {
173            // skip paradox step if an order was resolved
174            continue;
175        }
176
177        // paradoxes
178    
179        paradox::handle_cycles(map, state, orders, &mut order_status);
180        if order_status.len() != num_resolved {
181            continue;
182        }
183
184        paradox::handle_convoy(map, state, orders, &mut order_status);
185        if order_status.len() != num_resolved {
186            continue;
187        }
188
189        break;
190    }
191
192    order_status
193}