dcc_tiler/
board.rs

1use crate::tile::{Direction, Tile, TileCollection};
2use serde_derive::Serialize;
3use std::collections::HashSet;
4use std::fmt;
5
6#[derive(Clone, PartialEq, Eq, Hash, Serialize)]
7pub struct RectangularBoard {
8    #[serde(skip_serializing)]
9    pub width: usize,
10
11    #[serde(skip_serializing)]
12    pub height: usize,
13
14    pub board: Vec<Vec<bool>>,
15
16    #[serde(skip_serializing)]
17    counts: Vec<Vec<usize>>,
18}
19
20impl RectangularBoard {
21    pub fn new(width: usize, height: usize) -> Self {
22        let mut counts = vec![vec![0; width]; height];
23
24        for row in counts.iter_mut() {
25            row[0] = 1;
26            row[width - 1] = 1;
27        }
28
29        for j in 0..width {
30            counts[0][j] = 1;
31            counts[height - 1][j] = 1;
32        }
33
34        RectangularBoard {
35            width,
36            height,
37            board: vec![vec![false; width]; height],
38            counts,
39        }
40    }
41
42    /// Generates a new L-tetromino shaped board.
43    ///
44    /// This is a two step process - first we make an L shape
45    /// with long side having length n, and then we replace each
46    /// box with a scale^2 box.
47    pub fn l_board(n: usize, scale: usize) -> Self {
48        let mut board = RectangularBoard::new(n * scale, 2 * scale);
49
50        for row in 0..scale {
51            for col in scale..(n * scale) {
52                board.board[row][col] = true;
53            }
54        }
55
56        board
57    }
58
59    /// Generates a new T-tetromino shaped board.
60    ///
61    /// This is a two step process - first we make a T shape
62    /// where the two tils have length n, and then we replace
63    /// each box with a scale^2 box.
64    pub fn t_board(n: usize, scale: usize) -> Self {
65        let mut board = RectangularBoard::new((2 * n + 1) * scale, 2 * scale);
66
67        for row in 0..scale {
68            for col in 0..(n * scale) {
69                board.board[row][col] = true;
70            }
71            for col in ((n + 1) * scale)..((2 * n + 1) * scale) {
72                board.board[row][col] = true;
73            }
74        }
75
76        board
77    }
78
79    /// What does it do?
80    ///
81    /// Details here.
82    ///
83    /// # Panics
84    ///
85    /// When does it panic?
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// // Example code here
91    /// ```
92    fn mark(&mut self, p: Position) {
93        for xp in (p.x - 1)..=(p.x + 1) {
94            if xp == p.x {
95                continue;
96            }
97            if self.is_valid(Position::from((xp, p.y))) {
98                self.counts[xp as usize][p.y as usize] += 1;
99            }
100        }
101        for yp in (p.y - 1)..=(p.y + 1) {
102            if yp == p.y {
103                continue;
104            }
105            if self.is_valid(Position::from((p.x, yp))) {
106                self.counts[p.x as usize][yp as usize] += 1;
107            }
108        }
109
110        self.board[p.x as usize][p.y as usize] = true;
111    }
112
113    /// Determines whether the entire board is marked
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// // Example code here
119    /// ```
120    pub fn is_all_marked(&self) -> bool {
121        for row in self.board.iter() {
122            for col in row.iter() {
123                if !(*col) {
124                    return false;
125                }
126            }
127        }
128        true
129    }
130
131    pub fn place_tile(&self, tile_collection: &TileCollection) -> Vec<RectangularBoard> {
132        let mut largest_count = None;
133        let mut largest_position = None;
134
135        // find the position with the highest count
136        for j in 0..self.width {
137            for i in 0..self.height {
138                if !self.board[i][j] {
139                    let count = self.counts[i][j];
140
141                    // If our tile collection doesn't contain a 1x1 tile,
142                    // then we've found a spot that cannot be tiled, so we're done
143                    if !tile_collection.contains_single_tile() && count == 4 {
144                        return Vec::new();
145                    }
146
147                    // keep track of the largest count we've found so far
148                    if largest_count.is_none() || self.counts[i][j] > largest_count.unwrap() {
149                        largest_count = Some(self.counts[i][j]);
150                        largest_position = Some((i, j));
151                    }
152                }
153            }
154        }
155
156        // Next, find all the tiles that fit at out best position
157        let mut fitting_tiles = Vec::new();
158
159        if let Some((i, j)) = largest_position {
160            for tile in tile_collection.iter() {
161                for start_index in 0..=tile.directions.len() {
162                    if let Some(tp) =
163                        self.tile_fits_at_position(tile, Position::from((i, j)), start_index)
164                    {
165                        // Really we should be using a HashSet for fitting_tiles, but it's annoying
166                        // to hash a HashSet, so we just check for containment here instead
167                        if !fitting_tiles.contains(&tp) {
168                            fitting_tiles.push(tp);
169                        }
170                    }
171                }
172            }
173        }
174
175        // For each fitting tile we find, return the corresponding board
176        fitting_tiles
177            .into_iter()
178            .map(|tp| {
179                let mut child_board = self.clone();
180                child_board.mark_tile_at_position(tp);
181                child_board
182            })
183            .collect()
184    }
185
186    fn is_marked(&self, p: Position) -> bool {
187        assert!(self.is_valid(p));
188
189        self.board[p.x as usize][p.y as usize]
190    }
191
192    fn is_valid(&self, p: Position) -> bool {
193        p.x >= 0 && (p.x as usize) < self.height && p.y >= 0 && (p.y as usize) < self.width
194    }
195
196    fn move_in_direction(&self, p: Position, direction: Direction) -> Position {
197        let mut row = p.x;
198        let mut col = p.y;
199
200        col += match direction {
201            Direction::Left => -1,
202            Direction::Right => 1,
203            Direction::UpLeft => -1,
204            Direction::UpRight => 1,
205            Direction::DownLeft => -1,
206            Direction::DownRight => 1,
207            _ => 0,
208        };
209        row += match direction {
210            Direction::Up => -1,
211            Direction::Down => 1,
212            Direction::UpLeft => -1,
213            Direction::UpRight => -1,
214            Direction::DownLeft => 1,
215            Direction::DownRight => 1,
216            _ => 0,
217        };
218
219        Position::new(row, col)
220    }
221
222    /// Tests whether the specified tile fits at the specified board position.
223    /// If it does, then return Some(TilePosition)
224    ///
225    /// # Examples
226    ///
227    /// ```
228    /// // Example code here
229    /// ```
230    fn tile_fits_at_position(
231        &self,
232        tile: &Tile,
233        position: Position,
234        start_index: usize,
235    ) -> Option<TilePosition> {
236        // make sure our start index isn't too large
237        assert!(start_index <= tile.directions.len());
238
239        let mut current_position = position;
240
241        let valid_and_unmarked = |p: Position| self.is_valid(p) && !self.is_marked(p);
242
243        if !valid_and_unmarked(current_position) {
244            return None;
245        }
246
247        let mut covered = HashSet::new();
248        covered.insert(current_position);
249
250        // move backwards from start_index - 1
251        for i in (0..start_index).rev() {
252            current_position =
253                self.move_in_direction(current_position, tile.directions[i].opposite());
254
255            if !valid_and_unmarked(current_position) {
256                return None;
257            }
258            covered.insert(current_position);
259        }
260
261        let mut current_position = position;
262
263        // now move forwards after the start index
264        for i in start_index..tile.directions.len() {
265            current_position = self.move_in_direction(current_position, tile.directions[i]);
266
267            if !valid_and_unmarked(current_position) {
268                return None;
269            }
270
271            covered.insert(current_position);
272        }
273
274        Some(TilePosition::new(
275            position,
276            tile.clone(),
277            start_index,
278            covered,
279        ))
280    }
281
282    fn mark_tile_at_position(&mut self, tp: TilePosition) {
283        for position in tp.covered {
284            self.mark(position);
285        }
286    }
287}
288
289impl fmt::Debug for RectangularBoard {
290    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
291        let mut os = Vec::with_capacity((1 + self.width) * self.height);
292
293        for i in 0..self.height {
294            for j in 0..self.width {
295                os.push(if self.board[i][j] { "x" } else { "*" });
296            }
297            os.push("\n");
298        }
299
300        write!(f, "{}", os.join(""))
301    }
302}
303
304#[derive(Copy, Clone, PartialEq, Eq, Hash)]
305struct Position {
306    x: isize,
307    y: isize,
308}
309
310impl From<(isize, isize)> for Position {
311    fn from(p: (isize, isize)) -> Self {
312        Position::new(p.0, p.1)
313    }
314}
315
316impl From<(usize, usize)> for Position {
317    fn from(p: (usize, usize)) -> Self {
318        Position::new(p.0 as isize, p.1 as isize)
319    }
320}
321
322impl Position {
323    pub fn new(x: isize, y: isize) -> Self {
324        Position { x, y }
325    }
326}
327
328#[derive(Eq, Clone)]
329struct TilePosition {
330    position: Position,
331    tile: Tile,
332    start_index: usize,
333    covered: HashSet<Position>,
334}
335
336impl TilePosition {
337    pub fn new(
338        position: Position,
339        tile: Tile,
340        start_index: usize,
341        covered: HashSet<Position>,
342    ) -> Self {
343        TilePosition {
344            covered,
345            position,
346            tile,
347            start_index,
348        }
349    }
350}
351
352impl PartialEq for TilePosition {
353    fn eq(&self, other: &TilePosition) -> bool {
354        self.covered == other.covered
355    }
356}