words_game/models/
board.rs

1use serde::{Serialize, Deserialize};
2use super::super::constants::{BOARD, BOARD_SIZE, DICTIONARY};
3use super::super::error::{Error, Result};
4use super::tile::Tile;
5use super::{Direction, Point, Strip};
6use std::fmt;
7
8/**
9 * Represents how the cell on the board affects the scoring of the final word
10 */
11#[derive(Debug, PartialEq)]
12struct BoardCellMultiplier {
13    pub word: u32,
14    pub letter: u32,
15}
16
17impl BoardCellMultiplier {
18    pub fn new(word: u32, letter: u32) -> Self {
19        BoardCellMultiplier { word, letter }
20    }
21}
22
23/**
24 * Represents the state of a cell on the board
25 */
26#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
27pub enum BoardCell {
28    StartingSpot,
29    Empty,
30    DoubleLetter,
31    TripleLetter,
32    DoubleWord,
33    TripleWord,
34    Tile(Tile),
35}
36
37impl BoardCell {
38    fn get_multiplier(&self) -> BoardCellMultiplier {
39        match self {
40            Self::DoubleLetter => BoardCellMultiplier::new(1, 2),
41            Self::TripleLetter => BoardCellMultiplier::new(1, 3),
42            Self::DoubleWord => BoardCellMultiplier::new(2, 1),
43            Self::TripleWord => BoardCellMultiplier::new(3, 1),
44            _ => BoardCellMultiplier::new(1, 1),
45        }
46    }
47}
48
49impl From<char> for BoardCell {
50    fn from(c: char) -> Self {
51        match c {
52            '.' => Self::Empty,
53            '3' => Self::TripleWord,
54            '2' => Self::DoubleWord,
55            '@' => Self::DoubleLetter,
56            '#' => Self::TripleLetter,
57            '+' => Self::StartingSpot,
58            'A'..='Z' => Self::Tile(Tile::Letter(c)),
59            _ => unreachable!("BoardCell:from Parsing invalid tile character {}", c),
60        }
61    }
62}
63
64impl Into<char> for &BoardCell {
65    fn into(self) -> char {
66        match *self {
67            BoardCell::StartingSpot => '+',
68            BoardCell::Empty => '.',
69            BoardCell::DoubleLetter => '@',
70            BoardCell::TripleLetter => '#',
71            BoardCell::DoubleWord => '2',
72            BoardCell::TripleWord => '3',
73            BoardCell::Tile(Tile::Letter(letter)) => letter,
74            BoardCell::Tile(Tile::Blank) => unreachable!()
75        }
76    }
77}
78
79impl fmt::Display for BoardCell {
80    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
81        write!(f, "{}", Into::<char>::into(self))
82    }
83}
84
85/**
86 * Trait defines basic operations that can be performed on a board
87 */
88trait ReadableBoard {
89    fn is_in_bounds(&self, point: Point) -> bool;
90    fn get(&self, point: Point) -> Option<&BoardCell>;
91}
92
93#[inline]
94fn xy_to_idx(width: u32, point: Point) -> usize {
95    (point.y * width as i32 + point.x) as usize
96}
97
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct Board {
100    pub cells: Vec<BoardCell>,
101}
102
103impl fmt::Display for Board {
104    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
105        for y in 0..BOARD_SIZE {
106            for x in 0..BOARD_SIZE {
107                self.get(Point::new(x as i32, y as i32)).unwrap().fmt(f)?;
108            }
109            writeln!(f)?
110        }
111        Ok(())
112    }
113}
114
115impl ReadableBoard for Board {
116    #[inline]
117    fn is_in_bounds(&self, point: Point) -> bool {
118        (0..BOARD_SIZE).contains(&(point.x as u32)) && (0..BOARD_SIZE).contains(&(point.y as u32))
119    }
120
121    fn get(&self, point: Point) -> Option<&BoardCell> {
122        if !self.is_in_bounds(point) {
123            return None;
124        }
125
126        self.cells.get(xy_to_idx(BOARD_SIZE, point))
127    }
128}
129
130impl Board {
131    pub fn new() -> Board {
132        let cells = BOARD.chars().map(|x| BoardCell::from(x)).collect();
133        Board { cells }
134    }
135
136    #[allow(dead_code)]
137    fn set(&mut self, point: Point, bc: BoardCell) -> Result<()> {
138        if !self.is_in_bounds(point) {
139            return Err(Error::BadAction("Out of bounds".to_string()).into());
140        }
141        self.cells[xy_to_idx(BOARD_SIZE, point)] = bc;
142        Ok(())
143    }
144
145    fn get_mut(&mut self, point: Point) -> Option<&mut BoardCell> {
146        if !self.is_in_bounds(point) {
147            return None;
148        }
149
150        self.cells.get_mut(xy_to_idx(BOARD_SIZE, point))
151    }
152
153    fn for_each_mut(&mut self, strip: &Strip, f: &mut dyn FnMut(Point, &mut BoardCell) -> bool) {
154        let mut loc = strip.start;
155
156        for _ in 0..strip.len {
157            if let Some(bc) = self.get_mut(loc) {
158                if !f(loc, bc) {
159                    return;
160                }
161            } else {
162                return;
163            }
164
165            loc += strip.dir;
166        }
167    }
168}
169
170/**
171 * A decorator for a Board that places an "uncommitted" line of pieces above
172 * a line on the original board
173 */
174pub struct BoardWithOverlay {
175    board: Board,
176    strip: Strip,
177    board_cells: Vec<Option<BoardCell>>,
178}
179
180pub struct OverlaidWord(Vec<(BoardCell, Option<BoardCell>)>);
181
182impl std::ops::Deref for OverlaidWord {
183    type Target = Vec<(BoardCell, Option<BoardCell>)>;
184
185    fn deref(&self) -> &Self::Target {
186        &self.0
187    }
188}
189
190impl OverlaidWord {
191    pub fn calculate_word_and_score(&self) -> Result<(String, u32)> {
192        let mut aggregate_word = Vec::<char>::with_capacity(self.len());
193
194        let mut letter_score = 0;
195        let mut word_multiplier = 1;
196
197        for (bc, bottom_bc) in self.iter() {
198            let curr_letter_val = match bc {
199                BoardCell::Tile(tile) => {
200                    match tile {
201                        Tile::Letter(letter) => aggregate_word.push(*letter),
202                        _ => unreachable!(),
203                    }
204                    tile.point_value()
205                }
206                _ => unreachable!(),
207            };
208
209            if let Some(under_board_cell) = bottom_bc {
210                let BoardCellMultiplier {
211                    word: word_mult,
212                    letter: letter_mult,
213                } = under_board_cell.get_multiplier();
214
215                letter_score += curr_letter_val * letter_mult;
216                word_multiplier *= word_mult;
217            } else {
218                letter_score += curr_letter_val;
219            }
220        }
221
222        let word = aggregate_word.into_iter().collect::<String>();
223        if !DICTIONARY.contains(&word[..]) {
224            Err(Error::InvalidWord(word).into())
225        } else {
226            Ok((word, letter_score * word_multiplier))
227        }
228    }
229
230    pub fn ensure_word_covering_starting_spot(&self) -> Result<()> {
231        for (_, under_bc) in self.iter() {
232            if let Some(BoardCell::StartingSpot) = under_bc {
233                return Ok(());
234            }
235        }
236
237        Err(Error::StartingTileNotCovered.into())
238    }
239}
240
241impl BoardWithOverlay {
242    fn get_overlay_mask(
243        board: &Board,
244        strip: &Strip,
245        word: &str,
246    ) -> Result<Vec<Option<BoardCell>>> {
247        let mut curr_point = strip.start;
248
249        let mut mask = Vec::<Option<BoardCell>>::with_capacity(strip.len as usize);
250
251        for curr_letter in word.chars() {
252            let board_cell = board.get(curr_point).ok_or_else(|| {
253                Error::BadAction("This placement goes off of the board!".to_string())
254            })?;
255
256            match board_cell {
257                BoardCell::Tile(Tile::Letter(letter)) => {
258                    if letter == &curr_letter {
259                        mask.push(None);
260                    } else {
261                        return Err(Error::BadAction("Pieces do not fit".to_string()).into());
262                    }
263                }
264                _ => mask.push(Some(BoardCell::Tile(Tile::Letter(curr_letter)))),
265            }
266
267            curr_point += strip.dir;
268        }
269
270        Ok(mask)
271    }
272
273    pub fn try_overlay(
274        board: Board,
275        point: Point,
276        dir: Direction,
277        word: &str,
278    ) -> Result<BoardWithOverlay> {
279        let strip = Strip::new(point, dir, word.len() as i32);
280
281        let overlay_mask = Self::get_overlay_mask(&board, &strip, word)?;
282
283        let bwo = BoardWithOverlay {
284            board,
285            strip,
286            board_cells: overlay_mask,
287        };
288
289        Ok(bwo)
290    }
291
292    pub fn get_overlaid_letters(&self) -> Vec<Tile> {
293        self.board_cells
294            .iter()
295            .filter(|w| w.is_some())
296            .map(|w| match *w {
297                Some(BoardCell::Tile(tile)) => tile,
298                _ => unreachable!(),
299            })
300            .collect()
301    }
302
303    fn get_overlay_at(&self, point: Point) -> Option<&BoardCell> {
304        let dist_from_start = self.strip.distance_in(point)?;
305
306        self.board_cells[dist_from_start as usize].as_ref()
307    }
308
309    fn is_point_covered(&self, point: Point) -> bool {
310        self.get_overlay_at(point).is_some()
311    }
312
313    /// Returns a vector tuple. The first element stores the flattened tile
314    /// (i.e which ever tile .get returns) where as the second element
315    /// stores the tile underneath if the first is an overlaid tile
316    pub fn get_connecting_letters_from(
317        &self,
318        start: Point,
319        dir: Direction,
320    ) -> Vec<(BoardCell, Option<BoardCell>)> {
321        let mut accum_vec = Vec::<(BoardCell, Option<BoardCell>)>::new();
322
323        self.for_each_until(start, dir, &mut |point, board_cell| match *board_cell {
324            BoardCell::Tile(_) => {
325                accum_vec.push((
326                    (*board_cell).clone(),
327                    if self.strip.contains(point) {
328                        Some((*self.board.get(point).unwrap()).clone())
329                    } else {
330                        None
331                    },
332                ));
333                true
334            }
335            _ => false,
336        });
337
338        accum_vec
339    }
340
341    pub fn get_whole_word(&self, start: Point, dir: Direction) -> OverlaidWord {
342        let mut word = Vec::new();
343
344        let mut opposite_dir = self.get_connecting_letters_from(start + (dir * -1), dir * -1);
345        opposite_dir.reverse();
346
347        word.append(&mut opposite_dir);
348        word.append(&mut self.get_connecting_letters_from(start, dir));
349
350        OverlaidWord(word)
351    }
352
353    pub fn get_formed_words(&self) -> (OverlaidWord, Vec<OverlaidWord>) {
354        let main_line_word = self.get_whole_word(self.strip.start, self.strip.dir);
355
356        let mut branching_words = Vec::new();
357
358        let perp_direction = if self.strip.dir.is_horizontal() {
359            Direction::down()
360        } else {
361            Direction::right()
362        };
363
364        self.for_each(&self.strip, &mut |point, _| {
365            if self.is_point_covered(point) {
366                let word = self.get_whole_word(point, perp_direction);
367
368                if word.len() > 1 {
369                    branching_words.push(word)
370                }
371            }
372            true
373        });
374
375        (main_line_word, branching_words)
376    }
377
378    pub fn apply_to_board(mut self) -> Board {
379        let mut i = 0usize;
380        let board_cells = &self.board_cells;
381
382        self.board.for_each_mut(&self.strip, &mut |_, board_cell| {
383            if let Some(ref new_board_cell) = board_cells[i] {
384                *board_cell = new_board_cell.clone();
385            }
386            i += 1;
387            true
388        });
389
390        self.board
391    }
392}
393
394impl ReadableBoard for BoardWithOverlay {
395    fn is_in_bounds(&self, point: Point) -> bool {
396        self.board.is_in_bounds(point)
397    }
398
399    fn get(&self, point: Point) -> Option<&BoardCell> {
400        if !self.is_in_bounds(point) {
401            return None;
402        }
403
404        if !self.strip.contains(point) {
405            // This piece is not being overlayed on
406            return self.board.get(point);
407        }
408
409        // If we are looking at a cell that is actually being overlayed,
410        // and not just a part of the strip, then we return the cell
411        // otherwise, we return the underlying piece
412        if let Some(ref cell) = self.get_overlay_at(point) {
413            Some(cell)
414        } else {
415            self.board.get(point)
416        }
417    }
418}
419
420trait IterableBoard {
421    fn for_each_until(
422        &self,
423        start: Point,
424        dir: Direction,
425        f: &mut dyn FnMut(Point, &BoardCell) -> bool,
426    );
427
428    fn for_each(&self, strip: &Strip, f: &mut dyn FnMut(Point, &BoardCell) -> bool);
429}
430
431impl<T: ReadableBoard> IterableBoard for T {
432    /**
433     * Allows us to easily iterate over a line of the board
434     *
435     * The iterator function can control if it want's to continue iterator
436     * by returning a result. An Err will immediately end the iteration
437     */
438    fn for_each_until(
439        &self,
440        start: Point,
441        dir: Direction,
442        f: &mut dyn FnMut(Point, &BoardCell) -> bool,
443    ) {
444        let mut loc = start;
445
446        loop {
447            if let Some(bc) = self.get(loc) {
448                if !f(loc, bc) {
449                    return;
450                }
451            } else {
452                return;
453            }
454
455            loc += dir;
456        }
457    }
458
459    fn for_each(&self, strip: &Strip, f: &mut dyn FnMut(Point, &BoardCell) -> bool) {
460        let mut loc = strip.start;
461
462        for _ in 0..strip.len {
463            if let Some(bc) = self.get(loc) {
464                if !f(loc, bc) {
465                    return;
466                }
467            } else {
468                return;
469            }
470
471            loc += strip.dir;
472        }
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn at_no_tile_test() {
482        let board = Board::new();
483
484        assert_eq!(board.get(Point::new(0, 0)).unwrap(), &BoardCell::TripleWord);
485        assert_eq!(
486            board.get(Point::new(3, 0)).unwrap(),
487            &BoardCell::DoubleLetter
488        );
489        assert_eq!(board.get(Point::new(1, 1)).unwrap(), &BoardCell::DoubleWord);
490        assert_eq!(
491            board.get(Point::new(7, 7)).unwrap(),
492            &BoardCell::StartingSpot
493        );
494        assert_eq!(
495            board.get(Point::new(BOARD_SIZE as i32, BOARD_SIZE as i32)),
496            None
497        );
498    }
499
500    #[test]
501    fn set_and_get_tiles() {
502        let mut board = Board::new();
503
504        assert_eq!(
505            board
506                .set(Point::new(0, 0), BoardCell::Tile(Tile::Letter('A')))
507                .is_ok(),
508            true
509        );
510        assert_eq!(
511            board.get(Point::new(0, 0)).unwrap(),
512            &BoardCell::Tile(Tile::Letter('A'))
513        );
514
515        assert_eq!(
516            board
517                .set(
518                    Point::new(BOARD_SIZE as i32, BOARD_SIZE as i32),
519                    BoardCell::Empty
520                )
521                .is_err(),
522            true
523        );
524    }
525
526    #[test]
527    fn pieces_for_place() {
528        let mut board = Board::new();
529        let mut board_with_overlay =
530            BoardWithOverlay::try_overlay(board, Point::new(0, 0), Direction::new(1, 0), "HI")
531                .unwrap();
532
533        assert_eq!(
534            board_with_overlay.get_overlaid_letters(),
535            vec![Tile::Letter('H'), Tile::Letter('I')]
536        );
537
538        board = Board::new();
539        board
540            .set(Point::new(1, 0), BoardCell::Tile(Tile::Letter('E')))
541            .unwrap();
542        board
543            .set(Point::new(3, 0), BoardCell::Tile(Tile::Letter('L')))
544            .unwrap();
545        board_with_overlay =
546            BoardWithOverlay::try_overlay(board, Point::new(0, 0), Direction::new(1, 0), "HELLO")
547                .unwrap();
548
549        assert_eq!(
550            board_with_overlay.get_overlaid_letters(),
551            vec![Tile::Letter('H'), Tile::Letter('L'), Tile::Letter('O')]
552        );
553    }
554
555    #[test]
556    fn pieces_for_place_err() {
557        let mut board = Board::new();
558        let mut board_with_overlay = BoardWithOverlay::try_overlay(
559            board,
560            Point::new(0, 0),
561            Direction::new(1, 0),
562            "REALLY LONG WORD THAT OVERFLOWS THE ENTIRE BOARD",
563        );
564
565        assert_eq!(board_with_overlay.is_err(), true);
566
567        board = Board::new();
568        board_with_overlay = BoardWithOverlay::try_overlay(
569            board,
570            Point::new(10, 0),
571            Direction::new(1, 0),
572            "LONGWORD",
573        );
574
575        assert_eq!(board_with_overlay.is_err(), true);
576    }
577
578    fn make_board_with_overlay() -> Result<BoardWithOverlay> {
579        /*
580         * We construct a board that looks like this (in the top left corner)
581         * .P.C..
582         * .R.R..
583         * ....D. <-- We play MINED here
584         * .M.E..
585         * .E.K..
586         */
587        let mut board = Board::new();
588        board.set(Point::new(1, 0), BoardCell::Tile(Tile::from('P')))?;
589        board.set(Point::new(1, 1), BoardCell::Tile(Tile::from('R')))?;
590        // board.set(Point::new(1, 2), BoardCell::Tile(Tile::from('I')))?;
591        board.set(Point::new(1, 3), BoardCell::Tile(Tile::from('M')))?;
592        board.set(Point::new(1, 4), BoardCell::Tile(Tile::from('E')))?;
593
594        board.set(Point::new(3, 0), BoardCell::Tile(Tile::from('C')))?;
595        board.set(Point::new(3, 1), BoardCell::Tile(Tile::from('R')))?;
596        // board.set(Point::new(3, 2), BoardCell::Tile(Tile::from('E')))?;
597        board.set(Point::new(3, 3), BoardCell::Tile(Tile::from('E')))?;
598        board.set(Point::new(3, 4), BoardCell::Tile(Tile::from('K')))?;
599
600        board.set(Point::new(4, 2), BoardCell::Tile(Tile::from('D')))?;
601
602        let board_overlay =
603            BoardWithOverlay::try_overlay(board, Point::new(0, 2), Direction::right(), "MINED")?;
604
605        Ok(board_overlay)
606    }
607
608    #[test]
609    fn get_overlay_at() -> Result<()> {
610        let board = make_board_with_overlay()?;
611
612        assert_eq!(
613            board.get_overlay_at(Point::new(0, 2)),
614            Some(&BoardCell::Tile(Tile::from('M')))
615        );
616
617        assert_eq!(
618            board.get_overlay_at(Point::new(1, 2)),
619            Some(&BoardCell::Tile(Tile::from('I')))
620        );
621
622        assert_eq!(
623            board.get_overlay_at(Point::new(3, 2)),
624            Some(&BoardCell::Tile(Tile::from('E')))
625        );
626
627        assert_eq!(board.get_overlay_at(Point::new(0, 1)), None);
628        assert_eq!(board.get_overlay_at(Point::new(4, 3)), None);
629        assert_eq!(board.get_overlay_at(Point::new(2, 4)), None);
630        Ok(())
631    }
632
633    #[test]
634    fn full_overlay_test() -> Result<()> {
635        let board_overlay = make_board_with_overlay()?;
636
637        let (main_word, perp_words) = board_overlay.get_formed_words();
638
639        assert_eq!(main_word.len(), 5);
640        assert_eq!(perp_words.len(), 2);
641
642        Ok(())
643    }
644
645    #[test]
646    fn apply_to_board() -> Result<()> {
647        let board_overlay = make_board_with_overlay()?;
648
649        let board = board_overlay.apply_to_board();
650
651        assert_eq!(
652            board.get(Point::new(0, 2)).unwrap(),
653            &BoardCell::Tile(Tile::from('M'))
654        );
655        assert_eq!(
656            board.get(Point::new(1, 2)).unwrap(),
657            &BoardCell::Tile(Tile::from('I'))
658        );
659        assert_eq!(
660            board.get(Point::new(2, 2)).unwrap(),
661            &BoardCell::Tile(Tile::from('N'))
662        );
663        assert_eq!(
664            board.get(Point::new(3, 2)).unwrap(),
665            &BoardCell::Tile(Tile::from('E'))
666        );
667        assert_eq!(
668            board.get(Point::new(4, 2)).unwrap(),
669            &BoardCell::Tile(Tile::from('D'))
670        );
671
672        Ok(())
673    }
674}