use std::collections::HashSet; use std::fmt; use std::fmt::{Formatter, Write}; use std::borrow::BorrowMut; use serde::{Deserialize, Serialize}; use tsify::Tsify; use crate::constants::{ALL_LETTERS_BONUS, GRID_LENGTH, TRAY_LENGTH}; use crate::dictionary::DictionaryImpl; #[derive(Clone, Copy)] pub enum Direction { Row, Column } impl Direction { pub fn invert(&self) -> Self { match &self { Direction::Row => {Direction::Column} Direction::Column => {Direction::Row} } } } #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct Coordinates (pub u8, pub u8); impl Coordinates { pub fn new_from_index(index: usize) -> Self { let y = index / GRID_LENGTH as usize; let x = index % GRID_LENGTH as usize; Coordinates(x as u8, y as u8) } 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)) } } pub fn increment(&self, direction: Direction) -> Option{ self.add(direction, 1) } pub fn decrement(&self, direction: Direction) -> Option{ self.add(direction, -1) } pub fn map_to_index(&self) -> usize { (self.0 + GRID_LENGTH*self.1) as usize } } #[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash)] #[tsify(from_wasm_abi)] pub struct Letter { pub text: char, pub points: u32, pub ephemeral: bool, pub is_blank: bool, } impl Letter { pub fn new_fixed(text: char, points: u32) -> Self { Letter { text, points, ephemeral: false, is_blank: false, } } pub fn new(text: Option, points: u32) -> Letter { match text { None => { Letter { text: ' ', points, ephemeral: true, is_blank: true, } } Some(text) => { Letter { text, points, ephemeral: true, is_blank: false, } } } } pub fn partial_match(&self, other: &Letter) -> bool { self == other || (self.is_blank && other.is_blank && self.points == other.points) } } #[derive(Debug, Copy, Clone, Serialize)] pub enum CellType { Normal, DoubleWord, DoubleLetter, TripleLetter, TripleWord, Start, } #[derive(Debug, Clone)] pub struct Cell { pub value: Option, pub cell_type: CellType, pub coordinates: Coordinates, } #[derive(Debug, Clone)] pub struct Board { pub 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> { pub 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 starting_coords = Coordinates(starting_row, starting_column); let main_word = self.find_word_at_position(starting_coords, direction).unwrap(); 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") } } pub 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, }) } pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> { for (mut letter, coords) in play { { let cell = match self.get_cell_mut(coords) { Ok(cell) => {cell} Err(e) => {return Err(e.to_string())} }; if cell.value.is_some() { return Err(format!("There's already a letter at {:?}", coords)); } letter.ephemeral = true; cell.value = Some(letter); } } Ok(()) } pub fn fix_tiles(&mut self) { for cell in self.cells.iter_mut() { match cell.value.borrow_mut() { None => {} Some(x) => { x.ephemeral = false; } } } } } 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_from_path("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); } }