thud/board/
mod.rs

1mod raycast;
2use crate::coord::Coord;
3use crate::direction::Direction;
4use crate::piece::Piece;
5use crate::{EndState, Player, ThudError};
6
7#[cfg(feature = "serialize")]
8use serde::{Deserialize, Serialize};
9#[cfg(test)]
10mod tests;
11
12/// A configuration of Thud [`Piece`s](enum.Piece.html) on a Thud board
13///
14/// **Note**: `Board` is not aware of the whole state of the game, only the position of the pieces.
15/// As a result, the movement methods provided only perform checks according to the pieces on the
16/// board, but they will *not* check whether the move is valid in terms of turn progress - you
17/// should use the methods on [`Thud`](struct.Thud.html) for that.
18#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
19#[derive(Debug, Copy, Clone, Default)]
20pub struct Board {
21    // 1-based indexing
22    squares: [[Piece; 15]; 15],
23}
24
25type MoveResult = Result<(), ThudError>;
26
27impl Board {
28    /// Get a "fresh" `Board`, with [`Piece`s](enum.Piece.html) placed in the default positions for thud.
29    pub fn fresh() -> Self {
30        let mut filled_board = Self::default();
31        // Place the trolls
32        for i in 6..9 {
33            for j in 6..9 {
34                filled_board.place((i, j).into(), Piece::Troll);
35            }
36        }
37        // Place the dwarves
38        {
39            // Diagonals
40            let calcs: Vec<Box<dyn Fn(usize) -> (usize, usize)>> = vec![
41                Box::new(|num| (num, 5 - num)),
42                Box::new(|num| (num + 9, num)),
43                Box::new(|num| (num, num + 9)),
44                Box::new(|num| (num + 9, 14 - num)),
45            ];
46            for calc in calcs {
47                for dwarf in (0..=5).map(|seed| calc(seed).into()) {
48                    filled_board.place(dwarf, Piece::Dwarf);
49                }
50            }
51
52            // Extras at corners
53            for dwarf in vec![
54                (0, 6),
55                (0, 8),
56                (14, 6),
57                (14, 8),
58                (6, 0),
59                (8, 0),
60                (6, 14),
61                (8, 14),
62            ] {
63                filled_board.place(dwarf.into(), Piece::Dwarf);
64            }
65        }
66        // Place the thudstone
67        filled_board.place((7, 7).into(), Piece::Thudstone);
68        filled_board
69    }
70
71    /// Put a [`Piece`](enum.Piece.html) on the board.
72    pub fn place(&mut self, square: Coord, piece: Piece) {
73        let (x, y) = square.value();
74        self.squares[x][y] = piece;
75    }
76
77    /// Find what [`Piece`](enum.Piece.html) is at the [`Coord`](struct.Coord.html) specified.
78    pub fn get(&self, square: Coord) -> Piece {
79        let (x, y) = square.value();
80        self.squares[x][y]
81    }
82
83    pub fn full_raw(&self) -> [[Piece; 15]; 15] {
84        self.squares
85    }
86
87    /// Return a vector of all the [`Coord`s](struct.Coord.html) of squares occupied by the given piece type.
88    ///
89    /// ```
90    /// use thud::{Board, Piece, Coord};
91    ///
92    /// let board = Board::fresh();
93    /// let stone = board.army(Piece::Thudstone);
94    ///
95    /// assert_eq!(stone[0].value(), (7, 7));
96    /// ```
97    pub fn army(&self, piece_type: Piece) -> Vec<Coord> {
98        let mut result: Vec<Coord> = Vec::new();
99        for x in 0..15 {
100            for y in 0..15 {
101                if self.squares[x][y] == piece_type {
102                    if let Ok(coord) = Coord::zero_based(x, y) {
103                        result.push(coord);
104                    }
105                }
106            }
107        }
108        result
109    }
110
111    /// Get a vector of valid [`Coord`s](struct.Coord.html) in the 8 possible adjacent squares to the one given.
112    ///
113    /// Coordinates out of board bounds will not be included.
114    pub fn adjacent(&self, square: Coord) -> Vec<(Coord, Piece)> {
115        let mut adjacent: Vec<(Coord, Piece)> = Vec::with_capacity(8);
116
117        for dir in Direction::all() {
118            if let Ok(coord) = dir.modify(square) {
119                adjacent.push((coord, self.get(coord)));
120            }
121        }
122        adjacent
123    }
124
125    /// Move a troll.
126    ///
127    /// Returns [`Err(ThudError::IllegalMove)`](enum.ThudError.html) if:
128    ///
129    /// - The `troll` square is not [`Piece::Troll`](enum.Piece.html)
130    /// - The `target` square is not [`Piece::Empty`](enum.Piece.html)
131    /// - The `target` square is more than 1 squares away from the `troll` square
132    pub fn troll_move(&mut self, troll: Coord, target: Coord) -> MoveResult {
133        // Check the target is clear and the place we're moving from actually has a troll
134        if (self.get(troll), self.get(target)) != (Piece::Troll, Piece::Empty) {
135            return Err(ThudError::IllegalMove);
136        };
137
138        // Validate the move, ie. one space between them
139        if troll.diff(target).max() != 1 {
140            return Err(ThudError::IllegalMove);
141        }
142
143        // Move the troll
144        self.place(troll, Piece::Empty);
145        self.place(target, Piece::Troll);
146
147        Ok(())
148    }
149
150    /// "Shove" a troll.
151    ///
152    /// Returns [`Err(ThudError::IllegalMove)`](enum.ThudError.html) if:
153    ///
154    /// - The `troll` square is not [`Piece::Troll`](enum.Piece.html)
155    /// - The `target` square is not [`Piece::Empty`](enum.Piece.html)
156    /// - There are no [`Piece::Dwarf`s](enum.Piece.html) adjacent to the `target` square
157    ///
158    /// Returns [`Err(ThudError::Obstacle)`](enum.ThudError.html) if the target square is obstructed
159    ///
160    /// Returns [`Err(ThudError::LineTooShort)`](enum.ThudError.html) if the distance to the target
161    /// square is larger than the length of the line of trolls going in the other direction
162    pub fn troll_shove(&mut self, troll: Coord, target: Coord) -> MoveResult {
163        if (self.get(troll), self.get(target)) != (Piece::Troll, Piece::Empty) {
164            return Err(ThudError::IllegalMove);
165        }
166        self.verify_clear(troll, target)?;
167
168        let dwarves: Vec<(Coord, Piece)> = self
169            .adjacent(target)
170            .into_iter()
171            .filter(|(_, x)| *x == Piece::Dwarf)
172            .collect();
173        if dwarves.len() == 0 {
174            return Err(ThudError::IllegalMove);
175        }
176
177        let troll_len = self.count_line(
178            troll,
179            // unwrap because `self.verify_clear` would return an error if we weren't in a straight line
180            Direction::from_route(target, troll).unwrap(),
181            Piece::Troll,
182        );
183        let dist = troll.diff(target).max();
184        if dist > troll_len {
185            // Move is too far
186            return Err(ThudError::LineTooShort(dist, troll_len));
187        }
188
189        // Move the troll
190        self.place(troll, Piece::Empty);
191        self.place(target, Piece::Troll);
192
193        Ok(())
194    }
195
196    /// Use a troll to selectively capture dwarves around it.
197    ///
198    /// `targets` should be a `Vec` of [`Direction`s](enum.Direction.html) in which to capture; if there is a dwarf above
199    /// your troll and you wish to capture it then `targets` should contain
200    /// [`Direction::Up`](enum.Direction.html).
201    ///
202    /// Note that any invalid (out of board limits) or duplicate
203    /// [`Direction`s](enum.Direction.html) will be ignored.
204    ///
205    /// Returns [`Err(ThudError::IllegalMove)`](enum.ThudError.html) if the piece at `troll` is not [`Piece::Troll`](enum.Piece.html).
206    pub fn troll_capture(
207        &mut self,
208        troll: Coord,
209        targets: Vec<Direction>,
210    ) -> Result<usize, ThudError> {
211        if self.get(troll) != Piece::Troll {
212            return Err(ThudError::IllegalMove);
213        }
214
215        let mut captured = 0;
216
217        // Grab all the true coordinates from `targets`, returning an error if any are invalid
218        for target in targets.into_iter() {
219            if let Ok(coord) = target.modify(troll) {
220                if self.get(coord) == Piece::Dwarf {
221                    self.place(coord, Piece::Empty);
222                    captured += 1;
223                }
224            }
225        }
226
227        Ok(captured)
228    }
229
230    /// Move a dwarf.
231    ///
232    /// Returns [`Err(ThudError::IllegalMove)`](enum.ThudError.html) if:
233    ///
234    /// - square `dwarf` is not [`Piece::Dwarf`](enum.Piece.html)
235    /// - square `target` is not [`Piece::Empty`](enum.Piece.html)
236    ///
237    /// Returns [`Err(ThudError::Obstacle)`](enum.ThudError.html) if there is a piece in the way.
238    pub fn dwarf_move(&mut self, dwarf: Coord, target: Coord) -> MoveResult {
239        // Check the target is clear and the place we're moving from actually has a dwarf
240        if (self.get(dwarf), self.get(target)) != (Piece::Dwarf, Piece::Empty) {
241            return Err(ThudError::IllegalMove);
242        }
243        self.verify_clear(dwarf, target)?;
244
245        // Move the dwarf
246        self.place(dwarf, Piece::Empty);
247        self.place(target, Piece::Dwarf);
248
249        Ok(())
250    }
251
252    /// "Hurl" a dwarf.
253    ///
254    /// Returns [`Err(ThudError::IllegalMove)`](enum.ThudError.html) if:
255    ///
256    /// - square `dwarf` is not [`Piece::Dwarf`](enum.Piece.html)
257    /// - square `target` is not [`Piece::Troll`](enum.Piece.html)
258    ///
259    /// Returns [`Err(ThudError::Obstacle)`](enum.ThudError.html) if there is a piece in the way.
260    ///
261    /// Returns [`Err(ThudError::LineTooShort)`](enum.ThudError.html) if the distance to the target
262    /// square is larger than the length of the line of dwarves going in the other direction
263    pub fn dwarf_hurl(&mut self, dwarf: Coord, target: Coord) -> MoveResult {
264        if self.get(dwarf) != Piece::Dwarf || self.get(target) != Piece::Troll {
265            return Err(ThudError::IllegalMove);
266        }
267        self.verify_clear(dwarf, target)?;
268
269        // Make sure there are enough supporting dwarves
270        let dwarf_len = self.count_line(
271            dwarf,
272            Direction::from_route(target, dwarf).unwrap(),
273            Piece::Dwarf,
274        );
275        let dist = dwarf.diff(target).max();
276        if dwarf_len < dist {
277            return Err(ThudError::LineTooShort(dist, dwarf_len));
278        }
279
280        self.place(dwarf, Piece::Empty);
281        self.place(target, Piece::Dwarf);
282
283        Ok(())
284    }
285
286    /// Get a `Vec` of [`Coord`s](struct.Coord.html) that the piece at `loc` can make
287    pub fn available_moves(&self, loc: Coord) -> Vec<Coord> {
288        let mut avail: Vec<Coord> = Vec::new();
289        match self.get(loc) {
290            Piece::Dwarf => {
291                for dir in Direction::all() {
292                    // Count the dwarves behind us
293                    let line_behind = self.count_line(loc, dir.opposite(), Piece::Dwarf);
294
295                    for (count, (poss, piece)) in self.cast(loc, dir).into_iter().enumerate() {
296                        match piece {
297                            // If it's empty, we can move into it!
298                            Piece::Empty => avail.push(poss),
299                            // If there's a troll there, we can take it if we're not so far out
300                            // that our line of dwarves can't support us (but cannot jump over it)
301                            Piece::Troll => {
302                                if count <= line_behind {
303                                    avail.push(poss);
304                                }
305                                break;
306                            }
307                            _ => break,
308                        }
309                    }
310                }
311            }
312            Piece::Troll => {
313                // Look as far as we are allowed by our line of trolls in all directions, and get
314                // any empty squares we find
315                for dir in Direction::all() {
316                    let behind_line = self.count_line(loc, dir.opposite(), Piece::Troll);
317                    let mut cast = self.cast(loc, dir);
318                    cast.next();
319                    for (poss, piece) in cast.take(behind_line) {
320                        match piece {
321                            Piece::Empty => avail.push(poss),
322                            _ => break,
323                        }
324                    }
325                }
326            }
327            _ => (),
328        }
329
330        avail
331    }
332
333    /// Find if there is a winner or the game is over.
334    ///
335    /// Returns:
336    ///
337    /// - [`Some(EndState::Won(Player))`](enum.EndState.html) if a player has won the match
338    /// - [`Some(EndState::Draw)`](enum.EndState.html) if the match is a draw
339    /// - `None` if the board still has moves to play
340    pub fn winner(&self) -> Option<EndState> {
341        // Check dwarves
342        let mut dwarf_moves = 0;
343        for dwarf in self.army(Piece::Dwarf) {
344            dwarf_moves += self.available_moves(dwarf).len();
345        }
346
347        // Check trolls
348        let mut troll_moves = 0;
349        for troll in self.army(Piece::Troll) {
350            troll_moves += self.available_moves(troll).len();
351        }
352
353        if troll_moves == 0 || dwarf_moves == 0 {
354            let (dwarf_score, troll_score) = self.score();
355            if dwarf_score > troll_score {
356                Some(EndState::Won(Player::Dwarf))
357            } else if troll_score > dwarf_score {
358                Some(EndState::Won(Player::Troll))
359            } else {
360                Some(EndState::Draw)
361            }
362        } else {
363            None
364        }
365    }
366
367    /// Get the scores of each player
368    ///
369    /// Given in format `(<dwarf score>, <troll score>)`
370    pub fn score(&self) -> (usize, usize) {
371        let dwarves = self.army(Piece::Dwarf).len();
372        let trolls = self.army(Piece::Troll).len() * 4;
373        (dwarves, trolls)
374    }
375
376    fn cast(&self, loc: Coord, dir: Direction) -> raycast::RayCast {
377        raycast::RayCast::new(self, loc, dir)
378    }
379
380    fn verify_clear(&self, src: Coord, dest: Coord) -> MoveResult {
381        let dir = Direction::from_route(src, dest)?;
382        // Skip the first element
383        for (current, piece) in self.cast(src, dir) {
384            if current == dest {
385                // Stop at the target square
386                break;
387            }
388            if piece != Piece::Empty {
389                // There is something in the way
390                let (x, y) = current.value();
391                return Err(ThudError::Obstacle(x, y));
392            }
393        }
394
395        Ok(())
396    }
397
398    fn count_line(&self, start: Coord, dir: Direction, piece: Piece) -> usize {
399        if self.get(start) != piece {
400            return 0;
401        }
402
403        let mut length = 1;
404        for (_, cur_piece) in self.cast(start, dir) {
405            if cur_piece != piece {
406                break;
407            }
408            length += 1;
409        }
410        length
411    }
412}