diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..0a00748 --- /dev/null +++ b/src/board.rs @@ -0,0 +1,886 @@ +use std::collections::HashSet; +use std::fmt; +use std::fmt::{Formatter, Write}; +use crate::constants::{ALL_LETTERS_BONUS, GRID_LENGTH, TRAY_LENGTH}; +use crate::dictionary::DictionaryImpl; + +#[derive(Clone, Copy)] +enum Direction { + Row, Column +} + +impl Direction { + fn invert(&self) -> Self { + match &self { + Direction::Row => {Direction::Column} + Direction::Column => {Direction::Row} + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Coordinates (pub u8, pub u8); + +impl Coordinates { + fn add(&self, direction: Direction, i: i8) -> Option { + let proposed = match direction { + Direction::Column => {(self.0 as i8, self.1 as i8+i)} + Direction::Row => {(self.0 as i8+i, self.1 as i8)} + }; + + if proposed.0 < 0 || proposed.0 >= GRID_LENGTH as i8 || proposed.1 < 0 || proposed.1 >= GRID_LENGTH as i8 { + None + } else{ + Some(Coordinates(proposed.0 as u8, proposed.1 as u8)) + } + } + + fn increment(&self, direction: Direction) -> Option{ + self.add(direction, 1) + } + + fn decrement(&self, direction: Direction) -> Option{ + self.add(direction, -1) + } + + fn map_to_index(&self) -> usize { + (self.0 + GRID_LENGTH*self.1) as usize + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Letter { + text: char, + points: u32, + ephemeral: bool, + is_blank: bool, +} + +impl Letter { + pub fn new_fixed(text: char, points: u32) -> Self { + Letter { + text, + points, + ephemeral: false, + is_blank: false, + } + } +} + +#[derive(Debug)] +pub enum CellType { + Normal, + DoubleWord, + DoubleLetter, + TripleLetter, + TripleWord, + Start, +} + +#[derive(Debug)] +pub struct Cell { + pub value: Option, + cell_type: CellType, + coordinates: Coordinates, +} + +#[derive(Debug)] +pub struct Board { + cells: Vec, +} + +pub struct Word<'a> { + cells: Vec<&'a Cell>, + coords: Coordinates, +} + +impl<'a> ToString for Word<'a> { + fn to_string(&self) -> String { + let mut text = String::with_capacity(self.cells.len()); + for cell in self.cells.as_slice() { + text.push(cell.value.as_ref().unwrap().text); + } + + text + + } +} + +impl <'a> Word<'a> { + + fn calculate_score(&self) -> u32{ + let mut multiplier = 1; + let mut unmultiplied_score = 0; + + for cell in self.cells.as_slice() { + let cell_value = cell.value.unwrap(); + if cell_value.ephemeral { + let cell_multiplier = + match cell.cell_type { + CellType::Normal => {1} + CellType::DoubleWord => { + multiplier *= 2; + 1 + } + CellType::DoubleLetter => {2} + CellType::TripleLetter => {3} + CellType::TripleWord => { + multiplier *= 3; + 1 + } + CellType::Start => { + multiplier *= 2; + 1 + } + }; + unmultiplied_score += cell_value.points * cell_multiplier; + } else { + // no cell multiplier unfortunately + unmultiplied_score += cell_value.points; + } + } + + unmultiplied_score * multiplier + } +} + +impl Board { + pub fn new() -> Self { + let mut cells = Vec::new(); + + + /// Since the board is symmetrical in both directions for the purposes of our logic we can keep our coordinates in one corner + /// + /// # Arguments + /// + /// * `x`: A coordinate + /// + /// returns: u8 The coordinate mapped onto the lower-half + fn map_to_corner(x: u8) -> u8 { + return if x > GRID_LENGTH / 2 { + GRID_LENGTH - x - 1 + } else { + x + } + } + + for i_orig in 0..GRID_LENGTH { + let i = map_to_corner(i_orig); + for j_orig in 0..GRID_LENGTH { + let j = map_to_corner(j_orig); + + let mut typee = CellType::Normal; + + // double word scores are diagonals + if i == j { + typee = CellType::DoubleWord; + } + + // Triple letters + if (i % 4 == 1) && j % 4 == 1 && !(i == 1 && j == 1) { + typee = CellType::TripleLetter; + } + + // Double letters + if (i % 4 == 2) && (j % 4 == 2) && !( + i == 2 && j == 2 + ) { + typee = CellType::DoubleLetter; + } + if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j)==3 && i.max(j) == 7) { + typee = CellType::DoubleLetter; + } + + // Triple word scores + if (i % 7 == 0) && (j % 7 == 0) { + typee = CellType::TripleWord; + } + + // Start + if i == 7 && j == 7 { + typee = CellType::Start; + } + + cells.push(Cell { + cell_type: typee, + value: None, + coordinates: Coordinates(j_orig, i_orig), + }) + + } + } + + Board {cells} + } + + pub fn get_cell(&self, coordinates: Coordinates) -> Result<&Cell, &str> { + if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH { + Err("x & y must be within the board's coordinates") + } else { + let index = coordinates.map_to_index(); + Ok(self.cells.get(index).unwrap()) + } + } + + pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, &str> { + if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH { + Err("x & y must be within the board's coordinates") + } else { + let index = coordinates.map_to_index(); + Ok(self.cells.get_mut(index).unwrap()) + } + } + + + pub fn calculate_scores(&self, dictionary: &DictionaryImpl) -> Result<(Vec<(Word, u32)>, u32), String> { + let (words, tiles_played) = self.find_played_words()?; + let mut words_and_scores = Vec::new(); + let mut total_score = 0; + + for word in words { + if !dictionary.contains_key(&word.to_string()) { + return Err(format!("{} is not a valid word", word.to_string())); + } + + let score = word.calculate_score(); + total_score += score; + words_and_scores.push((word, score)); + } + + if tiles_played == TRAY_LENGTH { + total_score += ALL_LETTERS_BONUS; + } + + Ok((words_and_scores, total_score)) + } + + pub fn find_played_words(&self) -> Result<(Vec, u8), &str> { + // We don't assume that the move is valid, so let's first establish that + + + // Let's first establish what rows and columns tiles were played in + let mut rows_played = HashSet::with_capacity(15); + let mut columns_played = HashSet::with_capacity(15); + let mut tiles_played = 0; + + for x in 0..GRID_LENGTH { + for y in 0..GRID_LENGTH { + let coords = Coordinates(x, y); + let cell = self.get_cell(coords).unwrap(); + match &cell.value { + Some(value) => { + if value.ephemeral { + rows_played.insert(x); + columns_played.insert(y); + tiles_played += 1; + } + } + _ => {} + } + } + } + + if rows_played.is_empty() { + return Err("Tiles need to be played") + } else if rows_played.len() > 1 && columns_played.len() > 1 { + return Err("Tiles need to be played on one row or column") + } + + let direction = if rows_played.len() > 1 { + Direction::Row + } else { + Direction::Column + }; + + let starting_row = *rows_played.iter().min().unwrap(); + let starting_column = *columns_played.iter().min().unwrap(); + + let mut starting_coords = Coordinates(starting_row, starting_column); + let main_word = self.find_word_at_position(starting_coords, direction).unwrap(); + + starting_coords = main_word.coords; + + let mut words = Vec::new(); + let mut observed_tiles_played = 0; + + // At this point we now know that we're at the start of the word and we have the direction. + // Now we'll head forward and look for every word that intersects one of the played tiles + for cell in main_word.cells.as_slice() { + if cell.value.as_ref().unwrap().ephemeral { + observed_tiles_played += 1; + let side_word = self.find_word_at_position(cell.coordinates, direction.invert()); + match side_word { + None => {} + Some(side_word) => { + if side_word.cells.len() > 1 { + words.push(side_word); + } + } + } + } + } + + // there are tiles not part of the main word + if observed_tiles_played != tiles_played { + return Err("Played tiles cannot have empty gap"); + } + + // don't want the case of a single letter word + if main_word.cells.len() > 1 { + words.push(main_word); + } else if words.is_empty() { + return Err("All words must be at least one letter"); + } + + + + + // need to verify that the play is 'anchored' + let mut anchored = false; + 'outer: for word in words.as_slice() { + for cell in word.cells.as_slice() { + // either one of the letters + if !cell.value.as_ref().unwrap().ephemeral || (cell.coordinates.0 == GRID_LENGTH / 2 && cell.coordinates.1 == GRID_LENGTH / 2){ + anchored = true; + break 'outer; + } + } + } + + if anchored { + Ok((words, tiles_played)) + } else { + return Err("Played tiles must be anchored to something") + } + + + } + + fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option { + // let's see how far we can backtrack to the start of the word + let mut times_moved = 0; + loop { + let one_back = start_coords.add(direction, -times_moved); + match one_back { + None => { break } + Some(new_coords) => { + let cell = self.get_cell(new_coords).unwrap(); + if cell.value.is_some(){ + times_moved += 1; + } else { + break + } + } + } + } + + if times_moved == 0 { + return None; + } + + start_coords = start_coords.add(direction, -times_moved + 1).unwrap(); + + // since we moved and we know that start_coords has started on a letter, we know we have a word + // we'll now keep track of the cells that form it + let mut cells = Vec::with_capacity(GRID_LENGTH as usize); + cells.push(self.get_cell(start_coords).unwrap()); + + loop { + let position = start_coords.add(direction, cells.len() as i8); + match position { + None => {break} + Some(x) => { + let cell = self.get_cell(x).unwrap(); + match cell.value { + None => {break} + Some(_) => { + cells.push(cell); + } + } + } + } + } + + Some(Word { + cells, + coords: start_coords, + }) + } + + +} + +impl fmt::Display for Board { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + + let mut str = String::new(); + + let normal = "\x1b[48;5;174m\x1b[38;5;0m"; + let triple_word = "\x1b[48;5;196m\x1b[38;5;0m"; + let double_word = "\x1b[48;5;204m\x1b[38;5;0m"; + let triple_letter = "\x1b[48;5;21m\x1b[38;5;15m"; + let double_letter = "\x1b[48;5;51m\x1b[38;5;0m"; + str.write_char('\n').unwrap(); + + for x in 0..GRID_LENGTH { + for y in 0..GRID_LENGTH { + let coords = Coordinates(x, y); + + let cell = self.get_cell(coords).unwrap(); + + let color = match cell.cell_type { + CellType::Normal => {normal} + CellType::DoubleWord => {double_word} + CellType::DoubleLetter => {double_letter} + CellType::TripleLetter => {triple_letter} + CellType::TripleWord => {triple_word} + CellType::Start => {double_word} + }; + + let content = match &cell.value { + None => {' '} + Some(letter) => {letter.text} + }; + + + str.write_str(color).unwrap(); + str.write_char(content).unwrap(); + + + } + str.write_str("\x1b[0m\n").unwrap(); + } + + write!(f, "{}", str) + + } +} + +#[cfg(test)] +mod tests { + use crate::dictionary::Dictionary; + use super::*; + + #[test] + fn test_cell_types() { + let board = Board::new(); + + assert!(matches!(board.get_cell(Coordinates(0, 0)).unwrap().cell_type, CellType::TripleWord)); + assert!(matches!(board.get_cell(Coordinates(1, 0)).unwrap().cell_type, CellType::Normal)); + assert!(matches!(board.get_cell(Coordinates(0, 1)).unwrap().cell_type, CellType::Normal)); + assert!(matches!(board.get_cell(Coordinates(1, 1)).unwrap().cell_type, CellType::DoubleWord)); + + assert!(matches!(board.get_cell(Coordinates(13, 13)).unwrap().cell_type, CellType::DoubleWord)); + assert!(matches!(board.get_cell(Coordinates(14, 14)).unwrap().cell_type, CellType::TripleWord)); + assert!(matches!(board.get_cell(Coordinates(11, 14)).unwrap().cell_type, CellType::DoubleLetter)); + + assert!(matches!(board.get_cell(Coordinates(7, 7)).unwrap().cell_type, CellType::Start)); + assert!(matches!(board.get_cell(Coordinates(8, 6)).unwrap().cell_type, CellType::DoubleLetter)); + assert!(matches!(board.get_cell(Coordinates(5, 9)).unwrap().cell_type, CellType::TripleLetter)); + } + + + #[test] + fn test_cell_coordinates() { + let board = Board::new(); + + for x in 0..GRID_LENGTH { + for y in 0..GRID_LENGTH { + let cell = board.get_cell(Coordinates(x, y)).unwrap(); + let coords = cell.coordinates; + + assert_eq!(x, coords.0); + assert_eq!(y, coords.1); + } + } + } + + #[test] + fn test_word_finding_at_position() { + let mut board = Board::new(); + + board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 0)); + board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(Letter::new_fixed('O', 0)); + board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(Letter::new_fixed('E', 0)); + board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed('L', 0)); + + board.get_cell_mut(Coordinates(0, 0)).unwrap().value = Some(Letter::new_fixed('I', 0)); + board.get_cell_mut(Coordinates(1, 0)).unwrap().value = Some(Letter::new_fixed('S', 0)); + + board.get_cell_mut(Coordinates(3, 0)).unwrap().value = Some(Letter::new_fixed('C', 0)); + board.get_cell_mut(Coordinates(4, 0)).unwrap().value = Some(Letter::new_fixed('O', 0)); + board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 0)); + board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 0)); + + + board.get_cell_mut(Coordinates(9, 8)).unwrap().value = Some(Letter::new_fixed('G', 0)); + board.get_cell_mut(Coordinates(10, 8)).unwrap().value = Some(Letter::new_fixed('G', 0)); + + for x in vec![6, 7, 8, 9] { + println!("x is {}", x); + let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column); + match first_word { + None => { panic!("Expected to find word JOEL") } + Some(x) => { + assert_eq!(x.coords.0, 8); + assert_eq!(x.coords.1, 6); + + assert_eq!(x.to_string(), "JOEL"); + } + } + } + + let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row); + match single_letter_word { + None => { panic!("Expected to find letter L") } + Some(x) => { + assert_eq!(x.coords.0, 8); + assert_eq!(x.coords.1, 9); + + assert_eq!(x.to_string(), "L"); + } + } + + for x in vec![0, 1] { + println!("x is {}", x); + let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); + match word { + None => { panic!("Expected to find word IS") } + Some(x) => { + assert_eq!(x.coords.0, 0); + assert_eq!(x.coords.1, 0); + + assert_eq!(x.to_string(), "IS"); + } + } + } + + for x in vec![3, 4, 5, 6] { + println!("x is {}", x); + let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); + match word { + None => { panic!("Expected to find word COOL") } + Some(x) => { + assert_eq!(x.coords.0, 3); + assert_eq!(x.coords.1, 0); + + assert_eq!(x.to_string(), "COOL"); + } + } + } + + let no_word = board.find_word_at_position(Coordinates(2, 0), Direction::Row); + assert!(no_word.is_none()); + + let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row); + match word { + None => { panic!("Expected to find word EGG") } + Some(x) => { + assert_eq!(x.coords.0, 8); + assert_eq!(x.coords.1, 8); + + assert_eq!(x.to_string(), "EGG"); + } + } + } + + #[test] + fn test_word_finding_one_letter() { + let mut board = Board::new(); + + board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter { + text: 'I', + points: 1, + ephemeral: true, + is_blank: false, + }); + + match board.find_played_words() { + Ok(_) => {panic!("Expected error")} + Err(e) => {assert_eq!(e, "All words must be at least one letter");} + } + + board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter { + text: 'I', + points: 1, + ephemeral: false, // fixed now + is_blank: false, + }); + + board.get_cell_mut(Coordinates(7, 8)).unwrap().value = Some(Letter { + text: 'S', + points: 1, + ephemeral: true, + is_blank: false, + }); + + let (words, tiles_played) = board.find_played_words().unwrap(); + assert_eq!(tiles_played, 1); + assert_eq!(words.len(), 1); + let word = words.first().unwrap(); + assert_eq!(word.calculate_score(), 2); + + // making fixed + board.get_cell_mut(Coordinates(7, 8)).unwrap().value = Some(Letter { + text: 'S', + points: 1, + ephemeral: false, + is_blank: false, + }); + + // trying other orientation + board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(Letter { + text: 'S', + points: 1, + ephemeral: true, + is_blank: false, + }); + + let (words, tiles_played) = board.find_played_words().unwrap(); + assert_eq!(tiles_played, 1); + assert_eq!(words.len(), 1); + let word = words.first().unwrap(); + assert_eq!(word.calculate_score(), 2); + } + + #[test] + fn test_word_finding_anchor() { + let mut board = Board::new(); + + fn make_letter(x: char, ephemeral: bool) -> Letter { + Letter { + text: x, + points: 0, + ephemeral, + is_blank: false, + } + } + + board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(make_letter('J', true)); + board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true)); + board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true)); + board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true)); + + let words = board.find_played_words(); + match words { + Ok(_) => {panic!("Expected the not-anchored error")} + Err(x) => {assert_eq!(x, "Played tiles must be anchored to something")} + } + + // Adding anchor + board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false)); + assert!(board.find_played_words().is_ok()); + + board = Board::new(); + + // we go through center so this is anchored + board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(make_letter('J', true)); + board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true)); + board.get_cell_mut(Coordinates(9, 7)).unwrap().value = Some(make_letter('E', true)); + board.get_cell_mut(Coordinates(10, 7)).unwrap().value = Some(make_letter('L', true)); + + assert!(board.find_played_words().is_ok()); + } + + + #[test] + fn test_word_finding_with_break() { + // Verify that if I play my tiles on one row or column but with a break in-between I get an error + + let mut board = Board::new(); + + fn make_letter(x: char, ephemeral: bool) -> Letter { + Letter { + text: x, + points: 0, + ephemeral, + is_blank: false, + } + } + + board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 0)); + board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true)); + board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true)); + board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed( 'L', 0)); + + board.get_cell_mut(Coordinates(8, 11)).unwrap().value = Some(make_letter('I', true)); + board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0)); + + let words = board.find_played_words(); + match words { + Ok(_) => {panic!("Expected to find an error!")} + Err(x) => { + assert_eq!(x, "Played tiles cannot have empty gap") + } + } + + } + + + #[test] + fn test_word_finding_whole_board() { + let mut board = Board::new(); + + fn make_letter(x: char, ephemeral: bool, points: u32) -> Letter { + Letter { + text: x, + points, + ephemeral, + is_blank: false, + } + } + + let words = board.find_played_words(); + match words { + Ok(_) => {panic!("Expected to find no words")} + Err(x) => {assert_eq!(x, "Tiles need to be played")} + } + + board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 8)); + board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true, 1)); + board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true, 1)); + board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed( 'L', 1)); + + board.get_cell_mut(Coordinates(0, 0)).unwrap().value = Some(Letter::new_fixed('I', 1)); + board.get_cell_mut(Coordinates(1, 0)).unwrap().value = Some(Letter::new_fixed('S', 1)); + + board.get_cell_mut(Coordinates(3, 0)).unwrap().value = Some(Letter::new_fixed('C', 3)); + board.get_cell_mut(Coordinates(4, 0)).unwrap().value = Some(Letter::new_fixed('O', 1)); + board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 1)); + board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1)); + + fn check_board(board: &mut Board, inverted: bool) { + let dictionary = DictionaryImpl::create("resources/dictionary.csv"); + println!("{}", board); + let words = board.find_played_words(); + match words { + Ok((x, tiles_played)) => { + assert_eq!(tiles_played, 2); + assert_eq!(x.len(), 1); + let word = x.get(0).unwrap(); + assert_eq!(word.to_string(), "JOEL"); + assert!(!dictionary.is_word_valid(word)); + + assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); + } + Err(e) => { panic!("Expected to find a word to play; found error {}", e) } + } + + let maybe_invert = |coords: Coordinates| { + if inverted { + return Coordinates(coords.1, coords.0); + } + return coords; + }; + + let maybe_invert_direction = |direction: Direction| { + if inverted { + return direction.invert(); + } + return direction; + }; + + board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(Letter::new_fixed('G', 2)); + board.get_cell_mut(maybe_invert(Coordinates(10, 8))).unwrap().value = Some(Letter::new_fixed('G', 2)); + + let word = board.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row)); + match word { + None => {panic!("Expected to find word EGG")} + Some(x) => { + assert_eq!(x.coords.0, 8); + assert_eq!(x.coords.1, 8); + + assert_eq!(x.to_string(), "EGG"); + assert_eq!(x.calculate_score(), 2 + 2 + 2); + assert!(dictionary.is_word_valid(&x)); + + } + } + + let words = board.find_played_words(); + match words { + Ok((x, tiled_played)) => { + assert_eq!(tiled_played, 2); + assert_eq!(x.len(), 2); + let word = x.get(0).unwrap(); + assert_eq!(word.to_string(), "EGG"); + assert_eq!(word.calculate_score(), 2 + 2 + 2); + assert!(dictionary.is_word_valid(word)); + + let word = x.get(1).unwrap(); + assert_eq!(word.to_string(), "JOEL"); + assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); + assert!(!dictionary.is_word_valid(word)); + } + Err(e) => { panic!("Expected to find a word to play; found error {}", e) } + } + + let scores = board.calculate_scores(&dictionary); + match scores { + Ok(_) => {panic!("Expected an error")} + Err(e) => {assert_eq!(e, "JOEL is not a valid word")} + } + + let mut alt_dictionary = DictionaryImpl::new(); + alt_dictionary.insert("JOEL".to_string(), 0.5); + alt_dictionary.insert("EGG".to_string(), 0.5); + + + let scores = board.calculate_scores(&alt_dictionary); + match scores { + Ok((words, total_score)) => { + assert_eq!(words.len(), 2); + let (word, score) = words.get(0).unwrap(); + assert_eq!(word.to_string(), "EGG"); + assert_eq!(word.calculate_score(), 2 + 2 + 2); + assert_eq!(*score, 2 + 2 + 2); + + let (word, score) = words.get(1).unwrap(); + assert_eq!(word.to_string(), "JOEL"); + assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); + assert_eq!(*score, 8 + 1 + 2 + 1); + + assert_eq!(total_score, 18); + } + Err(e) => {panic!("Wasn't expecting to encounter error {e}")} + } + + + // replace one of the 'G' in EGG with an ephemeral to trigger an error + board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(make_letter('G', true, 2)); + + let words = board.find_played_words(); + match words { + Ok(_) => { panic!("Expected error as we played tiles in multiple rows and columns") } + Err(e) => { assert_eq!(e, "Tiles need to be played on one row or column") } + } + } + + // make a copy of the board now with x and y swapped + let mut inverted_board = Board::new(); + + for x in 0..GRID_LENGTH { + for y in 0..GRID_LENGTH { + let cell_original = board.get_cell(Coordinates(x, y)).unwrap(); + let cell_new = inverted_board.get_cell_mut(Coordinates(y, x)).unwrap(); + + match &cell_original.value { + None => {} + Some(x) => { + cell_new.value = Some(*x); + } + } + + } + } + + println!("Checking original board"); + check_board(&mut board, false); + + println!("Checking inverted board"); + check_board(&mut inverted_board, true); + + + } + +} \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..7e3d29f --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,3 @@ +pub const GRID_LENGTH: u8 = 15; +pub const TRAY_LENGTH: u8 = 7; +pub const ALL_LETTERS_BONUS: u32 = 50; \ No newline at end of file diff --git a/src/dictionary.rs b/src/dictionary.rs new file mode 100644 index 0000000..4a63e7c --- /dev/null +++ b/src/dictionary.rs @@ -0,0 +1,124 @@ +use std::collections::{HashMap, HashSet}; +use std::str::FromStr; +use crate::board::Word; + +pub trait Dictionary { + fn create(path: &str) -> Self; + fn filter_to_sub_dictionary(&self, proportion: f64) -> Self; + fn substring_set(&self) -> HashSet<&str>; + fn is_word_valid(&self, word: &Word) -> bool; +} +pub type DictionaryImpl = HashMap; + +impl Dictionary for DictionaryImpl{ + + fn create(path: &str) -> Self { + let mut reader = csv::Reader::from_path(path).unwrap(); + let mut map = HashMap::new(); + + for result in reader.records() { + let record = result.unwrap(); + let word = record.get(0).unwrap().to_string(); + + let score = record.get(1).unwrap(); + let score = f64::from_str(score).unwrap(); + + map.insert(word, score); + + } + + map + } + + fn filter_to_sub_dictionary(&self, proportion: f64) -> Self { + let mut map = HashMap::new(); + + for (word, score) in self.iter() { + if *score >= proportion { + map.insert(word.clone(), *score); + } + } + + map + + } + + fn substring_set(&self) -> HashSet<&str> { + let mut set = HashSet::new(); + + for (word, _score) in self.iter() { + for j in 0..word.len() { + for k in (j+1)..(word.len()+1) { + set.insert(&word[j..k]); + } + } + + } + + set + } + + fn is_word_valid(&self, word: &Word) -> bool { + let text = word.to_string(); + self.contains_key(&text) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + + #[test] + fn test_dictionary() { + let dictionary = HashMap::create("resources/dictionary.csv"); + + assert_eq!(dictionary.len(), 279429); + + assert!(dictionary.contains_key("AA")); + assert!(dictionary.contains_key("AARDVARK")); + + assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001) + + } + + #[test] + fn test_dictionary_sets() { + let mut dictionary = HashMap::new(); + dictionary.insert("JOEL".to_string(), 0.7); + dictionary.insert("JOHN".to_string(), 0.5); + dictionary.insert("XYZ".to_string(), 0.1); + + let dictionary = dictionary.filter_to_sub_dictionary(0.3); + assert_eq!(dictionary.len(), 2); + assert!(dictionary.contains_key("JOEL")); + assert!(dictionary.contains_key("JOHN")); + + let set = dictionary.substring_set(); + + assert!(set.contains("J")); + assert!(set.contains("O")); + assert!(set.contains("E")); + assert!(set.contains("L")); + assert!(set.contains("H")); + assert!(set.contains("N")); + + assert!(set.contains("JO")); + assert!(set.contains("OE")); + assert!(set.contains("EL")); + assert!(set.contains("OH")); + assert!(set.contains("HN")); + + assert!(set.contains("JOE")); + assert!(set.contains("OEL")); + assert!(set.contains("JOH")); + assert!(set.contains("OHN")); + + + assert!(!set.contains("XY")); + assert!(!set.contains("JH")); + assert!(!set.contains("JE")); + + } + +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 21f6771..108ab85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,1009 +1,5 @@ -use std::collections::{HashMap, HashSet}; -use std::fmt; -use std::fmt::{Formatter, Write}; -use std::str::FromStr; -pub const GRID_LENGTH: u8 = 15; -pub const TRAY_LENGTH: u8 = 7; -pub const ALL_LETTERS_BONUS: u32 = 50; +pub mod constants; +pub mod board; +pub mod dictionary; -#[derive(Clone, Copy)] -enum Direction { - Row, Column -} - -impl Direction { - fn invert(&self) -> Self { - match &self { - Direction::Row => {Direction::Column} - Direction::Column => {Direction::Row} - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct Coordinates (pub u8, pub u8); - -impl Coordinates { - fn add(&self, direction: Direction, i: i8) -> Option { - let proposed = match direction { - Direction::Column => {(self.0 as i8, self.1 as i8+i)} - Direction::Row => {(self.0 as i8+i, self.1 as i8)} - }; - - if proposed.0 < 0 || proposed.0 >= GRID_LENGTH as i8 || proposed.1 < 0 || proposed.1 >= GRID_LENGTH as i8 { - None - } else{ - Some(Coordinates(proposed.0 as u8, proposed.1 as u8)) - } - } - - fn increment(&self, direction: Direction) -> Option{ - self.add(direction, 1) - } - - fn decrement(&self, direction: Direction) -> Option{ - self.add(direction, -1) - } - - fn map_to_index(&self) -> usize { - (self.0 + GRID_LENGTH*self.1) as usize - } -} - -#[derive(Debug, Copy, Clone)] -pub struct Letter { - text: char, - points: u32, - ephemeral: bool, - is_blank: bool, -} - -impl Letter { - pub fn new_fixed(text: char, points: u32) -> Self { - Letter { - text, - points, - ephemeral: false, - is_blank: false, - } - } -} - -#[derive(Debug)] -pub enum CellType { - Normal, - DoubleWord, - DoubleLetter, - TripleLetter, - TripleWord, - Start, -} - -#[derive(Debug)] -pub struct Cell { - pub value: Option, - cell_type: CellType, - coordinates: Coordinates, -} - -trait Dictionary { - fn create(path: &str) -> Self; - fn filter_to_sub_dictionary(&self, proportion: f64) -> Self; - fn substring_set(&self) -> HashSet<&str>; - fn is_word_valid(&self, word: &Word) -> bool; -} -type DictionaryImpl = HashMap; - -impl Dictionary for DictionaryImpl{ - - fn create(path: &str) -> Self { - let mut reader = csv::Reader::from_path(path).unwrap(); - let mut map = HashMap::new(); - - for result in reader.records() { - let record = result.unwrap(); - let word = record.get(0).unwrap().to_string(); - - let score = record.get(1).unwrap(); - let score = f64::from_str(score).unwrap(); - - map.insert(word, score); - - } - - map - } - - fn filter_to_sub_dictionary(&self, proportion: f64) -> Self { - let mut map = HashMap::new(); - - for (word, score) in self.iter() { - if *score >= proportion { - map.insert(word.clone(), *score); - } - } - - map - - } - - fn substring_set(&self) -> HashSet<&str> { - let mut set = HashSet::new(); - - for (word, _score) in self.iter() { - for j in 0..word.len() { - for k in (j+1)..(word.len()+1) { - set.insert(&word[j..k]); - } - } - - } - - set - } - - fn is_word_valid(&self, word: &Word) -> bool { - let text = word.to_string(); - self.contains_key(&text) - } -} - - -#[derive(Debug)] -pub struct Board { - cells: Vec, -} - -pub struct Word<'a> { - cells: Vec<&'a Cell>, - coords: Coordinates, -} - -impl<'a> ToString for Word<'a> { - fn to_string(&self) -> String { - let mut text = String::with_capacity(self.cells.len()); - for cell in self.cells.as_slice() { - text.push(cell.value.as_ref().unwrap().text); - } - - text - - } -} - -impl <'a> Word<'a> { - - fn calculate_score(&self) -> u32{ - let mut multiplier = 1; - let mut unmultiplied_score = 0; - - for cell in self.cells.as_slice() { - let cell_value = cell.value.unwrap(); - if cell_value.ephemeral { - let cell_multiplier = - match cell.cell_type { - CellType::Normal => {1} - CellType::DoubleWord => { - multiplier *= 2; - 1 - } - CellType::DoubleLetter => {2} - CellType::TripleLetter => {3} - CellType::TripleWord => { - multiplier *= 3; - 1 - } - CellType::Start => { - multiplier *= 2; - 1 - } - }; - unmultiplied_score += cell_value.points * cell_multiplier; - } else { - // no cell multiplier unfortunately - unmultiplied_score += cell_value.points; - } - } - - unmultiplied_score * multiplier - } -} - -impl Board { - pub fn new() -> Self { - let mut cells = Vec::new(); - - - /// Since the board is symmetrical in both directions for the purposes of our logic we can keep our coordinates in one corner - /// - /// # Arguments - /// - /// * `x`: A coordinate - /// - /// returns: u8 The coordinate mapped onto the lower-half - fn map_to_corner(x: u8) -> u8 { - return if x > GRID_LENGTH / 2 { - GRID_LENGTH - x - 1 - } else { - x - } - } - - for i_orig in 0..GRID_LENGTH { - let i = map_to_corner(i_orig); - for j_orig in 0..GRID_LENGTH { - let j = map_to_corner(j_orig); - - let mut typee = CellType::Normal; - - // double word scores are diagonals - if i == j { - typee = CellType::DoubleWord; - } - - // Triple letters - if (i % 4 == 1) && j % 4 == 1 && !(i == 1 && j == 1) { - typee = CellType::TripleLetter; - } - - // Double letters - if (i % 4 == 2) && (j % 4 == 2) && !( - i == 2 && j == 2 - ) { - typee = CellType::DoubleLetter; - } - if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j)==3 && i.max(j) == 7) { - typee = CellType::DoubleLetter; - } - - // Triple word scores - if (i % 7 == 0) && (j % 7 == 0) { - typee = CellType::TripleWord; - } - - // Start - if i == 7 && j == 7 { - typee = CellType::Start; - } - - cells.push(Cell { - cell_type: typee, - value: None, - coordinates: Coordinates(j_orig, i_orig), - }) - - } - } - - Board {cells} - } - - pub fn get_cell(&self, coordinates: Coordinates) -> Result<&Cell, &str> { - if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH { - Err("x & y must be within the board's coordinates") - } else { - let index = coordinates.map_to_index(); - Ok(self.cells.get(index).unwrap()) - } - } - - pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, &str> { - if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH { - Err("x & y must be within the board's coordinates") - } else { - let index = coordinates.map_to_index(); - Ok(self.cells.get_mut(index).unwrap()) - } - } - - - pub fn calculate_scores(&self, dictionary: &DictionaryImpl) -> Result<(Vec<(Word, u32)>, u32), String> { - let (words, tiles_played) = self.find_played_words()?; - let mut words_and_scores = Vec::new(); - let mut total_score = 0; - - for word in words { - if !dictionary.contains_key(&word.to_string()) { - return Err(format!("{} is not a valid word", word.to_string())); - } - - let score = word.calculate_score(); - total_score += score; - words_and_scores.push((word, score)); - } - - if tiles_played == TRAY_LENGTH { - total_score += ALL_LETTERS_BONUS; - } - - Ok((words_and_scores, total_score)) - } - - pub fn find_played_words(&self) -> Result<(Vec, u8), &str> { - // We don't assume that the move is valid, so let's first establish that - - - // Let's first establish what rows and columns tiles were played in - let mut rows_played = HashSet::with_capacity(15); - let mut columns_played = HashSet::with_capacity(15); - let mut tiles_played = 0; - - for x in 0..GRID_LENGTH { - for y in 0..GRID_LENGTH { - let coords = Coordinates(x, y); - let cell = self.get_cell(coords).unwrap(); - match &cell.value { - Some(value) => { - if value.ephemeral { - rows_played.insert(x); - columns_played.insert(y); - tiles_played += 1; - } - } - _ => {} - } - } - } - - if rows_played.is_empty() { - return Err("Tiles need to be played") - } else if rows_played.len() > 1 && columns_played.len() > 1 { - return Err("Tiles need to be played on one row or column") - } - - let direction = if rows_played.len() > 1 { - Direction::Row - } else { - Direction::Column - }; - - let starting_row = *rows_played.iter().min().unwrap(); - let starting_column = *columns_played.iter().min().unwrap(); - - let mut starting_coords = Coordinates(starting_row, starting_column); - let main_word = self.find_word_at_position(starting_coords, direction).unwrap(); - - starting_coords = main_word.coords; - - let mut words = Vec::new(); - let mut observed_tiles_played = 0; - - // At this point we now know that we're at the start of the word and we have the direction. - // Now we'll head forward and look for every word that intersects one of the played tiles - for cell in main_word.cells.as_slice() { - if cell.value.as_ref().unwrap().ephemeral { - observed_tiles_played += 1; - let side_word = self.find_word_at_position(cell.coordinates, direction.invert()); - match side_word { - None => {} - Some(side_word) => { - if side_word.cells.len() > 1 { - words.push(side_word); - } - } - } - } - } - - // there are tiles not part of the main word - if observed_tiles_played != tiles_played { - return Err("Played tiles cannot have empty gap"); - } - - // don't want the case of a single letter word - if main_word.cells.len() > 1 { - words.push(main_word); - } else if words.is_empty() { - return Err("All words must be at least one letter"); - } - - - - - // need to verify that the play is 'anchored' - let mut anchored = false; - 'outer: for word in words.as_slice() { - for cell in word.cells.as_slice() { - // either one of the letters - if !cell.value.as_ref().unwrap().ephemeral || (cell.coordinates.0 == GRID_LENGTH / 2 && cell.coordinates.1 == GRID_LENGTH / 2){ - anchored = true; - break 'outer; - } - } - } - - if anchored { - Ok((words, tiles_played)) - } else { - return Err("Played tiles must be anchored to something") - } - - - } - - fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option { - // let's see how far we can backtrack to the start of the word - let mut times_moved = 0; - loop { - let one_back = start_coords.add(direction, -times_moved); - match one_back { - None => { break } - Some(new_coords) => { - let cell = self.get_cell(new_coords).unwrap(); - if cell.value.is_some(){ - times_moved += 1; - } else { - break - } - } - } - } - - if times_moved == 0 { - return None; - } - - start_coords = start_coords.add(direction, -times_moved + 1).unwrap(); - - // since we moved and we know that start_coords has started on a letter, we know we have a word - // we'll now keep track of the cells that form it - let mut cells = Vec::with_capacity(GRID_LENGTH as usize); - cells.push(self.get_cell(start_coords).unwrap()); - - loop { - let position = start_coords.add(direction, cells.len() as i8); - match position { - None => {break} - Some(x) => { - let cell = self.get_cell(x).unwrap(); - match cell.value { - None => {break} - Some(_) => { - cells.push(cell); - } - } - } - } - } - - Some(Word { - cells, - coords: start_coords, - }) - } - - -} - -impl fmt::Display for Board { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - - let mut str = String::new(); - - let normal = "\x1b[48;5;174m\x1b[38;5;0m"; - let triple_word = "\x1b[48;5;196m\x1b[38;5;0m"; - let double_word = "\x1b[48;5;204m\x1b[38;5;0m"; - let triple_letter = "\x1b[48;5;21m\x1b[38;5;15m"; - let double_letter = "\x1b[48;5;51m\x1b[38;5;0m"; - str.write_char('\n').unwrap(); - - for x in 0..GRID_LENGTH { - for y in 0..GRID_LENGTH { - let coords = Coordinates(x, y); - - let cell = self.get_cell(coords).unwrap(); - - let color = match cell.cell_type { - CellType::Normal => {normal} - CellType::DoubleWord => {double_word} - CellType::DoubleLetter => {double_letter} - CellType::TripleLetter => {triple_letter} - CellType::TripleWord => {triple_word} - CellType::Start => {double_word} - }; - - let content = match &cell.value { - None => {' '} - Some(letter) => {letter.text} - }; - - - str.write_str(color).unwrap(); - str.write_char(content).unwrap(); - - - } - str.write_str("\x1b[0m\n").unwrap(); - } - - write!(f, "{}", str) - - } -} - - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_cell_types() { - let board = Board::new(); - - assert!(matches!(board.get_cell(Coordinates(0, 0)).unwrap().cell_type, CellType::TripleWord)); - assert!(matches!(board.get_cell(Coordinates(1, 0)).unwrap().cell_type, CellType::Normal)); - assert!(matches!(board.get_cell(Coordinates(0, 1)).unwrap().cell_type, CellType::Normal)); - assert!(matches!(board.get_cell(Coordinates(1, 1)).unwrap().cell_type, CellType::DoubleWord)); - - assert!(matches!(board.get_cell(Coordinates(13, 13)).unwrap().cell_type, CellType::DoubleWord)); - assert!(matches!(board.get_cell(Coordinates(14, 14)).unwrap().cell_type, CellType::TripleWord)); - assert!(matches!(board.get_cell(Coordinates(11, 14)).unwrap().cell_type, CellType::DoubleLetter)); - - assert!(matches!(board.get_cell(Coordinates(7, 7)).unwrap().cell_type, CellType::Start)); - assert!(matches!(board.get_cell(Coordinates(8, 6)).unwrap().cell_type, CellType::DoubleLetter)); - assert!(matches!(board.get_cell(Coordinates(5, 9)).unwrap().cell_type, CellType::TripleLetter)); - } - - #[test] - fn test_cell_coordinates() { - let board = Board::new(); - - for x in 0..GRID_LENGTH { - for y in 0..GRID_LENGTH { - let cell = board.get_cell(Coordinates(x, y)).unwrap(); - let coords = cell.coordinates; - - assert_eq!(x, coords.0); - assert_eq!(y, coords.1); - } - } - } - - #[test] - fn test_word_finding_at_position() { - let mut board = Board::new(); - - board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 0)); - board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(Letter::new_fixed('O', 0)); - board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(Letter::new_fixed( 'E', 0)); - board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed( 'L', 0)); - - board.get_cell_mut(Coordinates(0, 0)).unwrap().value = Some(Letter::new_fixed('I', 0)); - board.get_cell_mut(Coordinates(1, 0)).unwrap().value = Some(Letter::new_fixed('S', 0)); - - board.get_cell_mut(Coordinates(3, 0)).unwrap().value = Some(Letter::new_fixed('C', 0)); - board.get_cell_mut(Coordinates(4, 0)).unwrap().value = Some(Letter::new_fixed('O', 0)); - board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 0)); - board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 0)); - - - board.get_cell_mut(Coordinates(9, 8)).unwrap().value = Some(Letter::new_fixed( 'G', 0)); - board.get_cell_mut(Coordinates(10, 8)).unwrap().value = Some(Letter::new_fixed( 'G', 0)); - - for x in vec![6, 7, 8, 9] { - println!("x is {}", x); - let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column); - match first_word { - None => {panic!("Expected to find word JOEL")} - Some(x) => { - assert_eq!(x.coords.0, 8); - assert_eq!(x.coords.1, 6); - - assert_eq!(x.to_string(), "JOEL"); - - } - } - } - - let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row); - match single_letter_word { - None => {panic!("Expected to find letter L")} - Some(x) => { - assert_eq!(x.coords.0, 8); - assert_eq!(x.coords.1, 9); - - assert_eq!(x.to_string(), "L"); - } - } - - for x in vec![0, 1] { - println!("x is {}", x); - let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); - match word { - None => {panic!("Expected to find word IS")} - Some(x) => { - assert_eq!(x.coords.0, 0); - assert_eq!(x.coords.1, 0); - - assert_eq!(x.to_string(), "IS"); - - } - } - } - - for x in vec![3, 4, 5, 6] { - println!("x is {}", x); - let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); - match word { - None => {panic!("Expected to find word COOL")} - Some(x) => { - assert_eq!(x.coords.0, 3); - assert_eq!(x.coords.1, 0); - - assert_eq!(x.to_string(), "COOL"); - - } - } - } - - let no_word = board.find_word_at_position(Coordinates(2, 0), Direction::Row); - assert!(no_word.is_none()); - - let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row); - match word { - None => {panic!("Expected to find word EGG")} - Some(x) => { - assert_eq!(x.coords.0, 8); - assert_eq!(x.coords.1, 8); - - assert_eq!(x.to_string(), "EGG"); - - } - } - - - - } - - #[test] - fn test_word_finding_one_letter() { - let mut board = Board::new(); - - board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter { - text: 'I', - points: 1, - ephemeral: true, - is_blank: false, - }); - - match board.find_played_words() { - Ok(_) => {panic!("Expected error")} - Err(e) => {assert_eq!(e, "All words must be at least one letter");} - } - - board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter { - text: 'I', - points: 1, - ephemeral: false, // fixed now - is_blank: false, - }); - - board.get_cell_mut(Coordinates(7, 8)).unwrap().value = Some(Letter { - text: 'S', - points: 1, - ephemeral: true, - is_blank: false, - }); - - let (words, tiles_played) = board.find_played_words().unwrap(); - assert_eq!(tiles_played, 1); - assert_eq!(words.len(), 1); - let word = words.first().unwrap(); - assert_eq!(word.calculate_score(), 2); - - // making fixed - board.get_cell_mut(Coordinates(7, 8)).unwrap().value = Some(Letter { - text: 'S', - points: 1, - ephemeral: false, - is_blank: false, - }); - - // trying other orientation - board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(Letter { - text: 'S', - points: 1, - ephemeral: true, - is_blank: false, - }); - - let (words, tiles_played) = board.find_played_words().unwrap(); - assert_eq!(tiles_played, 1); - assert_eq!(words.len(), 1); - let word = words.first().unwrap(); - assert_eq!(word.calculate_score(), 2); - } - - #[test] - fn test_word_finding_anchor() { - let mut board = Board::new(); - - fn make_letter(x: char, ephemeral: bool) -> Letter { - Letter { - text: x, - points: 0, - ephemeral, - is_blank: false, - } - } - - board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(make_letter('J', true)); - board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true)); - board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true)); - board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true)); - - let words = board.find_played_words(); - match words { - Ok(_) => {panic!("Expected the not-anchored error")} - Err(x) => {assert_eq!(x, "Played tiles must be anchored to something")} - } - - // Adding anchor - board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false)); - assert!(board.find_played_words().is_ok()); - - board = Board::new(); - - // we go through center so this is anchored - board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(make_letter('J', true)); - board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true)); - board.get_cell_mut(Coordinates(9, 7)).unwrap().value = Some(make_letter('E', true)); - board.get_cell_mut(Coordinates(10, 7)).unwrap().value = Some(make_letter('L', true)); - - assert!(board.find_played_words().is_ok()); - } - - #[test] - fn test_word_finding_with_break() { - // Verify that if I play my tiles on one row or column but with a break in-between I get an error - - let mut board = Board::new(); - - fn make_letter(x: char, ephemeral: bool) -> Letter { - Letter { - text: x, - points: 0, - ephemeral, - is_blank: false, - } - } - - board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 0)); - board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true)); - board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true)); - board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed( 'L', 0)); - - board.get_cell_mut(Coordinates(8, 11)).unwrap().value = Some(make_letter('I', true)); - board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0)); - - let words = board.find_played_words(); - match words { - Ok(_) => {panic!("Expected to find an error!")} - Err(x) => { - assert_eq!(x, "Played tiles cannot have empty gap") - } - } - - } - - - #[test] - fn test_word_finding_whole_board() { - let mut board = Board::new(); - - fn make_letter(x: char, ephemeral: bool, points: u32) -> Letter { - Letter { - text: x, - points, - ephemeral, - is_blank: false, - } - } - - let words = board.find_played_words(); - match words { - Ok(_) => {panic!("Expected to find no words")} - Err(x) => {assert_eq!(x, "Tiles need to be played")} - } - - board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 8)); - board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true, 1)); - board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true, 1)); - board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed( 'L', 1)); - - board.get_cell_mut(Coordinates(0, 0)).unwrap().value = Some(Letter::new_fixed('I', 1)); - board.get_cell_mut(Coordinates(1, 0)).unwrap().value = Some(Letter::new_fixed('S', 1)); - - board.get_cell_mut(Coordinates(3, 0)).unwrap().value = Some(Letter::new_fixed('C', 3)); - board.get_cell_mut(Coordinates(4, 0)).unwrap().value = Some(Letter::new_fixed('O', 1)); - board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 1)); - board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1)); - - fn check_board(board: &mut Board, inverted: bool) { - let dictionary = HashMap::create("resources/dictionary.csv"); - println!("{}", board); - let words = board.find_played_words(); - match words { - Ok((x, tiles_played)) => { - assert_eq!(tiles_played, 2); - assert_eq!(x.len(), 1); - let word = x.get(0).unwrap(); - assert_eq!(word.to_string(), "JOEL"); - assert!(!dictionary.is_word_valid(word)); - - assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); - } - Err(e) => { panic!("Expected to find a word to play; found error {}", e) } - } - - let maybe_invert = |coords: Coordinates| { - if inverted { - return Coordinates(coords.1, coords.0); - } - return coords; - }; - - let maybe_invert_direction = |direction: Direction| { - if inverted { - return direction.invert(); - } - return direction; - }; - - board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(Letter::new_fixed('G', 2)); - board.get_cell_mut(maybe_invert(Coordinates(10, 8))).unwrap().value = Some(Letter::new_fixed('G', 2)); - - let word = board.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row)); - match word { - None => {panic!("Expected to find word EGG")} - Some(x) => { - assert_eq!(x.coords.0, 8); - assert_eq!(x.coords.1, 8); - - assert_eq!(x.to_string(), "EGG"); - assert_eq!(x.calculate_score(), 2 + 2 + 2); - assert!(dictionary.is_word_valid(&x)); - - } - } - - let words = board.find_played_words(); - match words { - Ok((x, tiled_played)) => { - assert_eq!(tiled_played, 2); - assert_eq!(x.len(), 2); - let word = x.get(0).unwrap(); - assert_eq!(word.to_string(), "EGG"); - assert_eq!(word.calculate_score(), 2 + 2 + 2); - assert!(dictionary.is_word_valid(word)); - - let word = x.get(1).unwrap(); - assert_eq!(word.to_string(), "JOEL"); - assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); - assert!(!dictionary.is_word_valid(word)); - } - Err(e) => { panic!("Expected to find a word to play; found error {}", e) } - } - - let scores = board.calculate_scores(&dictionary); - match scores { - Ok(_) => {panic!("Expected an error")} - Err(e) => {assert_eq!(e, "JOEL is not a valid word")} - } - - let mut alt_dictionary = DictionaryImpl::new(); - alt_dictionary.insert("JOEL".to_string(), 0.5); - alt_dictionary.insert("EGG".to_string(), 0.5); - - - let scores = board.calculate_scores(&alt_dictionary); - match scores { - Ok((words, total_score)) => { - assert_eq!(words.len(), 2); - let (word, score) = words.get(0).unwrap(); - assert_eq!(word.to_string(), "EGG"); - assert_eq!(word.calculate_score(), 2 + 2 + 2); - assert_eq!(*score, 2 + 2 + 2); - - let (word, score) = words.get(1).unwrap(); - assert_eq!(word.to_string(), "JOEL"); - assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); - assert_eq!(*score, 8 + 1 + 2 + 1); - - assert_eq!(total_score, 18); - } - Err(e) => {panic!("Wasn't expecting to encounter error {e}")} - } - - - // replace one of the 'G' in EGG with an ephemeral to trigger an error - board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(make_letter('G', true, 2)); - - let words = board.find_played_words(); - match words { - Ok(_) => { panic!("Expected error as we played tiles in multiple rows and columns") } - Err(e) => { assert_eq!(e, "Tiles need to be played on one row or column") } - } - } - - // make a copy of the board now with x and y swapped - let mut inverted_board = Board::new(); - - for x in 0..GRID_LENGTH { - for y in 0..GRID_LENGTH { - let cell_original = board.get_cell(Coordinates(x, y)).unwrap(); - let cell_new = inverted_board.get_cell_mut(Coordinates(y, x)).unwrap(); - - match &cell_original.value { - None => {} - Some(x) => { - cell_new.value = Some(*x); - } - } - - } - } - - println!("Checking original board"); - check_board(&mut board, false); - - println!("Checking inverted board"); - check_board(&mut inverted_board, true); - - - } - - #[test] - fn test_dictionary() { - let dictionary = HashMap::create("resources/dictionary.csv"); - - assert_eq!(dictionary.len(), 279429); - - assert!(dictionary.contains_key("AA")); - assert!(dictionary.contains_key("AARDVARK")); - - assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001) - - } - - #[test] - fn test_dictionary_sets() { - let mut dictionary = HashMap::new(); - dictionary.insert("JOEL".to_string(), 0.7); - dictionary.insert("JOHN".to_string(), 0.5); - dictionary.insert("XYZ".to_string(), 0.1); - - let dictionary = dictionary.filter_to_sub_dictionary(0.3); - assert_eq!(dictionary.len(), 2); - assert!(dictionary.contains_key("JOEL")); - assert!(dictionary.contains_key("JOHN")); - - let set = dictionary.substring_set(); - - assert!(set.contains("J")); - assert!(set.contains("O")); - assert!(set.contains("E")); - assert!(set.contains("L")); - assert!(set.contains("H")); - assert!(set.contains("N")); - - assert!(set.contains("JO")); - assert!(set.contains("OE")); - assert!(set.contains("EL")); - assert!(set.contains("OH")); - assert!(set.contains("HN")); - - assert!(set.contains("JOE")); - assert!(set.contains("OEL")); - assert!(set.contains("JOH")); - assert!(set.contains("OHN")); - - - assert!(!set.contains("XY")); - assert!(!set.contains("JH")); - assert!(!set.contains("JE")); - - } -} diff --git a/src/main.rs b/src/main.rs index 00f780b..0fdba7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use word_grid::{Board, Coordinates, Letter}; +use word_grid::board::{Board, Coordinates, Letter}; fn main() { let mut board = Board::new();