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
use std::collections::HashSet;
use std::num::NonZero;
use std::ops::IndexMut;
use itertools::Itertools;
use ndarray::{Array2, AssignElem};
use petgraph::graphmap::UnGraphMap;
use unordered_pair::UnorderedPair;
use crate::board::{Board, Edge, Node};
use crate::cell::Cell;
use crate::location::{Dimension, Location};
use crate::shape::{FullShape, SquareStep, Shape};
/// Reasons a builder may become invalid while building.
#[derive(Copy, Clone, Debug)]
pub enum BuilderInvalidReason {
/// A feature like a bridge was inserted outside the bounds specified by `dims` on a builder.
FeatureOutOfBounds,
/// A warp was inserted in a direction which does not make sense; e.g. attempting to place warp on the right edge in the up direction.
WarpBadDirection,
}
/// Functionality all builders must implement, parametrised over the grid shape `Sh` of the resulting board.
///
/// Builders mutate themselves while building but can be [`Clone`]d to save their state at some point.
pub trait Builder<Sh: FullShape>: Clone {
/// Construct a new [`Self`] with the specified dimensions, specified in `(x, y)` order.
fn with_dims(dims: (Dimension, Dimension)) -> Self;
/// Add termini or "flow endpoints". The order in which `locations` are specified does not matter.
///
/// May cause the builder to enter a [`FeatureOutOfBounds`](BuilderInvalidReason::FeatureOutOfBounds) invalid state if either location is out of bounds.
/// If the builder is already in an invalid state, this function does nothing.
fn add_termini(&mut self, display: char, locations: (Location, Location)) -> &mut Self;
/// Remove the most recently added pair of termini.
///
/// If the builder is in an invalid state or no termini are present, this function does nothing.
fn pop_termini(&mut self) -> &mut Self;
/// Add a bridge at the specified `location`.
///
/// A bridge allows paths to enter and exit independently of one another, all passing through the same location.
/// Paths must not change direction while moving through the bridge.
///
/// May cause the builder to enter a [`FeatureOutOfBounds`](BuilderInvalidReason::FeatureOutOfBounds) invalid state if `location` is out of bounds.
/// If the builder is already in an invalid state, this function does nothing.
fn add_bridge(&mut self, location: Location) -> &mut Self;
/// Drop a location from the board.
/// Any feature here, such as a bridge, warp, or terminus, will be deleted regardless of where in the chain this method is called.
/// Keep in mind this may cause the board to become unsolvable.
///
/// May cause the builder to enter a [`FeatureOutOfBounds`](BuilderInvalidReason::FeatureOutOfBounds) invalid state if `location` is out of bounds.
/// If the builder is already in an invalid state, this function does nothing.
fn drop_location(&mut self, location: Location) -> &mut Self;
/// Disconnect the two `locations`, i.e. place a wall between them.
///
/// A wall prevents paths from crossing it.
/// If the two locations are not adjacent, this function does nothing and does not invalidate the builder.
///
/// May cause the builder to enter a [`FeatureOutOfBounds`](BuilderInvalidReason::FeatureOutOfBounds) invalid state if either location is out of bounds.
/// If the builder is already in an invalid state, this function does nothing.
fn disconnect(&mut self, locations: UnorderedPair<Location>) -> &mut Self;
/// Shorthand for multiple calls to [`Self::disconnect`], with the same conditions.
///
/// Disconnect cells neighboring `location`.
/// Any appearance of a direction after the first in `directions` is ignored.
fn disconnect_around(&mut self, location: Location, directions: &Vec<Sh>) -> &mut Self;
/// Check the validity of this builder, ensuring no [`BuilderInvalidReason`] condition has arisen.
///
/// Returns `None` if the builder is valid, `Some(&Vec<BuilderInvalidReason>)` otherwise.
fn is_valid(&self) -> Option<&[BuilderInvalidReason]>;
/// Convert the state of this builder into a [`Board`].
/// If the builder is invalid for any reason, a reference to a [`Vec`] of [`BuilderInvalidReason`] will indicate why.
fn build(&self) -> Result<Board<Sh>, &[BuilderInvalidReason]>;
}
/// A builder for boards with square-shaped cells, i.e. the rectangular boards found in Numberlink puzzles and in Flow Free and the Bridges and Warps expansions.
#[derive(Clone)]
pub struct SquareBoardBuilder {
// width, height
dims: (Dimension, Dimension),
cells: Array2<Cell<SquareStep>>,
affiliation_displays: Vec<char>,
invalid_reasons: Vec<BuilderInvalidReason>,
// walls
edge_blacklist: HashSet<UnorderedPair<Location>>,
// holes
location_blacklist: HashSet<Location>,
bridges: HashSet<Location>,
// warps
edge_whitelist: HashSet<(UnorderedPair<Location>, SquareStep)>,
}
impl Default for SquareBoardBuilder {
fn default() -> Self {
Self::with_dims((NonZero::new(5).unwrap(), NonZero::new(5).unwrap()))
}
}
impl Builder<SquareStep> for SquareBoardBuilder {
fn with_dims(dims: (Dimension, Dimension)) -> Self {
Self {
dims,
cells: Array2::from_shape_simple_fn((dims.1.get(), dims.0.get()), Cell::default),
invalid_reasons: Default::default(),
edge_blacklist: Default::default(),
location_blacklist: Default::default(),
bridges: Default::default(),
edge_whitelist: Default::default(),
affiliation_displays: Default::default(),
}
}
fn add_termini(&mut self, display: char, locations: (Location, Location)) -> &mut Self {
if !self.invalid_reasons.is_empty() {
return self;
}
for location in [locations.0, locations.1] {
if location.0 >= self.dims.0.get() || location.1 >= self.dims.1.get() {
self.invalid_reasons.push(BuilderInvalidReason::FeatureOutOfBounds);
return self;
}
}
// non-null affiliation IDs start at 1
let aff = self.affiliation_displays.len() + 1;
self.affiliation_displays.push(display);
for location in [locations.0, locations.1] {
self.cells.index_mut(location.as_index()).assign_elem(Cell::Terminus { affiliation: aff })
}
self
}
fn pop_termini(&mut self) -> &mut Self {
if !self.invalid_reasons.is_empty() {
return self;
}
let aff_to_remove = self.affiliation_displays.len();
let display = self.affiliation_displays.pop();
if display.is_some() {
self.cells.map_inplace(|cell| {
match cell {
Cell::Terminus { affiliation } => if *affiliation == aff_to_remove {
cell.assign_elem(Cell::Empty);
},
_ => {}
}
})
}
self
}
fn add_bridge(&mut self, location: Location) -> &mut Self {
if !self.invalid_reasons.is_empty() {
return self;
}
// todo: check this better; bridges right next to warps are *technically* possible
if !(1..(self.dims.0.get() - 1)).contains(&location.0) || !(1..(self.dims.1.get() - 1)).contains(&location.1) {
self.invalid_reasons.push(BuilderInvalidReason::FeatureOutOfBounds);
return self;
}
self.bridges.insert(location);
self
}
fn drop_location(&mut self, location: Location) -> &mut Self {
if !self.invalid_reasons.is_empty() {
return self;
}
if location.0 >= self.dims.0.get() || location.1 >= self.dims.1.get() {
self.invalid_reasons.push(BuilderInvalidReason::FeatureOutOfBounds);
return self;
}
self.location_blacklist.insert(location);
self
}
fn disconnect(&mut self, locations: UnorderedPair<Location>) -> &mut Self {
for location in [locations.0, locations.1] {
if location.0 >= self.dims.0.get() || location.1 >= self.dims.1.get() {
self.invalid_reasons.push(BuilderInvalidReason::FeatureOutOfBounds);
return self;
}
}
if !SquareStep::direction_to(locations.0, locations.1).is_some() {
return self;
}
self.edge_blacklist.insert(locations);
self
}
fn disconnect_around(&mut self, location: Location, directions: &Vec<SquareStep>) -> &mut Self {
for direction in directions {
self.disconnect(UnorderedPair::from((location, direction.attempt_from(location))));
}
self
}
fn is_valid(&self) -> Option<&[BuilderInvalidReason]> {
if self.invalid_reasons.is_empty() {
None
} else {
Some(&self.invalid_reasons)
}
}
fn build(&self) -> Result<Board<SquareStep>, &[BuilderInvalidReason]> {
if !self.invalid_reasons.is_empty() {
return Err(&self.invalid_reasons);
}
let mut graph = UnGraphMap::with_capacity(
// naively allocate for a complete grid of this size, which usually isn't too far off
self.cells.len(),
// "horizontal" edges
(self.dims.0.get() - 1) * self.dims.1.get()
// "vertical" edges
+ (self.dims.1.get() - 1) * self.dims.0.get(),
);
let nodes = Array2::from_shape_fn(self.cells.raw_dim(), |ind| Node {
location: Location::from(ind),
cell: *self.cells.get(ind).unwrap(),
});
for x in 0..self.dims.0.get() {
for y in 0..self.dims.1.get() {
let location = Location(x, y);
// add edges down and to the right, if possible
let location_below = SquareStep::Down.attempt_from(location);
let location_right = SquareStep::Right.attempt_from(location);
let node = nodes.get(location.as_index()).unwrap();
let node_below = nodes.get(location_below.as_index());
let node_right = nodes.get(location_right.as_index());
node_below.and_then(|other_node| graph.add_edge(*node, *other_node, Edge { affiliation: 0, direction: SquareStep::Down }));
node_right.and_then(|other_node| graph.add_edge(*node, *other_node, Edge { affiliation: 0, direction: SquareStep::Right }));
}
}
for (UnorderedPair(l1, l2), direction) in self.edge_whitelist.iter() {
graph.add_edge(*nodes.get(l1.as_index()).unwrap(), *nodes.get(l2.as_index()).unwrap(), Edge { affiliation: 0, direction: *direction });
}
// we replace nodes at a bridge location with multiple nodes, all sharing a location, but each has neighbors only in two opposing directions
for bridge_loc in &self.bridges {
// assume there isn't already a bridge here (bridges is hashset so that'll be true)
let existing_node_here = graph.nodes().find(|n| n.location == *bridge_loc).unwrap();
// deref and collect to avoid mutating ref inside iterator borrowing ref
let old_edges = graph.edges(existing_node_here)
.map(|(n1, n2, e)| (n1, n2, *e))
.collect_vec();
// copy every incident edge on the old vertex to one of the bridge vertices based on its direction
for (n1, n2, e) in old_edges {
let other = if n1 == existing_node_here { n2 } else { n1 };
let bridge_node_this_direction = Node {
location: *bridge_loc,
cell: Cell::Bridge {
affiliation: None,
direction: e.direction.ensure_forward(),
},
};
graph.add_edge(other, bridge_node_this_direction, Edge {
affiliation: 0,
direction: e.direction,
});
}
// cut the old one out
graph.remove_node(existing_node_here);
}
for location in self.location_blacklist.iter() {
let to_rm = graph.nodes().filter(|n| n.location == *location).collect_vec();
to_rm.iter().for_each(|n| { graph.remove_node(*n); });
}
for UnorderedPair(l1, l2) in self.edge_blacklist.iter() {
for (n1, n2) in graph.nodes().filter(|n| n.location == (*l1)).collect_vec().into_iter()
.cartesian_product(graph.nodes().filter(|n| n.location == (*l2)).collect_vec().into_iter()) {
graph.remove_edge(n1, n2);
}
}
let mut affiliation_displays = Vec::with_capacity(self.affiliation_displays.len() + 1);
// affiliation 0 is unaffiliated and will display as empty
affiliation_displays.push('.');
affiliation_displays.extend(self.affiliation_displays.clone());
Ok(Board {
graph,
dims: self.dims,
affiliation_displays,
})
}
}
impl SquareBoardBuilder {
#[inline]
fn max_loc(&self) -> Location {
Location(self.dims.0.get() - 1, self.dims.1.get() - 1)
}
/// Add a warp at the specified `location` pointing in `direction`.
/// A warp is located on one edge of the board and connects one cell to its partner on the opposite edge of the board along a cardinal direction.
///
/// Specifying `direction` necessary if and only if `location` is at a corner, in which case the direction is ambiguous.
/// If `location` is on an edge but not at a corner, `direction` is ignored regardless of whether it is specified.
///
/// May cause the builder to enter a [`FeatureOutOfBounds`](BuilderInvalidReason::FeatureOutOfBounds) invalid state if `location` is out of bounds or not on an edge.
/// May cause the builder to enter a [`WarpBadDirection`](BuilderInvalidReason::WarpBadDirection) invalid state if `location` is at a corner and `direction` is missing or is impossible for a warp at this location.
/// If the builder is already in an invalid state, this function does nothing.
pub fn add_warp(&mut self, location: Location, direction: Option<SquareStep>) -> &mut Self {
if !self.invalid_reasons.is_empty() {
return self;
}
if location > self.max_loc() {
self.invalid_reasons.push(BuilderInvalidReason::FeatureOutOfBounds);
return self;
}
// not on any edge
if location.0 != 0 && location.1 != 0 && location.0 != self.max_loc().0 && location.1 != self.max_loc().1 {
self.invalid_reasons.push(BuilderInvalidReason::FeatureOutOfBounds);
return self;
}
let is_corner = match location {
Location(0, 0) => true,
Location(0, y) => y == self.max_loc().1,
Location(x, 0) => x == self.max_loc().0,
Location(x, y) => x == self.max_loc().0 && y == self.max_loc().1
};
let edge = if is_corner {
if direction.is_none() {
self.invalid_reasons.push(BuilderInvalidReason::WarpBadDirection);
return self;
}
direction.unwrap()
} else {
match location {
Location(0, _) => SquareStep::Left,
Location(_, 0) => SquareStep::Up,
Location(x, y) => {
if x == self.max_loc().0 {
SquareStep::Right
} else {
// always true: y == self.max_loc().1
SquareStep::Down
}
}
}
};
let partner = match edge {
SquareStep::Up => Location(location.0, self.max_loc().1),
SquareStep::Down => Location(location.0, 0),
SquareStep::Left => Location(self.max_loc().0, location.1),
SquareStep::Right => Location(0, location.1),
};
if partner == location {
// then the given direction was bad
// e.g. top-left corner with down direction specified => "partner" is the first cell in the column => this condition succeeds
self.invalid_reasons.push(BuilderInvalidReason::WarpBadDirection);
return self;
}
// direction is from lower indexed edge, which is always "backward"
self.edge_whitelist.insert((UnorderedPair::from((location, partner)), edge.ensure_forward().invert()));
self
}
}