n18route 0.1.0

Searches 18xx maps for optimal route combinations.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
//! Provide a convenient way to construct arbitrary paths.
//!
//! # Example
//!
//! ```rust,no_run
//! # extern crate cairo;
//! # use cairo::{Context, ImageSurface};
//! # use n18hex::{Colour, Hex, Orientation};
//! # use n18map::{Map, Coordinates, Letters, FirstRow};
//! # use n18token::Tokens;
//! # use n18route::builder::Result;
//! use n18route::builder::RouteBuilder;
//! use n18hex::HexFace;
//! use n18brush::highlight_route;
//!
//! # fn main() -> Result<()> {
//! # let hex = Hex::new(125.0);
//! # let tiles: Vec<n18tile::Tile> = vec![];
//! # let orient = Orientation::FlatTop;
//! # let map = Map::new(tiles.into(), Tokens::new(vec![]), vec![], orient);
//! # let surf = cairo::ImageSurface::create(cairo::Format::ARgb32, 10, 10)
//! #     .unwrap();
//! # let ctx = cairo::Context::new(&surf).unwrap();
//! // let hex: n18hex::Hex = ...
//! // let map: n18map::Map = ...
//! // let ctx: cairo::Context = ...
//! // Define the coordinate system.
//! let coords = Coordinates {
//!     orientation: Orientation::FlatTop,
//!     letters: Letters::AsColumns,
//!     first_row: FirstRow::OddColumns,
//! };
//! let a1 = coords.parse("A1").unwrap();
//! let route = RouteBuilder::from_edge(&map, a1, HexFace::LowerRight)?
//!     .to_city(0, true)?
//!     .to_edge(HexFace::Bottom)?
//!     .to_edge(HexFace::Bottom)?
//!     .to_city(0, true)?
//!     .to_edge(HexFace::LowerRight)?
//!     .to_edge(HexFace::UpperRight)?
//!     .to_edge(HexFace::LowerRight)?
//!     .to_city(0, false)?
//!     .to_edge(HexFace::Bottom)?
//!     .into_route();
//!
//! let highlight = Colour::from((179, 25, 25));
//! highlight.apply_colour(&ctx);
//! highlight_route(&hex, &ctx, &map, &route);
//! # Ok(())
//! # }
//! ```
//!

use super::{Path, Route, Step, StopLocation, Visit};
use n18hex::HexFace;
use n18map::{HexAddress, Map};
use n18tile::{Connection, Tile};
use std::collections::BTreeSet;

/// The different ways in which building a path may fail.
pub enum Error {
    /// A string could not be parsed as a valid hex address.
    InvalidHexAddress(String),
    /// There is no tile placed on the specified hex.
    NoTileAtHex(HexAddress),
    /// There is no tile adjacent to the specified hex face.
    NoTileAdjacent(HexAddress, HexFace),
    /// Unable to connect to a city space that does not exist.
    InvalidCity(HexAddress, usize),
    /// Unable to connect to a dit that does not exist.
    InvalidDit(HexAddress, usize),
    /// Unable to find a path to connect two locations on a tile.
    NotConnected(HexAddress, Connection, Connection),
}

/// Shorthand result type for `RouteBuilder` operations.
pub type Result<T> = std::result::Result<T, Error>;

/// Required in order to implement `std::error::Error`.
impl std::fmt::Debug for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        use Error::*;
        match self {
            InvalidHexAddress(s) => write!(f, "Invalid hex address {}", s),
            NoTileAtHex(addr) => write!(f, "No tile at hex {}", addr),
            NoTileAdjacent(addr, face) => {
                write!(f, "No tile adjacent to hex {}, face {:?}", addr, face)
            }
            InvalidCity(addr, ix) => {
                write!(f, "No city #{} at hex {}", ix, addr)
            }
            InvalidDit(addr, ix) => {
                write!(f, "No dit #{} at hex {}", ix, addr)
            }
            NotConnected(addr, _src, dest) => match dest {
                Connection::City { ix } => {
                    write!(f, "No connection to city #{} on hex {}", ix, addr)
                }
                Connection::Dit { ix } => {
                    write!(f, "No connection to dit #{} on hex {}", ix, addr)
                }
                Connection::Face { face } => {
                    write!(f, "No connection to {:?} on hex {}", face, addr)
                }
                Connection::Track { ix, .. } => write!(
                    f,
                    "No connection to track #{} on hex {}",
                    ix, addr
                ),
            },
        }
    }
}

/// Required in order to implement `std::error::Error`.
impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl std::error::Error for Error {}

/// Provides a builder interface for constructing routes.
pub struct RouteBuilder<'a> {
    map: &'a Map,
    steps: Vec<Step>,
    visits: Vec<Visit>,
    num_visits: usize,
    num_cities: usize,
    num_dits: usize,
    num_hexes: usize,
}

fn edge_to_tile_face(
    map: &Map,
    addr: HexAddress,
    face: HexFace,
) -> Result<HexFace> {
    let rotn = map
        .hex_state(addr)
        .ok_or(Error::NoTileAtHex(addr))?
        .rotation();
    let num_cw_turns = rotn.count_turns();
    let mut tile_face = face;
    for _ in 0..num_cw_turns {
        // NOTE: "undo" each clockwise rotation.
        // Switch this to .clockwise() to convert tile faces to map edges.
        tile_face = tile_face.anti_clockwise()
    }
    Ok(tile_face)
}

impl<'a> RouteBuilder<'a> {
    fn new(map: &'a Map, start: Step) -> Self {
        // NOTE: allowing paths to start at a hex face could be useful for,
        // e.g., illustrating conflicting segments of multiple paths.
        let (stop, cities, dits) = match start.conn {
            Connection::City { ix } => {
                (Some(StopLocation::City { ix }), 1, 0)
            }
            Connection::Dit { ix } => (Some(StopLocation::Dit { ix }), 0, 1),
            Connection::Track { .. } => (None, 0, 0),
            Connection::Face { .. } => (None, 0, 0),
        };
        let initial_visit = if let Some(stop) = stop {
            let visit = Visit {
                addr: start.addr,
                // NOTE: visit will only be highlighted by n18brush if they
                // have positive revenue.
                revenue: 1,
                visits: stop,
            };
            vec![visit]
        } else {
            vec![]
        };
        RouteBuilder {
            map,
            steps: vec![start],
            visits: initial_visit,
            num_visits: 1,
            num_cities: cities,
            num_dits: dits,
            num_hexes: 1,
        }
    }

    /// Start building a path from the edge of a tile, where `face` is
    /// specified with respect to the tile's innate orientation, rather than
    /// the tile's rotation on the map.
    pub fn from_tile_face(
        map: &'a Map,
        addr: HexAddress,
        face: HexFace,
    ) -> Result<Self> {
        map.tile_at(addr).ok_or(Error::NoTileAtHex(addr))?;
        let start = Step {
            addr,
            conn: Connection::Face { face },
        };
        Ok(Self::new(map, start))
    }

    /// Start building a path from the edge of a tile, where `face` is
    /// specified with respect to the tile's rotation on the map, rather than
    /// the tile's innate orientation.
    pub fn from_edge(
        map: &'a Map,
        addr: HexAddress,
        face: HexFace,
    ) -> Result<Self> {
        let tile_face = edge_to_tile_face(map, addr, face)?;
        Self::from_tile_face(map, addr, tile_face)
    }

    /// Start building a path from the city space `ix` of a tile.
    pub fn from_city(
        map: &'a Map,
        addr: HexAddress,
        ix: usize,
    ) -> Result<Self> {
        // NOTE: ensure that this initial step is valid.
        let tile = map.tile_at(addr).ok_or(Error::NoTileAtHex(addr))?;
        if tile.cities().len() <= ix {
            return Err(Error::InvalidCity(addr, ix));
        }
        let start = Step {
            addr,
            conn: Connection::City { ix },
        };
        Ok(Self::new(map, start))
    }

    /// Start building a path from the dit `ix` of a tile.
    pub fn from_dit(
        map: &'a Map,
        addr: HexAddress,
        ix: usize,
    ) -> Result<Self> {
        // NOTE: ensure that this initial step is valid.
        let tile = map.tile_at(addr).ok_or(Error::NoTileAtHex(addr))?;
        if tile.dits().len() <= ix {
            return Err(Error::InvalidDit(addr, ix));
        }
        let start = Step {
            addr,
            conn: Connection::Dit { ix },
        };
        Ok(Self::new(map, start))
    }

    /// Extend the path to the edge of the current tile, where `face` is
    /// specified with respect to the tile's innate orientation, and on to the
    /// edge of the adjacent tile (if any).
    // Note: allow this function to consume self, because we're using the "to"
    // prefix to refer to route connectivity, not type conversion.
    #[allow(clippy::wrong_self_convention)]
    pub fn to_tile_face(mut self, face: HexFace) -> Result<Self> {
        // NOTE: this does not account for the tile rotation, it refers to the
        // tile's orientation as defined.
        // NOTE: this also adds a step to the adjacent hex face.
        let curr = self.steps.last().unwrap();
        let mut new_steps =
            self.find_steps_to(curr, Connection::Face { face })?;
        self.steps.append(&mut new_steps);
        self.num_hexes += 1;
        Ok(self)
    }

    /// Extend the path to the edge of the current tile, where `face` is
    /// specified with respect to the tile's rotation on the map, and on to
    /// the edge of the adjacent tile (if any).
    // Note: allow this function to consume self, because we're using the "to"
    // prefix to refer to route connectivity, not type conversion.
    #[allow(clippy::wrong_self_convention)]
    pub fn to_edge(self, face: HexFace) -> Result<Self> {
        // Account for the tile rotation!
        let curr = self.steps.last().unwrap();
        let addr = curr.addr;
        let tile_face = edge_to_tile_face(self.map, addr, face)?;
        self.to_tile_face(tile_face)
    }

    /// Extend the path to the city space `ix` and, optionally, record this as
    /// a "stop" (i.e., earning revenue) so that it will be drawn as such by,
    /// e.g., `n18brush` functions.
    // Note: allow this function to consume self, because we're using the "to"
    // prefix to refer to route connectivity, not type conversion.
    #[allow(clippy::wrong_self_convention)]
    pub fn to_city(mut self, ix: usize, stop: bool) -> Result<Self> {
        let curr = self.steps.last().unwrap();
        // NOTE: need to copy the address for the visit in order to add new
        // steps, or the borrow checker will complain that we have mutable and
        // non-mutable borrows of self.steps.
        let addr = curr.addr;
        let mut new_steps =
            self.find_steps_to(curr, Connection::City { ix })?;
        self.steps.append(&mut new_steps);
        let revenue = if stop { 1 } else { 0 };
        self.num_cities += 1;
        self.num_visits += 1;
        self.visits.push(Visit {
            addr,
            // NOTE: visit will only be highlighted by n18brush if they
            // have positive revenue.
            revenue,
            visits: StopLocation::City { ix },
        });
        Ok(self)
    }

    /// Extend the path to the dit `ix` and, optionally, record this as a
    /// "stop" (i.e., earning revenue) so that it will be drawn as such by,
    /// e.g., `n18brush` functions.
    // Note: allow this function to consume self, because we're using the "to"
    // prefix to refer to route connectivity, not type conversion.
    #[allow(clippy::wrong_self_convention)]
    pub fn to_dit(mut self, ix: usize, stop: bool) -> Result<Self> {
        let curr = self.steps.last().unwrap();
        // NOTE: need to copy the address for the visit in order to add new
        // steps, or the borrow checker will complain that we have mutable and
        // non-mutable borrows of self.steps.
        let addr = curr.addr;
        let mut new_steps =
            self.find_steps_to(curr, Connection::Dit { ix })?;
        self.steps.append(&mut new_steps);
        let revenue = if stop { 1 } else { 0 };
        self.num_dits += 1;
        self.num_visits += 1;
        self.visits.push(Visit {
            addr,
            // NOTE: visit will only be highlighted by n18brush if they
            // have positive revenue.
            revenue,
            visits: StopLocation::Dit { ix },
        });
        Ok(self)
    }

    /// Construct the described path.
    ///
    /// The returned path can be drawn using `n18brush::highlight_path`.
    pub fn into_path(self) -> Path {
        Path {
            steps: self.steps,
            conflicts: BTreeSet::new(),
            route_conflicts: crate::conflict::RouteConflicts::new(),
            visits: self.visits,
            num_visits: self.num_visits,
            num_cities: self.num_cities,
            num_dits: self.num_dits,
            num_hexes: self.num_hexes,
            revenue: 0,
        }
    }

    /// Construct the described route.
    ///
    /// The returned route can be drawn using `n18brush::highlight_route`.
    pub fn into_route(self) -> Route {
        Route {
            steps: self.steps,
            visits: self.visits,
        }
    }

    fn find_steps_to(
        &self,
        src: &Step,
        dest: Connection,
    ) -> Result<Vec<Step>> {
        let mut seen: BTreeSet<Connection> = BTreeSet::new();
        let tile = self
            .map
            .tile_at(src.addr)
            .ok_or(Error::NoTileAtHex(src.addr))?;
        let mut steps = self
            .depth_first_search(
                &mut seen, tile, &src.addr, &src.conn, &dest, 0,
            )
            .ok_or(Error::NotConnected(src.addr, src.conn, dest))?;
        // NOTE: if dest is HexFace, also add the adjacent face on the next hex
        if let Connection::Face { face } = dest {
            let adj = self
                .map
                .adjacent_face(src.addr, face)
                .ok_or(Error::NoTileAdjacent(src.addr, face))?;
            let (adj_addr, adj_face, _adj_tile) = adj;
            let conn = Connection::Face { face: adj_face };
            let step = Step {
                addr: adj_addr,
                conn,
            };
            steps.push(step)
        }
        Ok(steps)
    }

    fn depth_first_search(
        &self,
        seen: &mut BTreeSet<Connection>,
        tile: &Tile,
        addr: &HexAddress,
        src: &Connection,
        dest: &Connection,
        level: usize,
    ) -> Option<Vec<Step>> {
        let mut found: Option<Vec<Step>> = None;
        for conn in tile.connections(src).unwrap() {
            if seen.contains(conn) {
                continue;
            } else {
                seen.insert(*conn)
            };

            // NOTE: if this is a track connection, switch to the other end.
            let other_end = conn.other_end().unwrap_or(*conn);
            let conn = if conn != &other_end {
                if seen.contains(&other_end) {
                    continue;
                } else {
                    seen.insert(other_end);
                    &other_end
                }
            } else {
                conn
            };

            if conn == dest {
                found = Some(vec![Step {
                    addr: *addr,
                    conn: *conn,
                }]);
                break;
            } else {
                let steps_opt = self.depth_first_search(
                    seen,
                    tile,
                    addr,
                    conn,
                    dest,
                    level + 1,
                );
                if let Some(mut steps) = steps_opt {
                    // NOTE: need to insert the source connection.
                    steps.insert(
                        0,
                        Step {
                            addr: *addr,
                            conn: *conn,
                        },
                    );
                    found = Some(steps);
                    break;
                }
            }
        }
        found
    }
}