diplomacy/judge/
convoy.rs

1use super::{Adjudicate, Context, MappedMainOrder, OrderState, ResolverState};
2use crate::geo::{Map, ProvinceKey, RegionKey, Terrain};
3use crate::judge::WillUseConvoy;
4use crate::order::{Command, MainCommand};
5use crate::{UnitPosition, UnitType};
6
7/// Failure cases for convoy route lookup.
8pub enum ConvoyRouteError {
9    /// Only armies can be convoyed.
10    CanOnlyConvoyArmy,
11
12    /// Hold, support, and convoy orders cannot be convoyed.
13    CanOnlyConvoyMove,
14}
15
16/// The outcome of a convoy order.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum ConvoyOutcome<O> {
20    /// The convoy order is invalid because the convoying unit is not at sea.
21    NotAtSea,
22    /// The convoying unit was dislodged by another move
23    Dislodged(O),
24    /// The convoy was failed to resolve a paradox
25    Paradox,
26    /// The convoy was not disrupted. This doesn't mean the move necessarily succeeded.
27    NotDisrupted,
28}
29
30impl<O> ConvoyOutcome<O> {
31    /// Apply a function to any orders referenced by `self`, returning a new outcome.
32    pub fn map_order<U>(self, map_fn: impl Fn(O) -> U) -> ConvoyOutcome<U> {
33        use ConvoyOutcome::*;
34        match self {
35            NotAtSea => NotAtSea,
36            Dislodged(by) => Dislodged(map_fn(by)),
37            Paradox => Paradox,
38            NotDisrupted => NotDisrupted,
39        }
40    }
41}
42
43impl<O> From<&'_ ConvoyOutcome<O>> for OrderState {
44    fn from(other: &ConvoyOutcome<O>) -> Self {
45        if matches!(other, ConvoyOutcome::NotDisrupted) {
46            OrderState::Succeeds
47        } else {
48            OrderState::Fails
49        }
50    }
51}
52
53impl<O> From<ConvoyOutcome<O>> for OrderState {
54    fn from(other: ConvoyOutcome<O>) -> Self {
55        (&other).into()
56    }
57}
58
59/// Checks whether `convoy` is a valid convoy that will carry `mv_ord` from
60/// its current location to the destination.
61fn is_convoy_for(convoy: &MappedMainOrder, mv_ord: &MappedMainOrder) -> bool {
62    match &convoy.command {
63        MainCommand::Convoy(cm) => cm == mv_ord,
64        _ => false,
65    }
66}
67
68trait RouteStep: Eq + Clone {
69    fn region(&self) -> &RegionKey;
70}
71
72impl RouteStep for &MappedMainOrder {
73    fn region(&self) -> &RegionKey {
74        &self.region
75    }
76}
77
78impl<'a> RouteStep for UnitPosition<'a> {
79    fn region(&self) -> &RegionKey {
80        self.region
81    }
82}
83
84/// Find all routes from `origin` to `dest` given a set of valid convoys.
85fn route_steps<R: RouteStep>(
86    map: &Map,
87    convoys: &[R],
88    origin: &ProvinceKey,
89    dest: &ProvinceKey,
90    working_path: Vec<R>,
91) -> Vec<Vec<R>> {
92    let adjacent_regions = map.find_bordering(origin);
93    // if we've got a convoy going and there is one hop to the destination,
94    // we've found a valid solution.
95    if !working_path.is_empty() && adjacent_regions.iter().any(|&r| r == dest) {
96        vec![working_path]
97    } else {
98        let mut paths = vec![];
99        for convoy in convoys {
100            // move to adjacent, and don't allow backtracking/cycles
101            if !working_path.contains(convoy) && adjacent_regions.contains(&convoy.region()) {
102                let mut next_path = working_path.clone();
103                next_path.push(convoy.clone());
104                let mut steps =
105                    route_steps(map, convoys, convoy.region().province(), dest, next_path);
106                if !steps.is_empty() {
107                    paths.append(&mut steps);
108                }
109            }
110        }
111
112        paths
113    }
114}
115
116/// Finds all valid convoy routes for a given move order.
117pub fn routes<'a>(
118    ctx: &Context<'a, impl Adjudicate>,
119    state: &mut ResolverState<'a>,
120    mv_ord: &MappedMainOrder,
121) -> Result<Vec<Vec<&'a MappedMainOrder>>, ConvoyRouteError> {
122    if mv_ord.unit_type == UnitType::Fleet {
123        Err(ConvoyRouteError::CanOnlyConvoyArmy)
124    } else if let Some(dst) = mv_ord.move_dest() {
125        // Get the convoy orders that can ferry the provided move order and are
126        // successful. Per http://uk.diplom.org/pouch/Zine/S2009M/Kruijswijk/DipMath_Chp6.htm
127        // we resolve all convoy orders eagerly to avoid wild recursion during the depth-first
128        // search.
129        let mut convoy_steps = vec![];
130        for order in ctx.orders() {
131            if is_convoy_for(order, mv_ord) && state.resolve(ctx, order).into() {
132                convoy_steps.push(order);
133            }
134        }
135
136        Ok(route_steps(
137            ctx.world_map,
138            &convoy_steps,
139            mv_ord.region.province(),
140            dst.province(),
141            vec![],
142        ))
143    } else {
144        Err(ConvoyRouteError::CanOnlyConvoyMove)
145    }
146}
147
148/// Determines if any valid convoy route exists for the given move order.
149pub fn uses_convoy<'a>(
150    ctx: &Context<'a, impl Adjudicate + WillUseConvoy>,
151    state: &mut ResolverState<'a>,
152    mv_ord: &MappedMainOrder,
153) -> bool {
154    let Ok(r) = routes(ctx, state, mv_ord) else {
155        return false;
156    };
157
158    r.iter()
159        .any(|route| !route.is_empty() && ctx.rules.will_use_convoy(mv_ord, route))
160}
161
162/// Checks if a convoy route may exist for an order, based on the positions
163/// of fleets, the move order's source region, and the destination region.
164///
165/// This is used before adjudication to identify illegal orders, so it does
166/// not take in a full context.
167pub fn route_may_exist<'a>(
168    map: &'a Map,
169    unit_positions: impl IntoIterator<Item = UnitPosition<'a>>,
170    mv_ord: &MappedMainOrder,
171) -> bool {
172    if mv_ord.unit_type == UnitType::Fleet {
173        return false;
174    }
175
176    let Some(dst) = mv_ord.move_dest() else {
177        return false;
178    };
179
180    let fleets = unit_positions
181        .into_iter()
182        .filter(|u| {
183            u.unit.unit_type() == UnitType::Fleet
184                && map
185                    .find_region(&u.region.to_string())
186                    .map(|r| r.terrain() == Terrain::Sea)
187                    .unwrap_or(false)
188        })
189        .collect::<Vec<_>>();
190
191    let steps = route_steps(
192        map,
193        &fleets,
194        mv_ord.region.province(),
195        dst.province(),
196        vec![],
197    );
198
199    !steps.is_empty()
200}
201
202#[cfg(test)]
203mod test {
204    use crate::UnitType;
205    use crate::geo::{self, ProvinceKey, RegionKey};
206    use crate::judge::MappedMainOrder;
207    use crate::order::{ConvoyedMove, Order};
208
209    fn convoy(l: &str, f: &str, t: &str) -> MappedMainOrder {
210        Order::new(
211            "eng".into(),
212            UnitType::Fleet,
213            RegionKey::new(String::from(l), None),
214            ConvoyedMove::new(
215                RegionKey::new(String::from(f), None),
216                RegionKey::new(String::from(t), None),
217            )
218            .into(),
219        )
220    }
221
222    #[test]
223    fn pathfinder() {
224        let convoys = vec![
225            convoy("ska", "lon", "swe"),
226            convoy("eng", "lon", "swe"),
227            convoy("nth", "lon", "swe"),
228            convoy("nwg", "lon", "swe"),
229        ];
230
231        let routes = super::route_steps(
232            geo::standard_map(),
233            &convoys.iter().collect::<Vec<_>>(),
234            &ProvinceKey::new("lon"),
235            &ProvinceKey::new("swe"),
236            vec![],
237        );
238        for r in &routes {
239            println!("CHAIN");
240            for o in r.iter() {
241                println!("  {}", o);
242            }
243        }
244
245        assert_eq!(2, routes.len());
246    }
247}