Joel Therrien
8fd250170b
Also fixed some bugs that could cause AI process to either produce invalid words or crash.
950 lines
No EOL
31 KiB
Rust
950 lines
No EOL
31 KiB
Rust
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<Self> {
|
|
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>{
|
|
self.add(direction, 1)
|
|
}
|
|
|
|
pub fn decrement(&self, direction: Direction) -> Option<Self>{
|
|
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<char>, 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<Letter>,
|
|
pub cell_type: CellType,
|
|
pub coordinates: Coordinates,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct Board {
|
|
pub cells: Vec<Cell>,
|
|
}
|
|
|
|
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<Word>, 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<Word> {
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
} |