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}