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
7pub enum ConvoyRouteError {
9 CanOnlyConvoyArmy,
11
12 CanOnlyConvoyMove,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub enum ConvoyOutcome<O> {
20 NotAtSea,
22 Dislodged(O),
24 Paradox,
26 NotDisrupted,
28}
29
30impl<O> ConvoyOutcome<O> {
31 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
59fn 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
84fn 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 !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 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
116pub 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 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
148pub 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
162pub 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}