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
use crate::city::City;
use crate::track::{Track, TrackEnd};
use n18hex::{Hex, HexFace};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Connection {
Track { ix: usize, end: TrackEnd },
Dit { ix: usize },
City { ix: usize },
Face { face: HexFace },
}
impl From<HexFace> for Connection {
fn from(face: HexFace) -> Self {
Connection::Face { face }
}
}
impl Connection {
/// Returns whether this connection is equivalent to another connection.
///
/// This is less restrictive than equality as defined by `std::cmp::Eq`,
/// because connections to either end of the same track segment are
/// considered equivalent **but are not equal to each other**.
pub fn equivalent_to(&self, other: &Self) -> bool {
use Connection::*;
match (self, other) {
// NOTE: track connections are equivalent regardless of direction.
(Track { ix: a, .. }, Track { ix: b, .. }) => a == b,
(Dit { ix: a }, Dit { ix: b }) => a == b,
(City { ix: a }, City { ix: b }) => a == b,
(Face { face: a }, Face { face: b }) => a == b,
_ => false,
}
}
/// Returns the connection at the other end of a track segment, or `None`
/// if the provided connection is not a track segment.
pub fn other_end(&self) -> Option<Self> {
use Connection::*;
match self {
Track { ix, end } => Some(Track {
ix: *ix,
end: end.other_end(),
}),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Dit {
pub track_ix: usize,
end: TrackEnd,
pub revenue: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Connections {
// NOTE: dits are drawn by the track segment that "owns" them, but for
// connectivity purposes they are separate entities and so they should
// also be stored separately; here they are stored by index (0..N).
dits: Vec<Dit>,
track: BTreeMap<(usize, TrackEnd), Vec<Connection>>,
face: BTreeMap<HexFace, Vec<Connection>>,
city: BTreeMap<usize, Vec<Connection>>,
dit: BTreeMap<usize, Vec<Connection>>,
none: Vec<Connection>,
}
impl Connections {
// - Track ends may connect to a hex face, a dit, or a city.
// - If they cross another track, the tracks are not connected.
// - If they cross a dit or a city, this is an ERROR.
//
// Tracks with dits - the dit must be at one end or the other, and we
// can calculate the dit angle so that we need never look it up again.
// Then it really becomes a separate entity from the track.
//
// The (de)serialisation layer could possibly break apart a single
// track as defined in the existing setup (which may span a city or
// dit) into two track segments, and maybe piece these two back
// together when serialising? Could be really messy and inconsistent
// though. But the round-tripping would be nice!
pub fn new(tracks: &[Track], cities: &[City], hex: &Hex) -> Self {
let mut dits = vec![];
let mut track_conns = BTreeMap::new();
let mut face_conns = BTreeMap::new();
let mut dit_conns = BTreeMap::new();
let mut city_conns = BTreeMap::new();
let ctx = hex.context();
for i in 0..tracks.len() {
let track = tracks[i];
// Record connections between this track and hex faces.
for (end, face) in track.connected_to_faces() {
face_conns
.entry(face)
.or_insert_with(Vec::new)
.push(Connection::Track { ix: i, end });
track_conns
.entry((i, end))
.or_insert_with(Vec::new)
.push(Connection::Face { face });
}
if let Some((dit_end, revenue, _shape)) = track.dit {
// Record the connection between this track and the dit at one
// of its end.
let dit_ix = dits.len();
dits.push(Dit {
track_ix: i,
end: dit_end,
revenue,
});
dit_conns.entry(dit_ix).or_insert_with(Vec::new).push(
Connection::Track {
ix: i,
end: dit_end,
},
);
track_conns
.entry((i, dit_end))
.or_insert_with(Vec::new)
.push(Connection::Dit { ix: dit_ix });
// NOTE: Also connect this dit to any track segments that are
// connected to this end of the track.
for (j, other) in tracks.iter().enumerate() {
if j == i {
continue;
}
let conn_opt = track.connected_at(other, hex, ctx);
if let Some((conn_end, other_end)) = conn_opt {
if conn_end == dit_end {
dit_conns
.entry(dit_ix)
.or_insert_with(Vec::new)
.push(Connection::Track {
ix: j,
end: other_end,
});
track_conns
.entry((j, other_end))
.or_insert_with(Vec::new)
.push(Connection::Dit { ix: dit_ix });
}
}
}
}
}
for (cx, city) in cities.iter().enumerate() {
for (i, track) in tracks.iter().enumerate() {
let end_opt = track.connected_to_fill_at(city, hex, ctx);
if let Some(end) = end_opt {
city_conns
.entry(cx)
.or_insert_with(Vec::new)
.push(Connection::Track { ix: i, end });
track_conns
.entry((i, end))
.or_insert_with(Vec::new)
.push(Connection::City { ix: cx });
}
}
}
// Track segments are not connected to each other, their ends must
// only connect to other entities: hex faces, dits, and cities.
// Once all connections between each track segment and other entities
// (hex faces, dits, cities) have been recorded, check whether any
// track segment has an end with no connections, but is connected to
// another track segment, and print a warning message.
for i in 0..tracks.len() {
let track = tracks[i];
// Check whether each end of this track segment has connections.
let start_conns = track_conns.contains_key(&(i, TrackEnd::Start));
let end_conns = track_conns.contains_key(&(i, TrackEnd::End));
if !(start_conns && end_conns) {
for (j, other) in tracks.iter().enumerate().skip(i + 1) {
if track.connected(other, hex, ctx) {
println!("WARNING: tracks {} and {} connect", i, j);
}
}
}
}
Connections {
dits,
track: track_conns,
face: face_conns,
city: city_conns,
dit: dit_conns,
none: vec![],
}
}
pub fn dits(&self) -> &[Dit] {
&self.dits
}
pub fn from(&self, from: &Connection) -> Option<&[Connection]> {
use Connection::*;
let conns_opt = match from {
Track { ix, end } => {
let key = (*ix, *end);
self.track.get(&key)
}
Dit { ix } => self.dit.get(ix),
City { ix } => self.city.get(ix),
Face { face } => self.face.get(face),
};
conns_opt.map(|cs| cs.as_slice())
}
/// Returns all connections that can be reached from `start`.
///
/// Note that this returns a collection rather than an iterator because it
/// must record every visited connection to avoid repetition, and so there
/// is no gain to collect all of the visited connections and then discard
/// them.
pub fn connections_from(
&self,
start: &Connection,
) -> BTreeSet<Connection> {
let mut visited: BTreeSet<Connection> = BTreeSet::new();
let mut to_visit: Vec<&Connection> = match self.from(start) {
Some(conns) => conns.iter().collect(),
None => vec![],
};
while !to_visit.is_empty() {
let conn = to_visit.pop().unwrap();
if visited.contains(conn) {
continue;
}
visited.insert(*conn);
// NOTE: if this is one end of a track segment, continue exploring
// from the other end, making sure to record both ends.
let conn = if let Some(new_conn) = conn.other_end() {
if visited.contains(&new_conn) {
continue;
}
visited.insert(new_conn);
new_conn
} else {
*conn
};
// NOTE: stop exploring once we reach the tile edge.
if let Connection::Face { .. } = conn {
continue;
}
if let Some(conns) = self.from(&conn) {
for next_conn in conns {
to_visit.push(next_conn)
}
}
}
visited
}
}
#[cfg(test)]
/// Tests that check whether connections are defined appropriately.
mod tests {
use crate::*;
use n18hex::{Hex, HexColour::*, HexCorner::*, HexFace::*};
static HEX_DIAMETER: f64 = 150.0;
#[test]
/// Tile 5 contains a city with a single token space, and track segments
/// that run from this city to the bottom and lower-right hex faces.
fn test_single_tile_5() {
let tile_name = "5";
let hex = Hex::new(HEX_DIAMETER);
let tile = Tile::new(
Yellow,
tile_name,
vec![Track::mid(Bottom), Track::mid(LowerRight)],
vec![City::single(20)],
&hex,
)
.label(Label::Revenue(0), TopLeft.to_centre(0.3));
let city_ix = 0;
// Find connections from this tile's only city.
let from = Connection::City { ix: city_ix };
let conns = tile.connections(&from);
assert!(conns.is_some());
let conns = conns.unwrap();
assert_eq!(conns.len(), 2);
// Ensure that each connection is a Track segment.
for conn in conns {
match conn {
Connection::Track { ix, end: _ } => {
assert!(*ix == 0 || *ix == 1);
// Ensure the other end of the track connects to a face.
let other_end = conn.other_end();
assert!(other_end.is_some());
let conn = other_end.unwrap();
let track_conns = tile.connections(&conn);
assert!(track_conns.is_some());
let track_conns = track_conns.unwrap();
assert_eq!(track_conns.len(), 1);
let track_conn = track_conns[0];
if let Connection::Face { face } = track_conn {
if face != Bottom && face != LowerRight {
panic!(
"Unexpected 2nd connection {:?}",
track_conn
);
}
} else {
panic!("Unexpected 2nd connection {:?}", track_conn);
}
}
_ => {
panic!("Unexpected connection: {:?} is not a track", conn)
}
}
}
}
}