Compare commits

..

No commits in common. "multiplayer" and "main" have entirely different histories.

26 changed files with 1832 additions and 2903 deletions

5
.gitignore vendored
View file

@ -1,5 +1,4 @@
target/ /target
/Cargo.lock /Cargo.lock
.idea/ .idea/
pkg/ pkg/
.nvmrc

View file

@ -7,9 +7,16 @@ repository = "https://git.joeltherrien.ca/joel/WordGrid"
license = "AGPL-3" license = "AGPL-3"
description = "A (WIP) package for playing 'WordGrid'." description = "A (WIP) package for playing 'WordGrid'."
[lib]
crate-type = ["cdylib"]
[dependencies] [dependencies]
csv = "1.3.0" csv = "1.2.2"
rand = {version = "0.8.5", features = ["small_rng"]} rand = {version = "0.8.5", features = ["small_rng"]}
getrandom = {version = "0.2", features = ["js"]} getrandom = {version = "0.2", features = ["js"]}
wasm-bindgen = { version = "0.2.87", features = ["serde-serialize"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0.202", features = ["derive"] } serde = { version = "=1.0.171", features = ["derive"] }
serde-wasm-bindgen = "0.4.5"
tsify = { version = "0.4.5", features = ["js"] }

View file

@ -1,31 +1,32 @@
use crate::constants::{ALL_LETTERS_BONUS, GRID_LENGTH, TRAY_LENGTH};
use crate::dictionary::DictionaryImpl;
use crate::game::Error;
use serde::{Deserialize, Serialize};
use std::borrow::BorrowMut;
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt; use std::fmt;
use std::fmt::{Formatter, Write}; 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)] #[derive(Clone, Copy)]
pub enum Direction { pub enum Direction {
Row, Row, Column
Column,
} }
impl Direction { impl Direction {
pub fn invert(&self) -> Self { pub fn invert(&self) -> Self {
match &self { match &self {
Direction::Row => Direction::Column, Direction::Row => {Direction::Column}
Direction::Column => Direction::Row, Direction::Column => {Direction::Row}
} }
} }
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct Coordinates(pub u8, pub u8); pub struct Coordinates (pub u8, pub u8);
impl Coordinates { impl Coordinates {
pub fn new_from_index(index: usize) -> Self { pub fn new_from_index(index: usize) -> Self {
let y = index / GRID_LENGTH as usize; let y = index / GRID_LENGTH as usize;
let x = index % GRID_LENGTH as usize; let x = index % GRID_LENGTH as usize;
@ -35,35 +36,32 @@ impl Coordinates {
fn add(&self, direction: Direction, i: i8) -> Option<Self> { fn add(&self, direction: Direction, i: i8) -> Option<Self> {
let proposed = match direction { let proposed = match direction {
Direction::Column => (self.0 as i8, self.1 as i8 + i), Direction::Column => {(self.0 as i8, self.1 as i8+i)}
Direction::Row => (self.0 as i8 + i, self.1 as i8), Direction::Row => {(self.0 as i8+i, self.1 as i8)}
}; };
if proposed.0 < 0 if proposed.0 < 0 || proposed.0 >= GRID_LENGTH as i8 || proposed.1 < 0 || proposed.1 >= GRID_LENGTH as i8 {
|| proposed.0 >= GRID_LENGTH as i8
|| proposed.1 < 0
|| proposed.1 >= GRID_LENGTH as i8
{
None None
} else { } else{
Some(Coordinates(proposed.0 as u8, proposed.1 as u8)) Some(Coordinates(proposed.0 as u8, proposed.1 as u8))
} }
} }
pub fn increment(&self, direction: Direction) -> Option<Self> { pub fn increment(&self, direction: Direction) -> Option<Self>{
self.add(direction, 1) self.add(direction, 1)
} }
pub fn decrement(&self, direction: Direction) -> Option<Self> { pub fn decrement(&self, direction: Direction) -> Option<Self>{
self.add(direction, -1) self.add(direction, -1)
} }
pub fn map_to_index(&self) -> usize { pub fn map_to_index(&self) -> usize {
(self.0 + GRID_LENGTH * self.1) as usize (self.0 + GRID_LENGTH*self.1) as usize
} }
} }
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash)]
#[tsify(from_wasm_abi)]
pub struct Letter { pub struct Letter {
pub text: char, pub text: char,
pub points: u32, pub points: u32,
@ -83,27 +81,32 @@ impl Letter {
pub fn new(text: Option<char>, points: u32) -> Letter { pub fn new(text: Option<char>, points: u32) -> Letter {
match text { match text {
None => Letter { None => {
text: ' ', Letter {
points, text: ' ',
ephemeral: true, points,
is_blank: true, ephemeral: true,
}, is_blank: true,
Some(text) => Letter { }
text, }
points, Some(text) => {
ephemeral: true, Letter {
is_blank: false, text,
}, points,
ephemeral: true,
is_blank: false,
}
}
} }
} }
pub fn partial_match(&self, other: &Letter) -> bool { pub fn partial_match(&self, other: &Letter) -> bool {
self == other || (self.is_blank && other.is_blank && self.points == other.points) self == other || (self.is_blank && other.is_blank && self.points == other.points)
} }
} }
#[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Serialize)]
pub enum CellType { pub enum CellType {
Normal, Normal,
DoubleWord, DoubleWord,
@ -138,34 +141,37 @@ impl<'a> ToString for Word<'a> {
} }
text text
} }
} }
impl<'a> Word<'a> { impl <'a> Word<'a> {
pub fn calculate_score(&self) -> u32 {
pub fn calculate_score(&self) -> u32{
let mut multiplier = 1; let mut multiplier = 1;
let mut unmultiplied_score = 0; let mut unmultiplied_score = 0;
for cell in self.cells.as_slice() { for cell in self.cells.as_slice() {
let cell_value = cell.value.unwrap(); let cell_value = cell.value.unwrap();
if cell_value.ephemeral { if cell_value.ephemeral {
let cell_multiplier = match cell.cell_type { let cell_multiplier =
CellType::Normal => 1, match cell.cell_type {
CellType::DoubleWord => { CellType::Normal => {1}
multiplier *= 2; CellType::DoubleWord => {
1 multiplier *= 2;
} 1
CellType::DoubleLetter => 2, }
CellType::TripleLetter => 3, CellType::DoubleLetter => {2}
CellType::TripleWord => { CellType::TripleLetter => {3}
multiplier *= 3; CellType::TripleWord => {
1 multiplier *= 3;
} 1
CellType::Start => { }
multiplier *= 2; CellType::Start => {
1 multiplier *= 2;
} 1
}; }
};
unmultiplied_score += cell_value.points * cell_multiplier; unmultiplied_score += cell_value.points * cell_multiplier;
} else { } else {
// no cell multiplier unfortunately // no cell multiplier unfortunately
@ -181,6 +187,7 @@ impl Board {
pub fn new() -> Self { pub fn new() -> Self {
let mut cells = Vec::new(); 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 /// Since the board is symmetrical in both directions for the purposes of our logic we can keep our coordinates in one corner
/// ///
/// # Arguments /// # Arguments
@ -193,7 +200,7 @@ impl Board {
GRID_LENGTH - x - 1 GRID_LENGTH - x - 1
} else { } else {
x x
}; }
} }
for i_orig in 0..GRID_LENGTH { for i_orig in 0..GRID_LENGTH {
@ -214,10 +221,12 @@ impl Board {
} }
// Double letters // Double letters
if (i % 4 == 2) && (j % 4 == 2) && !(i == 2 && j == 2) { if (i % 4 == 2) && (j % 4 == 2) && !(
i == 2 && j == 2
) {
typee = CellType::DoubleLetter; typee = CellType::DoubleLetter;
} }
if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j) == 3 && i.max(j) == 7) { if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j)==3 && i.max(j) == 7) {
typee = CellType::DoubleLetter; typee = CellType::DoubleLetter;
} }
@ -236,10 +245,11 @@ impl Board {
value: None, value: None,
coordinates: Coordinates(j_orig, i_orig), coordinates: Coordinates(j_orig, i_orig),
}) })
} }
} }
Board { cells } Board {cells}
} }
pub fn get_cell(&self, coordinates: Coordinates) -> Result<&Cell, &str> { pub fn get_cell(&self, coordinates: Coordinates) -> Result<&Cell, &str> {
@ -251,28 +261,24 @@ impl Board {
} }
} }
pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, Error> { pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, &str> {
if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH { if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH {
Err(Error::Other( Err("x & y must be within the board's coordinates")
"x & y must be within the board's coordinates".to_string(),
))
} else { } else {
let index = coordinates.map_to_index(); let index = coordinates.map_to_index();
Ok(self.cells.get_mut(index).unwrap()) Ok(self.cells.get_mut(index).unwrap())
} }
} }
pub fn calculate_scores(
&self, pub fn calculate_scores(&self, dictionary: &DictionaryImpl) -> Result<(Vec<(Word, u32)>, u32), String> {
dictionary: &DictionaryImpl,
) -> Result<(Vec<(Word, u32)>, u32), Error> {
let (words, tiles_played) = self.find_played_words()?; let (words, tiles_played) = self.find_played_words()?;
let mut words_and_scores = Vec::new(); let mut words_and_scores = Vec::new();
let mut total_score = 0; let mut total_score = 0;
for word in words { for word in words {
if !dictionary.contains_key(&word.to_string()) { if !dictionary.contains_key(&word.to_string()) {
return Err(Error::InvalidWord(word.to_string())); return Err(format!("{} is not a valid word", word.to_string()));
} }
let score = word.calculate_score(); let score = word.calculate_score();
@ -287,9 +293,10 @@ impl Board {
Ok((words_and_scores, total_score)) Ok((words_and_scores, total_score))
} }
pub fn find_played_words(&self) -> Result<(Vec<Word>, u8), Error> { 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 // 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's first establish what rows and columns tiles were played in
let mut rows_played = HashSet::with_capacity(15); let mut rows_played = HashSet::with_capacity(15);
let mut columns_played = HashSet::with_capacity(15); let mut columns_played = HashSet::with_capacity(15);
@ -313,9 +320,9 @@ impl Board {
} }
if rows_played.is_empty() { if rows_played.is_empty() {
return Err(Error::NoTilesPlayed); return Err("Tiles need to be played")
} else if rows_played.len() > 1 && columns_played.len() > 1 { } else if rows_played.len() > 1 && columns_played.len() > 1 {
return Err(Error::TilesNotStraight); return Err("Tiles need to be played on one row or column")
} }
let direction = if rows_played.len() > 1 { let direction = if rows_played.len() > 1 {
@ -328,9 +335,7 @@ impl Board {
let starting_column = *columns_played.iter().min().unwrap(); let starting_column = *columns_played.iter().min().unwrap();
let starting_coords = Coordinates(starting_row, starting_column); let starting_coords = Coordinates(starting_row, starting_column);
let main_word = self let main_word = self.find_word_at_position(starting_coords, direction).unwrap();
.find_word_at_position(starting_coords, direction)
.unwrap();
let mut words = Vec::new(); let mut words = Vec::new();
let mut observed_tiles_played = 0; let mut observed_tiles_played = 0;
@ -354,25 +359,25 @@ impl Board {
// there are tiles not part of the main word // there are tiles not part of the main word
if observed_tiles_played != tiles_played { if observed_tiles_played != tiles_played {
return Err(Error::TilesHaveGap); return Err("Played tiles cannot have empty gap");
} }
// don't want the case of a single letter word // don't want the case of a single letter word
if main_word.cells.len() > 1 { if main_word.cells.len() > 1 {
words.push(main_word); words.push(main_word);
} else if words.is_empty() { } else if words.is_empty() {
return Err(Error::OneLetterWord); return Err("All words must be at least one letter");
} }
// need to verify that the play is 'anchored' // need to verify that the play is 'anchored'
let mut anchored = false; let mut anchored = false;
'outer: for word in words.as_slice() { 'outer: for word in words.as_slice() {
for cell in word.cells.as_slice() { for cell in word.cells.as_slice() {
// either one of the letters // either one of the letters
if !cell.value.as_ref().unwrap().ephemeral if !cell.value.as_ref().unwrap().ephemeral || (cell.coordinates.0 == GRID_LENGTH / 2 && cell.coordinates.1 == GRID_LENGTH / 2){
|| (cell.coordinates.0 == GRID_LENGTH / 2
&& cell.coordinates.1 == GRID_LENGTH / 2)
{
anchored = true; anchored = true;
break 'outer; break 'outer;
} }
@ -382,27 +387,23 @@ impl Board {
if anchored { if anchored {
Ok((words, tiles_played)) Ok((words, tiles_played))
} else { } else {
Err(Error::UnanchoredWord) return Err("Played tiles must be anchored to something")
} }
} }
pub fn find_word_at_position( pub fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option<Word> {
&self,
mut start_coords: Coordinates,
direction: Direction,
) -> Option<Word> {
// let's see how far we can backtrack to the start of the word // let's see how far we can backtrack to the start of the word
let mut times_moved = 0; let mut times_moved = 0;
loop { loop {
let one_back = start_coords.add(direction, -times_moved); let one_back = start_coords.add(direction, -times_moved);
match one_back { match one_back {
None => break, None => { break }
Some(new_coords) => { Some(new_coords) => {
let cell = self.get_cell(new_coords).unwrap(); let cell = self.get_cell(new_coords).unwrap();
if cell.value.is_some() { if cell.value.is_some(){
times_moved += 1; times_moved += 1;
} else { } else {
break; break
} }
} }
} }
@ -422,11 +423,11 @@ impl Board {
loop { loop {
let position = start_coords.add(direction, cells.len() as i8); let position = start_coords.add(direction, cells.len() as i8);
match position { match position {
None => break, None => {break}
Some(x) => { Some(x) => {
let cell = self.get_cell(x).unwrap(); let cell = self.get_cell(x).unwrap();
match cell.value { match cell.value {
None => break, None => {break}
Some(_) => { Some(_) => {
cells.push(cell); cells.push(cell);
} }
@ -441,16 +442,16 @@ impl Board {
}) })
} }
pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), Error> { pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> {
for (mut letter, coords) in play { for (mut letter, coords) in play {
{ {
let cell = self.get_cell_mut(coords)?; let cell = match self.get_cell_mut(coords) {
Ok(cell) => {cell}
Err(e) => {return Err(e.to_string())}
};
if cell.value.is_some() { if cell.value.is_some() {
return Err(Error::Other(format!( return Err(format!("There's already a letter at {:?}", coords));
"There's already a letter at {:?}",
coords
)));
} }
letter.ephemeral = true; letter.ephemeral = true;
@ -475,6 +476,7 @@ impl Board {
impl fmt::Display for Board { impl fmt::Display for Board {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut str = String::new(); let mut str = String::new();
let normal = "\x1b[48;5;174m\x1b[38;5;0m"; let normal = "\x1b[48;5;174m\x1b[38;5;0m";
@ -491,82 +493,57 @@ impl fmt::Display for Board {
let cell = self.get_cell(coords).unwrap(); let cell = self.get_cell(coords).unwrap();
let color = match cell.cell_type { let color = match cell.cell_type {
CellType::Normal => normal, CellType::Normal => {normal}
CellType::DoubleWord => double_word, CellType::DoubleWord => {double_word}
CellType::DoubleLetter => double_letter, CellType::DoubleLetter => {double_letter}
CellType::TripleLetter => triple_letter, CellType::TripleLetter => {triple_letter}
CellType::TripleWord => triple_word, CellType::TripleWord => {triple_word}
CellType::Start => double_word, CellType::Start => {double_word}
}; };
let content = match &cell.value { let content = match &cell.value {
None => ' ', None => {' '}
Some(letter) => letter.text, Some(letter) => {letter.text}
}; };
str.write_str(color).unwrap(); str.write_str(color).unwrap();
str.write_char(content).unwrap(); str.write_char(content).unwrap();
} }
str.write_str("\x1b[0m\n").unwrap(); str.write_str("\x1b[0m\n").unwrap();
} }
write!(f, "{}", str) write!(f, "{}", str)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::dictionary::Dictionary; use crate::dictionary::Dictionary;
use super::*;
#[test] #[test]
fn test_cell_types() { fn test_cell_types() {
let board = Board::new(); let board = Board::new();
assert!(matches!( assert!(matches!(board.get_cell(Coordinates(0, 0)).unwrap().cell_type, CellType::TripleWord));
board.get_cell(Coordinates(0, 0)).unwrap().cell_type, assert!(matches!(board.get_cell(Coordinates(1, 0)).unwrap().cell_type, CellType::Normal));
CellType::TripleWord 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(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!( assert!(matches!(board.get_cell(Coordinates(13, 13)).unwrap().cell_type, CellType::DoubleWord));
board.get_cell(Coordinates(13, 13)).unwrap().cell_type, assert!(matches!(board.get_cell(Coordinates(14, 14)).unwrap().cell_type, CellType::TripleWord));
CellType::DoubleWord assert!(matches!(board.get_cell(Coordinates(11, 14)).unwrap().cell_type, CellType::DoubleLetter));
));
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!( assert!(matches!(board.get_cell(Coordinates(7, 7)).unwrap().cell_type, CellType::Start));
board.get_cell(Coordinates(7, 7)).unwrap().cell_type, assert!(matches!(board.get_cell(Coordinates(8, 6)).unwrap().cell_type, CellType::DoubleLetter));
CellType::Start assert!(matches!(board.get_cell(Coordinates(5, 9)).unwrap().cell_type, CellType::TripleLetter));
));
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] #[test]
fn test_cell_coordinates() { fn test_cell_coordinates() {
let board = Board::new(); let board = Board::new();
@ -599,6 +576,7 @@ mod tests {
board.get_cell_mut(Coordinates(5, 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(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(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)); board.get_cell_mut(Coordinates(10, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
@ -606,9 +584,7 @@ mod tests {
println!("x is {}", x); println!("x is {}", x);
let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column); let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column);
match first_word { match first_word {
None => { None => { panic!("Expected to find word JOEL") }
panic!("Expected to find word JOEL")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 6); assert_eq!(x.coords.1, 6);
@ -620,9 +596,7 @@ mod tests {
let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row); let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row);
match single_letter_word { match single_letter_word {
None => { None => { panic!("Expected to find letter L") }
panic!("Expected to find letter L")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 9); assert_eq!(x.coords.1, 9);
@ -635,9 +609,7 @@ mod tests {
println!("x is {}", x); println!("x is {}", x);
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
match word { match word {
None => { None => { panic!("Expected to find word IS") }
panic!("Expected to find word IS")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 0); assert_eq!(x.coords.0, 0);
assert_eq!(x.coords.1, 0); assert_eq!(x.coords.1, 0);
@ -651,9 +623,7 @@ mod tests {
println!("x is {}", x); println!("x is {}", x);
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
match word { match word {
None => { None => { panic!("Expected to find word COOL") }
panic!("Expected to find word COOL")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 3); assert_eq!(x.coords.0, 3);
assert_eq!(x.coords.1, 0); assert_eq!(x.coords.1, 0);
@ -668,9 +638,7 @@ mod tests {
let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row); let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row);
match word { match word {
None => { None => { panic!("Expected to find word EGG") }
panic!("Expected to find word EGG")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 8); assert_eq!(x.coords.1, 8);
@ -691,10 +659,10 @@ mod tests {
is_blank: false, is_blank: false,
}); });
assert!(matches!( match board.find_played_words() {
board.find_played_words(), Ok(_) => {panic!("Expected error")}
Err(Error::OneLetterWord) Err(e) => {assert_eq!(e, "All words must be at least one letter");}
)); }
board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter { board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter {
text: 'I', text: 'I',
@ -758,7 +726,10 @@ mod tests {
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true)); board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true));
let words = board.find_played_words(); let words = board.find_played_words();
assert!(matches!(words, Err(Error::UnanchoredWord))); match words {
Ok(_) => {panic!("Expected the not-anchored error")}
Err(x) => {assert_eq!(x, "Played tiles must be anchored to something")}
}
// Adding anchor // Adding anchor
board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false)); board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false));
@ -775,6 +746,7 @@ mod tests {
assert!(board.find_played_words().is_ok()); assert!(board.find_played_words().is_ok());
} }
#[test] #[test]
fn test_word_finding_with_break() { 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 // Verify that if I play my tiles on one row or column but with a break in-between I get an error
@ -793,15 +765,22 @@ mod tests {
board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 0)); 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, 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, 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, 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, 11)).unwrap().value = Some(make_letter('I', true));
board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0)); board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0));
let words = board.find_played_words(); let words = board.find_played_words();
assert!(matches!(words, Err(Error::TilesHaveGap))); match words {
Ok(_) => {panic!("Expected to find an error!")}
Err(x) => {
assert_eq!(x, "Played tiles cannot have empty gap")
}
}
} }
#[test] #[test]
fn test_word_finding_whole_board() { fn test_word_finding_whole_board() {
let mut board = Board::new(); let mut board = Board::new();
@ -816,12 +795,15 @@ mod tests {
} }
let words = board.find_played_words(); let words = board.find_played_words();
assert!(matches!(words, Err(Error::NoTilesPlayed))); 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, 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, 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, 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(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(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(1, 0)).unwrap().value = Some(Letter::new_fixed('S', 1));
@ -832,7 +814,7 @@ mod tests {
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1)); board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1));
fn check_board(board: &mut Board, inverted: bool) { fn check_board(board: &mut Board, inverted: bool) {
let dictionary = DictionaryImpl::create_from_path("../resources/dictionary.csv"); let dictionary = DictionaryImpl::create_from_path("resources/dictionary.csv");
println!("{}", board); println!("{}", board);
let words = board.find_played_words(); let words = board.find_played_words();
match words { match words {
@ -845,9 +827,7 @@ mod tests {
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
} }
Err(e) => { Err(e) => { panic!("Expected to find a word to play; found error {}", e) }
panic!("Expected to find a word to play; found error {}", e)
}
} }
let maybe_invert = |coords: Coordinates| { let maybe_invert = |coords: Coordinates| {
@ -864,21 +844,12 @@ mod tests {
return direction; return direction;
}; };
board board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(Letter::new_fixed('G', 2));
.get_cell_mut(maybe_invert(Coordinates(9, 8))) board.get_cell_mut(maybe_invert(Coordinates(10, 8))).unwrap().value = Some(Letter::new_fixed('G', 2));
.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 let word = board.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row));
.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row));
match word { match word {
None => { None => {panic!("Expected to find word EGG")}
panic!("Expected to find word EGG")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 8); assert_eq!(x.coords.1, 8);
@ -886,6 +857,7 @@ mod tests {
assert_eq!(x.to_string(), "EGG"); assert_eq!(x.to_string(), "EGG");
assert_eq!(x.calculate_score(), 2 + 2 + 2); assert_eq!(x.calculate_score(), 2 + 2 + 2);
assert!(dictionary.is_word_valid(&x)); assert!(dictionary.is_word_valid(&x));
} }
} }
@ -904,29 +876,20 @@ mod tests {
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
assert!(!dictionary.is_word_valid(word)); assert!(!dictionary.is_word_valid(word));
} }
Err(e) => { Err(e) => { panic!("Expected to find a word to play; found error {}", e) }
panic!("Expected to find a word to play; found error {}", e)
}
} }
let scores = board.calculate_scores(&dictionary); let scores = board.calculate_scores(&dictionary);
match scores { match scores {
Ok(_) => { Ok(_) => {panic!("Expected an error")}
panic!("Expected an error") Err(e) => {assert_eq!(e, "JOEL is not a valid word")}
}
Err(e) => {
if let Error::InvalidWord(w) = e {
assert_eq!(w, "JOEL");
} else {
panic!("Expected an InvalidPlay error")
}
}
} }
let mut alt_dictionary = DictionaryImpl::new(); let mut alt_dictionary = DictionaryImpl::new();
alt_dictionary.insert("JOEL".to_string(), 0.5); alt_dictionary.insert("JOEL".to_string(), 0.5);
alt_dictionary.insert("EGG".to_string(), 0.5); alt_dictionary.insert("EGG".to_string(), 0.5);
let scores = board.calculate_scores(&alt_dictionary); let scores = board.calculate_scores(&alt_dictionary);
match scores { match scores {
Ok((words, total_score)) => { Ok((words, total_score)) => {
@ -943,19 +906,18 @@ mod tests {
assert_eq!(total_score, 18); assert_eq!(total_score, 18);
} }
Err(e) => { Err(e) => {panic!("Wasn't expecting to encounter error {e}")}
panic!("Wasn't expecting to encounter error {e}")
}
} }
// replace one of the 'G' in EGG with an ephemeral to trigger an error // replace one of the 'G' in EGG with an ephemeral to trigger an error
board board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(make_letter('G', true, 2));
.get_cell_mut(maybe_invert(Coordinates(9, 8)))
.unwrap()
.value = Some(make_letter('G', true, 2));
let words = board.find_played_words(); let words = board.find_played_words();
assert!(matches!(words, Err(Error::TilesNotStraight))); 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 // make a copy of the board now with x and y swapped
@ -972,6 +934,7 @@ mod tests {
cell_new.value = Some(*x); cell_new.value = Some(*x);
} }
} }
} }
} }
@ -980,5 +943,8 @@ mod tests {
println!("Checking inverted board"); println!("Checking inverted board");
check_board(&mut inverted_board, true); check_board(&mut inverted_board, true);
} }
}
}

View file

@ -1,6 +1,6 @@
use crate::board::Letter;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rand::Rng; use rand::{Rng};
use crate::board::Letter;
pub const GRID_LENGTH: u8 = 15; pub const GRID_LENGTH: u8 = 15;
pub const TRAY_LENGTH: u8 = 7; pub const TRAY_LENGTH: u8 = 7;
@ -49,4 +49,5 @@ pub fn standard_tile_pool<R: Rng>(rng: Option<&mut R>) -> Vec<Letter> {
} }
letters letters
}
}

View file

@ -1,9 +1,10 @@
use crate::board::Word;
use csv::Reader;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use csv::Reader;
use crate::board::Word;
pub trait Dictionary { pub trait Dictionary {
fn create_from_reader<T: std::io::Read>(reader: Reader<T>) -> Self; fn create_from_reader<T: std::io::Read>(reader: Reader<T>) -> Self;
fn create_from_path(path: &str) -> Self; fn create_from_path(path: &str) -> Self;
fn create_from_str(data: &str) -> Self; fn create_from_str(data: &str) -> Self;
@ -13,7 +14,8 @@ pub trait Dictionary {
} }
pub type DictionaryImpl = HashMap<String, f64>; pub type DictionaryImpl = HashMap<String, f64>;
impl Dictionary for DictionaryImpl { impl Dictionary for DictionaryImpl{
fn create_from_reader<T: std::io::Read>(mut reader: Reader<T>) -> Self { fn create_from_reader<T: std::io::Read>(mut reader: Reader<T>) -> Self {
let mut map = HashMap::new(); let mut map = HashMap::new();
@ -25,6 +27,7 @@ impl Dictionary for DictionaryImpl {
let score = f64::from_str(score).unwrap(); let score = f64::from_str(score).unwrap();
map.insert(word, score); map.insert(word, score);
} }
map map
@ -54,6 +57,7 @@ impl Dictionary for DictionaryImpl {
} }
map map
} }
fn substring_set(&self) -> HashSet<&str> { fn substring_set(&self) -> HashSet<&str> {
@ -61,10 +65,11 @@ impl Dictionary for DictionaryImpl {
for (word, _score) in self.iter() { for (word, _score) in self.iter() {
for j in 0..word.len() { for j in 0..word.len() {
for k in (j + 1)..(word.len() + 1) { for k in (j+1)..(word.len()+1) {
set.insert(&word[j..k]); set.insert(&word[j..k]);
} }
} }
} }
set set
@ -80,9 +85,10 @@ impl Dictionary for DictionaryImpl {
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_dictionary() { fn test_dictionary() {
let dictionary = HashMap::create_from_path("../resources/dictionary.csv"); let dictionary = HashMap::create_from_path("resources/dictionary.csv");
assert_eq!(dictionary.len(), 279429); assert_eq!(dictionary.len(), 279429);
@ -90,6 +96,7 @@ mod tests {
assert!(dictionary.contains_key("AARDVARK")); assert!(dictionary.contains_key("AARDVARK"));
assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001) assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001)
} }
#[test] #[test]
@ -124,8 +131,11 @@ mod tests {
assert!(set.contains("JOH")); assert!(set.contains("JOH"));
assert!(set.contains("OHN")); assert!(set.contains("OHN"));
assert!(!set.contains("XY")); assert!(!set.contains("XY"));
assert!(!set.contains("JH")); assert!(!set.contains("JH"));
assert!(!set.contains("JE")); assert!(!set.contains("JE"));
} }
}
}

View file

@ -1,90 +1,31 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use crate::board::{Board, Coordinates, Letter};
use crate::constants::{standard_tile_pool, TRAY_LENGTH};
use crate::dictionary::{Dictionary, DictionaryImpl};
use crate::player_interaction::ai::{Difficulty, AI};
use crate::player_interaction::Tray;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rand::rngs::SmallRng; use rand::rngs::SmallRng;
use rand::SeedableRng; use rand::SeedableRng;
use serde::{Deserialize, Serialize, Serializer}; use serde::{Deserialize, Serialize};
use tsify::Tsify;
use crate::board::{Board, Coordinates, Letter};
use crate::constants::{standard_tile_pool, TRAY_LENGTH};
use crate::dictionary::{Dictionary, DictionaryImpl};
use crate::player_interaction::ai::{AI, CompleteMove, Difficulty};
use crate::player_interaction::Tray;
pub enum Player { pub enum Player {
Human(String), Human(String),
AI { AI{
name: String, name: String,
difficulty: Difficulty, difficulty: Difficulty,
object: AI, object: AI,
},
}
#[derive(Debug)]
pub enum Error {
InvalidPlayer(String),
WrongTurn(String),
Other(String),
InvalidWord(String),
GameFinished,
NoTilesPlayed,
TilesNotStraight,
TilesHaveGap,
OneLetterWord,
UnanchoredWord,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
Error::InvalidPlayer(player) => {
write!(f, "{player} doesn't exist")
}
Error::WrongTurn(player) => {
write!(f, "It's not {player}'s turn")
}
Error::Other(msg) => {
write!(f, "Other error: {msg}")
}
Error::InvalidWord(word) => {
write!(f, "{word} is not a valid word")
}
Error::GameFinished => {
write!(f, "Moves cannot be made after a game has finished")
}
Error::NoTilesPlayed => {
write!(f, "Tiles need to be played")
}
Error::TilesNotStraight => {
write!(f, "Tiles need to be played on one row or column")
}
Error::TilesHaveGap => {
write!(f, "Played tiles cannot have empty gap")
}
Error::OneLetterWord => {
write!(f, "All words must be at least two letters")
}
Error::UnanchoredWord => {
write!(f, "Played tiles must be anchored to something")
}
}
}
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
} }
} }
impl Player { impl Player {
pub fn get_name(&self) -> &str { pub fn get_name(&self) -> &str {
match &self { match &self {
Player::Human(name) => name, Player::Human(name) => {name}
Player::AI { name, .. } => name, Player::AI { name, .. } => {name}
} }
} }
} }
@ -92,20 +33,18 @@ impl Player {
pub struct PlayerState { pub struct PlayerState {
pub player: Player, pub player: Player,
pub score: u32, pub score: u32,
pub tray: Tray, pub tray: Tray
} }
#[derive(Deserialize, Copy, Clone, Debug)] #[derive(Deserialize, Tsify, Copy, Clone, Debug)]
#[tsify(from_wasm_abi)]
pub struct PlayedTile { pub struct PlayedTile {
pub index: usize, pub index: usize,
pub character: Option<char>, // we only set this if PlayedTile is a blank pub character: Option<char>, // we only set this if PlayedTile is a blank
} }
impl PlayedTile { impl PlayedTile {
pub fn convert_tray( pub fn convert_tray(tray_tile_locations: &Vec<Option<PlayedTile>>, tray: &Tray) -> Vec<(Letter, Coordinates)> {
tray_tile_locations: &Vec<Option<PlayedTile>>,
tray: &Tray,
) -> Vec<(Letter, Coordinates)> {
let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new(); let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new();
for (i, played_tile) in tray_tile_locations.iter().enumerate() { for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() { if played_tile.is_some() {
@ -116,9 +55,7 @@ impl PlayedTile {
if letter.is_blank { if letter.is_blank {
match played_tile.character { match played_tile.character {
None => { None => {
panic!( panic!("You can't play a blank character without providing a letter value")
"You can't play a blank character without providing a letter value"
)
} }
Some(x) => { Some(x) => {
// TODO - check that x is a valid alphabet letter // TODO - check that x is a valid alphabet letter
@ -127,6 +64,7 @@ impl PlayedTile {
} }
} }
played_letters.push((letter, coord)); played_letters.push((letter, coord));
} }
} }
@ -134,13 +72,15 @@ impl PlayedTile {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct WordResult { pub struct WordResult {
word: String, word: String,
score: u32, score: u32,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct ScoreResult { pub struct ScoreResult {
words: Vec<WordResult>, words: Vec<WordResult>,
total: u32, total: u32,
@ -148,6 +88,7 @@ pub struct ScoreResult {
pub struct PlayerStates(pub Vec<PlayerState>); pub struct PlayerStates(pub Vec<PlayerState>);
impl PlayerStates { impl PlayerStates {
fn get_player_name_by_turn_id(&self, id: usize) -> &str { fn get_player_name_by_turn_id(&self, id: usize) -> &str {
let id_mod = id % self.0.len(); let id_mod = id % self.0.len();
let state = self.0.get(id_mod).unwrap(); let state = self.0.get(id_mod).unwrap();
@ -156,33 +97,37 @@ impl PlayerStates {
} }
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> { pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
self.0 self.0.iter()
.iter()
.filter(|state| state.player.get_name().eq(name)) .filter(|state| state.player.get_name().eq(name))
.nth(0) .nth(0)
} }
pub fn get_player_state_mut(&mut self, name: &str) -> Option<&mut PlayerState> { pub fn get_player_state_mut(&mut self, name: &str) -> Option<&mut PlayerState> {
self.0 self.0.iter_mut()
.iter_mut()
.filter(|state| state.player.get_name().eq(name)) .filter(|state| state.player.get_name().eq(name))
.nth(0) .nth(0)
} }
pub fn get_tray(&self, name: &str) -> Option<&Tray> { pub fn get_tray(&self, name: &str) -> Option<&Tray> {
let player = self.get_player_state(name)?; let player = self.get_player_state(name)?;
Some(&player.tray) Some(&player.tray)
} }
pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> { pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> {
let player = self.get_player_state_mut(name)?; let player = self.get_player_state_mut(name)?;
Some(&mut player.tray) Some(&mut player.tray)
} }
} }
#[derive(Deserialize, Serialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Tsify, Debug, Clone)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum GameState { pub enum GameState {
InProgress, InProgress,
@ -192,7 +137,7 @@ pub enum GameState {
}, },
} }
pub struct Game { pub struct Game{
pub tile_pool: Vec<Letter>, pub tile_pool: Vec<Letter>,
rng: SmallRng, rng: SmallRng,
board: Board, board: Board,
@ -203,20 +148,15 @@ pub struct Game {
state: GameState, state: GameState,
} }
impl Game { impl Game {
pub fn new( pub fn new(seed: u64, dictionary_text: &str, player_names: Vec<String>, ai_difficulties: Vec<Difficulty>) -> Self {
seed: u64,
dictionary_text: &str,
player_names: Vec<String>,
ai_difficulties: Vec<Difficulty>,
) -> Self {
let mut rng = SmallRng::seed_from_u64(seed); let mut rng = SmallRng::seed_from_u64(seed);
let mut letters = standard_tile_pool(Some(&mut rng)); let mut letters = standard_tile_pool(Some(&mut rng));
let dictionary = DictionaryImpl::create_from_str(dictionary_text); let dictionary = DictionaryImpl::create_from_str(dictionary_text);
let mut player_states: Vec<PlayerState> = player_names let mut player_states: Vec<PlayerState> = player_names.iter()
.iter()
.map(|name| { .map(|name| {
let mut tray = Tray::new(TRAY_LENGTH); let mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters); tray.fill(&mut letters);
@ -234,7 +174,7 @@ impl Game {
let mut tray = Tray::new(TRAY_LENGTH); let mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters); tray.fill(&mut letters);
let ai_player_name = if ai_length > 1 { let ai_player_name = if ai_length > 1 {
format!("AI {}", i + 1) format!("AI {}", i+1)
} else { } else {
"AI".to_string() "AI".to_string()
}; };
@ -264,15 +204,15 @@ impl Game {
} }
} }
pub fn get_board(&self) -> &Board {
&self.board
} pub fn get_board(&self) -> &Board {&self.board}
pub fn set_board(&mut self, new_board: Board) { pub fn set_board(&mut self, new_board: Board) {
self.board = new_board; self.board = new_board;
} }
fn fill_trays(&mut self) { fn fill_trays(&mut self){
for state in self.player_states.0.iter_mut() { for state in self.player_states.0.iter_mut() {
let tray = &mut state.tray; let tray = &mut state.tray;
tray.fill(&mut self.tile_pool); tray.fill(&mut self.tile_pool);
@ -283,18 +223,14 @@ impl Game {
&self.dictionary &self.dictionary
} }
fn verify_game_in_progress(&self) -> Result<(), Error> { fn verify_game_in_progress(&self) -> Result<(), String> {
if !matches!(self.state, GameState::InProgress) { if !matches!(self.state, GameState::InProgress) {
return Err(Error::GameFinished); return Err("Moves cannot be made after a game has finished".to_string());
} }
Ok(()) Ok(())
} }
pub fn receive_play( pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<(TurnAction, GameState), String> {
&mut self,
tray_tile_locations: Vec<Option<PlayedTile>>,
commit_move: bool,
) -> Result<(TurnAction, GameState), Error> {
self.verify_game_in_progress()?; self.verify_game_in_progress()?;
let player = self.current_player_name(); let player = self.current_player_name();
@ -302,8 +238,7 @@ impl Game {
let mut board_instance = self.get_board().clone(); let mut board_instance = self.get_board().clone();
let mut tray = self.player_states.get_tray(&player).unwrap().clone(); let mut tray = self.player_states.get_tray(&player).unwrap().clone();
let played_letters: Vec<(Letter, Coordinates)> = let played_letters: Vec<(Letter, Coordinates)> = PlayedTile::convert_tray(&tray_tile_locations, &tray);
PlayedTile::convert_tray(&tray_tile_locations, &tray);
for (i, played_tile) in tray_tile_locations.iter().enumerate() { for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() { if played_tile.is_some() {
*tray.letters.get_mut(i).unwrap() = None; *tray.letters.get_mut(i).unwrap() = None;
@ -312,16 +247,19 @@ impl Game {
board_instance.receive_play(played_letters)?; board_instance.receive_play(played_letters)?;
let x = board_instance.calculate_scores(self.get_dictionary())?; let x = board_instance.calculate_scores(self.get_dictionary())?;
let total_score = x.1; let total_score = x.1;
let words: Vec<WordResult> = let words: Vec<WordResult> = x.0.iter()
x.0.iter() .map(|(word, score)| {
.map(|(word, score)| WordResult { WordResult {
word: word.to_string(), word: word.to_string(),
score: *score, score: *score
}) }
.collect();
})
.collect();
if commit_move { if commit_move {
let player_state = self.player_states.get_player_state_mut(&player).unwrap(); let player_state = self.player_states.get_player_state_mut(&player).unwrap();
@ -338,42 +276,28 @@ impl Game {
// game is over // game is over
self.end_game(Some(player)); self.end_game(Some(player));
} }
} }
let locations = tray_tile_locations Ok((TurnAction::PlayTiles {
.iter() result: ScoreResult {
.filter_map(|x| x.as_ref()) words,
.map(|x| x.index) total: total_score,
.collect::<Vec<usize>>();
Ok((
TurnAction::PlayTiles {
result: ScoreResult {
words,
total: total_score,
},
locations,
}, },
self.get_state(), }, self.state.clone()))
))
} }
pub fn exchange_tiles( pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<(Tray, TurnAction, GameState), String> {
&mut self,
tray_tile_locations: Vec<bool>,
) -> Result<(Tray, TurnAction, GameState), Error> {
self.verify_game_in_progress()?; self.verify_game_in_progress()?;
let player = self.current_player_name(); let player = self.current_player_name();
let tray = match self.player_states.get_tray_mut(&player) { let tray = match self.player_states.get_tray_mut(&player) {
None => return Err(Error::InvalidPlayer(player)), None => {return Err(format!("Player {} not found", player))}
Some(x) => x, Some(x) => {x}
}; };
if tray.letters.len() != tray_tile_locations.len() { if tray.letters.len() != tray_tile_locations.len() {
return Err(Error::Other( return Err("Incoming tray and existing tray have different lengths".to_string());
"Incoming tray and existing tray have different lengths".to_string(),
));
} }
let tile_pool = &mut self.tile_pool; let tile_pool = &mut self.tile_pool;
@ -397,11 +321,8 @@ impl Game {
let state = self.increment_turn(false); let state = self.increment_turn(false);
Ok(( Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }, state.clone()))
tray,
TurnAction::ExchangeTiles { tiles_exchanged },
state.clone(),
))
} }
pub fn add_word(&mut self, word: String) { pub fn add_word(&mut self, word: String) {
@ -410,20 +331,21 @@ impl Game {
self.dictionary.insert(word, -1.0); self.dictionary.insert(word, -1.0);
} }
pub fn pass(&mut self) -> Result<GameState, Error> { pub fn pass(&mut self) -> Result<GameState, String> {
self.verify_game_in_progress()?; self.verify_game_in_progress()?;
Ok(self.increment_turn(false).clone()) Ok(self.increment_turn(false).clone())
} }
fn increment_turn(&mut self, played: bool) -> &GameState { fn increment_turn(&mut self, played: bool) -> &GameState{
self.turn_order += 1; self.turn_order += 1;
if !played { if !played {
self.turns_not_played += 1; self.turns_not_played += 1;
// check if game has ended due to passing // check if game has ended due to passing
if self.turns_not_played >= 2 * self.player_states.0.len() { if self.turns_not_played >= 2*self.player_states.0.len() {
self.end_game(None); self.end_game(None);
} }
} else { } else {
self.turns_not_played = 0; self.turns_not_played = 0;
} }
@ -432,6 +354,7 @@ impl Game {
} }
fn end_game(&mut self, finisher: Option<String>) { fn end_game(&mut self, finisher: Option<String>) {
let mut finished_letters_map = HashMap::new(); let mut finished_letters_map = HashMap::new();
let mut points_forfeit = 0; let mut points_forfeit = 0;
@ -453,30 +376,25 @@ impl Game {
} }
if let Some(finisher) = &finisher { if let Some(finisher) = &finisher {
let state = self.player_states.get_player_state_mut(finisher).unwrap(); let mut state = self.player_states.get_player_state_mut(finisher).unwrap();
state.score += points_forfeit; state.score += points_forfeit;
} }
self.state = GameState::Ended { self.state = GameState::Ended {
finisher, finisher,
remaining_tiles: finished_letters_map, remaining_tiles: finished_letters_map
}; };
} }
pub fn current_player_name(&self) -> String { pub fn current_player_name(&self) -> String {
self.player_states self.player_states.get_player_name_by_turn_id(self.turn_order).to_string()
.get_player_name_by_turn_id(self.turn_order)
.to_string()
} }
pub fn advance_turn(&mut self) -> Result<(TurnAdvanceResult, GameState), Error> { pub fn advance_turn(&mut self) -> Result<(TurnAdvanceResult, GameState), String> {
let current_player = self.current_player_name(); let current_player = self.current_player_name();
let state = self let state = self.player_states.get_player_state_mut(&current_player).ok_or("There should be a player available")?;
.player_states
.get_player_state_mut(&current_player)
.ok_or(Error::InvalidPlayer(current_player.clone()))?;
if let Player::AI { object, .. } = &mut state.player { if let Player::AI {object, .. } = &mut state.player {
let tray = &mut state.tray; let tray = &mut state.tray;
let best_move = object.find_best_move(tray, &self.board, &mut self.rng); let best_move = object.find_best_move(tray, &self.board, &mut self.rng);
@ -489,53 +407,43 @@ impl Game {
match tile_spot { match tile_spot {
None => { None => {
to_exchange.push(false); to_exchange.push(false);
} },
Some(_) => { Some(_) => {
to_exchange.push(true); to_exchange.push(true);
} }
} }
} }
if self.tile_pool.is_empty() { if self.tile_pool.is_empty(){
let game_state = self.increment_turn(false); let game_state = self.increment_turn(false);
Ok(( Ok((TurnAdvanceResult::AIMove {
TurnAdvanceResult::AIMove { name: current_player,
name: current_player, action: TurnAction::Pass,
action: TurnAction::Pass, }, game_state.clone()))
},
game_state.clone(),
))
} else { } else {
let (_, action, game_state) = self.exchange_tiles(to_exchange)?; let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
Ok(( Ok((TurnAdvanceResult::AIMove {
TurnAdvanceResult::AIMove { name: current_player,
name: current_player, action,
action, }, game_state))
},
game_state,
))
} }
} }
Some(best_move) => { Some(best_move) => {
let play = best_move.convert_to_play(tray); let play = best_move.convert_to_play(tray);
let (action, game_state) = self.receive_play(play, true)?; let (action, game_state) = self.receive_play(play, true)?;
Ok(( Ok((TurnAdvanceResult::AIMove {
TurnAdvanceResult::AIMove { name: current_player,
name: current_player, action,
action, }, game_state))
},
game_state,
))
} }
} }
} else { } else {
Ok(( Ok((TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()}, self.state.clone()))
TurnAdvanceResult::HumanInputRequired {
name: self.current_player_name(),
},
self.get_state(),
))
} }
} }
pub fn get_remaining_tiles(&self) -> usize { pub fn get_remaining_tiles(&self) -> usize {
@ -544,61 +452,60 @@ impl Game {
pub fn get_player_tile_count(&self, player: &str) -> Result<usize, String> { pub fn get_player_tile_count(&self, player: &str) -> Result<usize, String> {
let tray = match self.player_states.get_tray(&player) { let tray = match self.player_states.get_tray(&player) {
None => return Err(format!("Player {} not found", player)), None => {return Err(format!("Player {} not found", player))}
Some(x) => x, Some(x) => {x}
}; };
Ok(tray.count()) Ok(
tray.count()
)
} }
pub fn get_state(&self) -> GameState {
self.state.clone()
}
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Tsify, Debug)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum TurnAction { pub enum TurnAction {
Pass, Pass,
ExchangeTiles { ExchangeTiles{
tiles_exchanged: usize, tiles_exchanged: usize
}, },
PlayTiles { PlayTiles{
result: ScoreResult, result: ScoreResult
locations: Vec<usize>,
}, },
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Tsify, Debug)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum TurnAdvanceResult { pub enum TurnAdvanceResult {
HumanInputRequired { name: String }, HumanInputRequired{
AIMove { name: String, action: TurnAction }, name: String
},
AIMove{
name: String,
action: TurnAction,
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs;
use crate::game::Game; use crate::game::Game;
use crate::player_interaction::ai::Difficulty; use crate::player_interaction::ai::Difficulty;
use std::fs;
#[test] #[test]
fn test_game() { fn test_game() {
let seed = 124; let seed = 124;
let dictionary_path = "../resources/dictionary.csv"; let dictionary_path = "resources/dictionary.csv";
let dictionary_string = fs::read_to_string(dictionary_path).unwrap(); let dictionary_string = fs::read_to_string(dictionary_path).unwrap();
let mut game = Game::new( let mut game = Game::new(seed, &dictionary_string, vec!["Player".to_string()], vec![Difficulty{proportion: 0.5, randomness: 0.0}]);
seed,
&dictionary_string,
vec!["Player".to_string()],
vec![Difficulty {
proportion: 0.5,
randomness: 0.0,
}],
);
let current_player = game.current_player_name(); let current_player = game.current_player_name();
println!("Current player is {current_player}"); println!("Current player is {current_player}");
@ -615,5 +522,7 @@ mod tests {
assert_eq!(game.current_player_name(), "Player"); assert_eq!(game.current_player_name(), "Player");
assert_eq!(0, game.turns_not_played); assert_eq!(0, game.turns_not_played);
} }
}
}

21
src/lib.rs Normal file
View file

@ -0,0 +1,21 @@
use wasm_bindgen::prelude::wasm_bindgen;
pub mod constants;
pub mod board;
pub mod dictionary;
pub mod player_interaction;
pub mod game;
pub mod wasm;
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}

View file

@ -1,11 +1,14 @@
use crate::board::Letter;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tsify::Tsify;
use crate::board::Letter;
pub mod ai; pub mod ai;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Tsify, Clone)]
#[tsify(from_wasm_abi)]
pub struct Tray { pub struct Tray {
pub letters: Vec<Option<Letter>>, pub letters: Vec<Option<Letter>>
} }
impl Tray { impl Tray {
@ -14,7 +17,9 @@ impl Tray {
for _ in 0..tray_length { for _ in 0..tray_length {
letters.push(None); letters.push(None);
} }
Tray { letters } Tray {
letters
}
} }
pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) { pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) {
@ -32,20 +37,25 @@ impl Tray {
} }
pub fn count(&self) -> usize { pub fn count(&self) -> usize {
self.letters.iter().filter(|l| l.is_some()).count() self.letters.iter()
.filter(|l| l.is_some())
.count()
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_tray() { fn test_tray() {
let mut letters = vec![ let mut letters = vec![
Letter::new(Some('E'), 3), Letter::new(Some('E'), 3),
Letter::new(Some('O'), 2), Letter::new(Some('O'), 2),
Letter::new(Some('J'), 1), Letter::new(Some('J'), 1)
]; ];
let mut tray = Tray::new(5); let mut tray = Tray::new(5);
@ -79,5 +89,8 @@ mod tests {
assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E'); assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E');
assert!(tray.letters.get(3).unwrap().is_none()); assert!(tray.letters.get(3).unwrap().is_none());
assert!(tray.letters.get(4).unwrap().is_none()); assert!(tray.letters.get(4).unwrap().is_none());
} }
}
}

View file

@ -1,16 +1,14 @@
use std::collections::{HashMap, HashSet};
use rand::Rng;
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use crate::board::{Board, CellType, Coordinates, Direction, Letter}; use crate::board::{Board, CellType, Coordinates, Direction, Letter};
use crate::constants::GRID_LENGTH; use crate::constants::GRID_LENGTH;
use crate::dictionary::DictionaryImpl; use crate::dictionary::DictionaryImpl;
use crate::game::PlayedTile; use crate::game::PlayedTile;
use crate::player_interaction::Tray; use crate::player_interaction::Tray;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
const ALPHABET: [char; 26] = [ const ALPHABET: [char; 26] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
struct CoordinateLineMapper { struct CoordinateLineMapper {
direction: Direction, direction: Direction,
@ -20,13 +18,18 @@ struct CoordinateLineMapper {
impl CoordinateLineMapper { impl CoordinateLineMapper {
fn line_to_coord(&self, variable: u8) -> Coordinates { fn line_to_coord(&self, variable: u8) -> Coordinates {
match self.direction { match self.direction {
Direction::Row => Coordinates(variable, self.fixed), Direction::Row => {
Direction::Column => Coordinates(self.fixed, variable), Coordinates(variable, self.fixed)
}
Direction::Column => {
Coordinates(self.fixed, variable)
}
} }
} }
} }
#[derive(Copy, Clone, Serialize, Deserialize)] #[derive(Copy, Clone, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct Difficulty { pub struct Difficulty {
pub proportion: f64, pub proportion: f64,
pub randomness: f64, pub randomness: f64,
@ -45,6 +48,7 @@ pub struct AI {
type MoveMap = HashMap<char, u32>; type MoveMap = HashMap<char, u32>;
type CrossTiles = Vec<Option<MoveMap>>; type CrossTiles = Vec<Option<MoveMap>>;
#[derive(Debug, Eq, PartialEq, Hash)] #[derive(Debug, Eq, PartialEq, Hash)]
pub struct CompleteMove { pub struct CompleteMove {
moves: Vec<MoveComponent>, moves: Vec<MoveComponent>,
@ -52,11 +56,10 @@ pub struct CompleteMove {
} }
impl CompleteMove { impl CompleteMove {
pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> { pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> {
let mut played_tiles = Vec::with_capacity(tray.letters.len()); let mut played_tiles = Vec::with_capacity(tray.letters.len());
let mut moves = self let mut moves = self.moves.iter()
.moves
.iter()
.map(|m| Some(m.clone())) .map(|m| Some(m.clone()))
.collect::<Vec<Option<MoveComponent>>>(); .collect::<Vec<Option<MoveComponent>>>();
@ -85,7 +88,7 @@ impl CompleteMove {
found_match = true; found_match = true;
break; break;
} }
} },
None => {} None => {}
} }
} }
@ -124,28 +127,29 @@ struct MoveComponent {
} }
impl AI { impl AI {
pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self {
pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self{
let mut ai_dictionary = HashSet::new(); let mut ai_dictionary = HashSet::new();
let mut substrings = HashSet::new(); let mut substrings = HashSet::new();
for (word, score) in dictionary.iter() { for (word, score) in dictionary.iter() {
if *score >= difficulty.proportion { if *score >= difficulty.proportion { // TODO - may need to reverse
// TODO - may need to reverse
ai_dictionary.insert(word.clone()); ai_dictionary.insert(word.clone());
substrings.insert(word.clone()); substrings.insert(word.clone());
for i in 0..word.len() { for i in 0..word.len() {
for j in i + 1..word.len() { for j in i+1..word.len() {
substrings.insert(word[i..j].to_string()); substrings.insert(word[i..j].to_string());
} }
} }
} }
} }
let board_view = Board::new(); let board_view = Board::new();
let mut column_cross_tiles = let mut column_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
let mut row_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize); let mut row_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
board_view.cells.iter().for_each(|_| { board_view.cells.iter().for_each(|_| {
@ -164,20 +168,15 @@ impl AI {
} }
} }
pub fn find_best_move<R: Rng>( pub fn find_best_move<R: Rng>(&mut self, tray: &Tray, grid: &Board, rng: &mut R) -> Option<CompleteMove>{
&mut self,
tray: &Tray,
grid: &Board,
rng: &mut R,
) -> Option<CompleteMove> {
let move_set = self.find_all_moves(tray, grid); let move_set = self.find_all_moves(tray, grid);
let mut best_move: Option<(CompleteMove, f64)> = None; let mut best_move: Option<(CompleteMove, f64)> = None;
for possible_move in move_set { for possible_move in move_set {
let move_score = if self.difficulty.randomness > 0.0 { let move_score =
(1.0 - self.difficulty.randomness) * (possible_move.score as f64) if self.difficulty.randomness > 0.0 {
+ self.difficulty.randomness * rng.gen_range(0.0..1.0) (1.0 - self.difficulty.randomness) * (possible_move.score as f64) + self.difficulty.randomness * rng.gen_range(0.0..1.0)
} else { } else {
possible_move.score as f64 possible_move.score as f64
}; };
@ -195,9 +194,10 @@ impl AI {
} }
return match best_move { return match best_move {
None => None, None => {None}
Some((best_move, _)) => Some(best_move), Some((best_move, _)) => {Some(best_move)}
}; };
} }
fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> { fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> {
@ -215,17 +215,15 @@ impl AI {
&self, &self,
tray: &Tray, tray: &Tray,
direction: Direction, direction: Direction,
all_moves: &mut HashSet<CompleteMove>, all_moves: &mut HashSet<CompleteMove>) {
) {
// If you're building a word in one direction, you need to form valid words in the cross-direction // If you're building a word in one direction, you need to form valid words in the cross-direction
let cross_tiles = match direction { let cross_tiles = match direction {
Direction::Column => &self.row_cross_tiles, Direction::Column => {&self.row_cross_tiles}
Direction::Row => &self.column_cross_tiles, Direction::Row => {&self.column_cross_tiles}
}; };
let tray_letters = tray let tray_letters = tray.letters.iter()
.letters
.iter()
.filter(|letter| letter.is_some()) .filter(|letter| letter.is_some())
.map(|letter| letter.unwrap()) .map(|letter| letter.unwrap())
.collect::<Vec<Letter>>(); .collect::<Vec<Letter>>();
@ -236,7 +234,7 @@ impl AI {
let coord_mapper = CoordinateLineMapper { let coord_mapper = CoordinateLineMapper {
direction, direction,
fixed: k, fixed: k
}; };
for p in 0..GRID_LENGTH { for p in 0..GRID_LENGTH {
@ -248,12 +246,13 @@ impl AI {
for l in 0..GRID_LENGTH { for l in 0..GRID_LENGTH {
let coords = coord_mapper.line_to_coord(l); let coords = coord_mapper.line_to_coord(l);
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row) let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row) ||
|| check_if_anchored(&self.board_view, coords, Direction::Column); check_if_anchored(&self.board_view, coords, Direction::Column);
if is_anchored && line_letters.get(l as usize).unwrap().is_none() if is_anchored &&
// it's duplicate work to check here when we'll already check at either free side line_letters.get(l as usize).unwrap().is_none() // it's duplicate work to check here when we'll already check at either free side
{ {
self.evaluate_spot_heading_left( self.evaluate_spot_heading_left(
&line_letters, &line_letters,
&line_cross_letters, &line_cross_letters,
@ -263,11 +262,17 @@ impl AI {
&Vec::new(), &Vec::new(),
1, 1,
&MoveScoring::new(), &MoveScoring::new(),
all_moves,
); all_moves
);
} }
} }
} }
} }
fn evaluate_spot_heading_left( fn evaluate_spot_heading_left(
@ -282,8 +287,10 @@ impl AI {
min_length: usize, min_length: usize,
current_points: &MoveScoring, current_points: &MoveScoring,
all_moves: &mut HashSet<CompleteMove>, all_moves: &mut HashSet<CompleteMove>
) { ) {
if line_index < 0 || line_index >= GRID_LENGTH as i8 { if line_index < 0 || line_index >= GRID_LENGTH as i8 {
return; return;
} }
@ -293,13 +300,10 @@ impl AI {
Some(_) => { Some(_) => {
// there's a letter here; need to take a step left if we can // there's a letter here; need to take a step left if we can
if !(line_index >= 1 if !(line_index >= 1 &&
&& line_letters line_letters.get((line_index-1) as usize).unwrap().is_some() &&
.get((line_index - 1) as usize) min_length == 1
.unwrap() ) {
.is_some()
&& min_length == 1)
{
// if-statement is basically saying that if we're at the start of the process (min_length==1) and there's a word still to our left, // if-statement is basically saying that if we're at the start of the process (min_length==1) and there's a word still to our left,
// just stop. Other versions of the for-loops that call this function will have picked up that case. // just stop. Other versions of the for-loops that call this function will have picked up that case.
self.evaluate_spot_heading_left( self.evaluate_spot_heading_left(
@ -311,8 +315,9 @@ impl AI {
current_play, current_play,
min_length, min_length,
current_points, current_points,
all_moves, all_moves
); );
} }
} }
None => { None => {
@ -326,9 +331,10 @@ impl AI {
min_length, min_length,
current_points, current_points,
coord_mapper, coord_mapper,
&available_letters, &available_letters,
letter.clone(), letter.clone(),
all_moves, all_moves
); );
} }
@ -342,10 +348,12 @@ impl AI {
current_play, current_play,
min_length + 1, min_length + 1,
current_points, current_points,
all_moves, all_moves
); );
} }
} }
} }
fn evaluate_letter_at_spot( fn evaluate_letter_at_spot(
@ -358,10 +366,13 @@ impl AI {
current_points: &MoveScoring, current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper, coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>, available_letters: &Vec<Letter>,
letter: Letter, letter: Letter,
all_moves: &mut HashSet<CompleteMove>, all_moves: &mut HashSet<CompleteMove>
) { ) {
if letter.is_blank { if letter.is_blank {
// need to loop through alphabet // need to loop through alphabet
for alpha in ALPHABET { for alpha in ALPHABET {
@ -378,7 +389,8 @@ impl AI {
coord_mapper, coord_mapper,
available_letters, available_letters,
letter, letter,
all_moves, all_moves
); );
} }
} else { } else {
@ -392,9 +404,10 @@ impl AI {
coord_mapper, coord_mapper,
available_letters, available_letters,
letter.clone(), letter.clone(),
all_moves, all_moves
); );
} }
} }
fn evaluate_non_blank_letter_at_spot( fn evaluate_non_blank_letter_at_spot(
@ -406,12 +419,15 @@ impl AI {
min_length: usize, min_length: usize,
current_points: &MoveScoring, current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper, coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>, available_letters: &Vec<Letter>,
letter: Letter, letter: Letter,
all_moves: &mut HashSet<CompleteMove>, all_moves: &mut HashSet<CompleteMove>,
) { ) {
// let's now assign the letter to this spot // let's now assign the letter to this spot
let mut line_letters = line_letters.clone(); let mut line_letters = line_letters.clone();
*line_letters.get_mut(line_index as usize).unwrap() = Some(letter); *line_letters.get_mut(line_index as usize).unwrap() = Some(letter);
@ -442,11 +458,7 @@ impl AI {
// making copy // making copy
let mut current_points = current_points.clone(); let mut current_points = current_points.clone();
let cell_type = self let cell_type = self.board_view.get_cell(coord_mapper.line_to_coord(line_index as u8)).unwrap().cell_type;
.board_view
.get_cell(coord_mapper.line_to_coord(line_index as u8))
.unwrap()
.cell_type;
// first we score cross letters // first we score cross letters
if let Some(map) = cross_letters { if let Some(map) = cross_letters {
@ -496,8 +508,7 @@ impl AI {
current_points.main_scoring += letter.points * 2; current_points.main_scoring += letter.points * 2;
} }
CellType::TripleLetter => { CellType::TripleLetter => {
current_points.main_scoring += letter.points * 3; current_points.main_scoring += letter.points * 3;}
}
}; };
// finally, while we know that we're in a valid substring we should check if this is a valid word or not // finally, while we know that we're in a valid substring we should check if this is a valid word or not
@ -505,8 +516,7 @@ impl AI {
if word.len() >= min_length && self.dictionary.contains(&word) { if word.len() >= min_length && self.dictionary.contains(&word) {
let new_move = CompleteMove { let new_move = CompleteMove {
moves: current_play.clone(), moves: current_play.clone(),
score: current_points.cross_scoring score: current_points.cross_scoring + current_points.main_scoring*current_points.multiplier,
+ current_points.main_scoring * current_points.multiplier,
}; };
all_moves.insert(new_move); all_moves.insert(new_move);
} }
@ -515,26 +525,29 @@ impl AI {
let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1); let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1);
let mut skipped_one = false; let mut skipped_one = false;
available_letters.iter().for_each(|in_tray| { available_letters.iter()
if skipped_one || !in_tray.partial_match(&letter) { .for_each(|in_tray| {
new_available_letters.push(*in_tray); if skipped_one || !in_tray.partial_match(&letter) {
} else { new_available_letters.push(*in_tray);
skipped_one = true; } else {
} skipped_one = true;
}); }
});
assert!(skipped_one); // at least one should have been removed assert!(skipped_one); // at least one should have been removed
self.evaluate_spot_heading_right( self.evaluate_spot_heading_right(
&line_letters, &line_letters,
line_cross_letters, line_cross_letters,
line_index + 1, line_index+1,
current_play, current_play,
min_length, min_length,
&current_points, &current_points,
coord_mapper, coord_mapper,
&new_available_letters, &new_available_letters,
all_moves,
all_moves
); );
} }
@ -547,11 +560,13 @@ impl AI {
min_length: usize, min_length: usize,
current_points: &MoveScoring, current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper, coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>, available_letters: &Vec<Letter>,
all_moves: &mut HashSet<CompleteMove>, all_moves: &mut HashSet<CompleteMove>,
) { ) {
// out-of-bounds check // out-of-bounds check
if line_index < 0 || line_index >= GRID_LENGTH as i8 { if line_index < 0 || line_index >= GRID_LENGTH as i8 {
return; return;
@ -571,11 +586,11 @@ impl AI {
&current_points, &current_points,
coord_mapper, coord_mapper,
available_letters, available_letters,
all_moves, all_moves
); );
} }
None => { None => {
// there's a blank spot, so let's loop through every available letter and evaluate at this spot // there's a blank spot, so let's loop through every available letter and evaluate at this spot
for letter in available_letters { for letter in available_letters {
self.evaluate_letter_at_spot( self.evaluate_letter_at_spot(
line_letters, line_letters,
@ -586,8 +601,10 @@ impl AI {
current_points, current_points,
coord_mapper, coord_mapper,
available_letters, available_letters,
letter.clone(), letter.clone(),
all_moves, all_moves
) )
} }
} }
@ -608,29 +625,25 @@ impl AI {
} }
let mut check_for_valid_moves = |coords: Coordinates| { let mut check_for_valid_moves = |coords: Coordinates| {
let cell = new.get_cell(coords).unwrap(); let cell = new.get_cell(coords).unwrap();
if cell.value.is_none() { if cell.value.is_none() {
let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords); let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords);
let valid_column_moves = let valid_column_moves = self.get_letters_that_make_words(new, Direction::Column, coords);
self.get_letters_that_make_words(new, Direction::Column, coords);
let existing_row_moves = let existing_row_moves = self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
*existing_row_moves = valid_row_moves; *existing_row_moves = valid_row_moves;
let existing_column_moves = self let existing_column_moves = self.column_cross_tiles.get_mut(coords.map_to_index()).unwrap();
.column_cross_tiles
.get_mut(coords.map_to_index())
.unwrap();
*existing_column_moves = valid_column_moves; *existing_column_moves = valid_column_moves;
} }
}; };
updated_rows.iter().for_each(|&j| { updated_rows.iter().for_each(|&j| {
for i in 0..GRID_LENGTH { for i in 0..GRID_LENGTH {
let coords = Coordinates(i, j); let coords = Coordinates(i, j);
check_for_valid_moves(coords); check_for_valid_moves(coords);
} }
}); });
updated_columns.iter().for_each(|&i| { updated_columns.iter().for_each(|&i| {
@ -639,14 +652,10 @@ impl AI {
check_for_valid_moves(coords); check_for_valid_moves(coords);
} }
}); });
} }
fn get_letters_that_make_words( fn get_letters_that_make_words(&self, new: &Board, direction: Direction, coords: Coordinates) -> Option<MoveMap> {
&self,
new: &Board,
direction: Direction,
coords: Coordinates,
) -> Option<MoveMap> {
let is_anchored = check_if_anchored(new, coords, direction); let is_anchored = check_if_anchored(new, coords, direction);
if !is_anchored { if !is_anchored {
return None; return None;
@ -659,7 +668,7 @@ impl AI {
let cell = copy_grid.get_cell_mut(coords).unwrap(); let cell = copy_grid.get_cell_mut(coords).unwrap();
cell.value = Some(Letter { cell.value = Some(Letter {
text: letter, text: letter,
points: 0, // points are accounted for later points: 0, // points are accounted for later
ephemeral: false, // so that tile bonuses are ignored ephemeral: false, // so that tile bonuses are ignored
is_blank: false, is_blank: false,
}); });
@ -678,9 +687,11 @@ impl AI {
Some(new_map) Some(new_map)
} }
} }
fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool { fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool{
// Middle tile is always considered anchored // Middle tile is always considered anchored
if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 { if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 {
return true; return true;
@ -688,7 +699,7 @@ fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) ->
let has_letter = |alt_coord: Option<Coordinates>| -> bool { let has_letter = |alt_coord: Option<Coordinates>| -> bool {
match alt_coord { match alt_coord {
None => false, None => {false}
Some(alt_coord) => { Some(alt_coord) => {
let cell = grid.get_cell(alt_coord).unwrap(); let cell = grid.get_cell(alt_coord).unwrap();
cell.value.is_some() cell.value.is_some()
@ -708,11 +719,7 @@ fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> Stri
if start_word < 1 { if start_word < 1 {
break; break;
} }
if line_letters if line_letters.get((start_word - 1) as usize).unwrap().is_none() {
.get((start_word - 1) as usize)
.unwrap()
.is_none()
{
break; break;
} }
start_word -= 1; start_word -= 1;
@ -731,20 +738,19 @@ fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> Stri
let mut str = String::new(); let mut str = String::new();
line_letters[(start_word as usize)..((end_word + 1) as usize)] line_letters[(start_word as usize)..((end_word + 1) as usize)]
.iter() .iter().for_each(|letter| {
.for_each(|letter| { str.push(letter.unwrap().text);
str.push(letter.unwrap().text); });
});
str str
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::dictionary::Dictionary;
use rand::rngs::SmallRng; use rand::rngs::SmallRng;
use rand::SeedableRng; use rand::SeedableRng;
use crate::dictionary::Dictionary;
use super::*;
fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) { fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) {
let cell = board.get_cell_mut(Coordinates(x, y)).unwrap(); let cell = board.get_cell_mut(Coordinates(x, y)).unwrap();
@ -787,31 +793,31 @@ mod tests {
dictionary.insert("APPLE".to_string(), 0.9); dictionary.insert("APPLE".to_string(), 0.9);
let mut tray = Tray::new(7); let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter { tray.letters[0] = Some(Letter{
text: 'A', text: 'A',
points: 5, points: 5,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[1] = Some(Letter { tray.letters[1] = Some(Letter{
text: 'P', text: 'P',
points: 4, points: 4,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[2] = Some(Letter { tray.letters[2] = Some(Letter{
text: 'P', text: 'P',
points: 4, points: 4,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[3] = Some(Letter { tray.letters[3] = Some(Letter{
text: 'L', text: 'L',
points: 3, points: 3,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[4] = Some(Letter { tray.letters[4] = Some(Letter{
text: 'E', text: 'E',
points: 4, points: 4,
ephemeral: false, ephemeral: false,
@ -826,12 +832,14 @@ mod tests {
assert_eq!(moves.len(), 10); assert_eq!(moves.len(), 10);
tray.letters[4] = Some(Letter { tray.letters[4] = Some(
text: ' ', Letter {
points: 0, text: ' ',
ephemeral: false, points: 0,
is_blank: true, ephemeral: false,
}); is_blank: true,
}
);
let moves = ai.find_all_moves(&tray, &board); let moves = ai.find_all_moves(&tray, &board);
@ -849,6 +857,8 @@ mod tests {
set_cell(&mut board, 7, 9, 'A', 1); set_cell(&mut board, 7, 9, 'A', 1);
set_cell(&mut board, 7, 10, 'T', 1); set_cell(&mut board, 7, 10, 'T', 1);
let difficulty = Difficulty { let difficulty = Difficulty {
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
randomness: 0.0, randomness: 0.0,
@ -859,25 +869,25 @@ mod tests {
dictionary.insert("SLAM".to_string(), 0.9); dictionary.insert("SLAM".to_string(), 0.9);
let mut tray = Tray::new(7); let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter { tray.letters[0] = Some(Letter{
text: 'S', text: 'S',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[1] = Some(Letter { tray.letters[1] = Some(Letter{
text: 'L', text: 'L',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[6] = Some(Letter { tray.letters[6] = Some(Letter{
text: 'A', text: 'A',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[3] = Some(Letter { tray.letters[3] = Some(Letter{
text: 'M', text: 'M',
points: 3, points: 3,
ephemeral: false, ephemeral: false,
@ -888,10 +898,7 @@ mod tests {
ai.update_state(&board); ai.update_state(&board);
let end_of_boat = ai let end_of_boat = ai.column_cross_tiles.get(Coordinates(7, 11).map_to_index()).unwrap();
.column_cross_tiles
.get(Coordinates(7, 11).map_to_index())
.unwrap();
assert!(end_of_boat.is_some()); assert!(end_of_boat.is_some());
assert_eq!(end_of_boat.as_ref().unwrap().len(), 1); assert_eq!(end_of_boat.as_ref().unwrap().len(), 1);
@ -899,6 +906,7 @@ mod tests {
println!("Moves are {:?}", moves); println!("Moves are {:?}", moves);
// 3 possible moves - // 3 possible moves -
// 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS' // 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS'
// 2. Put 'S' at end of 'BOAT' // 2. Put 'S' at end of 'BOAT'
@ -912,6 +920,7 @@ mod tests {
let play = best_move.convert_to_play(&tray); let play = best_move.convert_to_play(&tray);
println!("Play is {:?}", play); println!("Play is {:?}", play);
} }
#[test] #[test]
@ -934,30 +943,25 @@ mod tests {
let above_cell_coords = Coordinates(7, 6); let above_cell_coords = Coordinates(7, 6);
let left_cell_cords = Coordinates(6, 7); let left_cell_cords = Coordinates(6, 7);
let row_cross_tiles = ai let row_cross_tiles = ai.row_cross_tiles.get(left_cell_cords.map_to_index()).unwrap();
.row_cross_tiles let column_cross_tiles = ai.column_cross_tiles.get(above_cell_coords.map_to_index()).unwrap();
.get(left_cell_cords.map_to_index())
.unwrap();
let column_cross_tiles = ai
.column_cross_tiles
.get(above_cell_coords.map_to_index())
.unwrap();
assert_eq!(row_cross_tiles.as_ref().unwrap().len(), 1); assert_eq!(row_cross_tiles.as_ref().unwrap().len(), 1);
assert_eq!(column_cross_tiles.as_ref().unwrap().len(), 1); assert_eq!(column_cross_tiles.as_ref().unwrap().len(), 1);
let far_off_tiles = ai.row_cross_tiles.get(0).unwrap(); let far_off_tiles = ai.row_cross_tiles.get(0).unwrap();
assert!(far_off_tiles.is_none()); assert!(far_off_tiles.is_none());
} }
#[test] #[test]
fn test_valid_moves() { fn test_valid_moves() {
let mut board = Board::new(); let mut board = Board::new();
set_cell(&mut board, 7 - 3, 7 + 3, 'Z', 1); set_cell(&mut board, 7-3, 7+3, 'Z', 1);
set_cell(&mut board, 6 - 3, 8 + 3, 'A', 1); set_cell(&mut board, 6-3, 8+3, 'A', 1);
set_cell(&mut board, 7 - 3, 8 + 3, 'A', 1); set_cell(&mut board, 7-3, 8+3, 'A', 1);
set_cell(&mut board, 7 - 3, 9 + 3, 'Z', 1); set_cell(&mut board, 7-3, 9+3, 'Z', 1);
let difficulty = Difficulty { let difficulty = Difficulty {
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
@ -968,7 +972,7 @@ mod tests {
dictionary.insert("AA".to_string(), 0.5); dictionary.insert("AA".to_string(), 0.5);
let mut tray = Tray::new(7); let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter { tray.letters[0] = Some(Letter{
text: 'A', text: 'A',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
@ -988,52 +992,50 @@ mod tests {
fn test_starting_move() { fn test_starting_move() {
let mut board = Board::new(); let mut board = Board::new();
let difficulty = Difficulty { let difficulty = Difficulty{proportion: 0.1, randomness: 0.0};
proportion: 0.1,
randomness: 0.0,
};
let mut dictionary = DictionaryImpl::new(); let mut dictionary = DictionaryImpl::new();
dictionary.insert("TWEETED".to_string(), 0.2); dictionary.insert("TWEETED".to_string(), 0.2);
let mut tray = Tray::new(7); let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter { tray.letters[0] = Some(Letter{
text: 'I', text: 'I',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[1] = Some(Letter { tray.letters[1] = Some(Letter{
text: 'R', text: 'R',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[2] = Some(Letter { tray.letters[2] = Some(Letter{
text: 'W', text: 'W',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[3] = Some(Letter { tray.letters[3] = Some(Letter{
text: 'I', text: 'I',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[4] = Some(Letter { tray.letters[4] = Some(Letter{
text: 'T', text: 'T',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[5] = Some(Letter { tray.letters[5] = Some(Letter{
text: 'E', text: 'E',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
is_blank: false, is_blank: false,
}); });
tray.letters[6] = Some(Letter { tray.letters[6] = Some(Letter{
text: 'D', text: 'D',
points: 1, points: 1,
ephemeral: false, ephemeral: false,
@ -1046,5 +1048,7 @@ mod tests {
println!("Available moves are {:?}", all_moves); println!("Available moves are {:?}", all_moves);
assert!(all_moves.is_empty()); assert!(all_moves.is_empty());
} }
}
}

213
src/wasm.rs Normal file
View file

@ -0,0 +1,213 @@
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::Error;
use tsify::Tsify;
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::board::{CellType, Letter};
use crate::game::{Game, GameState, PlayedTile};
use crate::player_interaction::ai::Difficulty;
#[wasm_bindgen]
pub struct GameWasm(Game);
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub enum ResponseType {
OK,
ERR,
}
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct MyResult<E: Serialize> {
response_type: ResponseType,
value: E,
game_state: Option<GameState>,
}
#[wasm_bindgen]
impl GameWasm {
#[wasm_bindgen(constructor)]
pub fn new(seed: u64, dictionary_text: &str, ai_difficulty: JsValue) -> GameWasm {
let difficulty: Difficulty = serde_wasm_bindgen::from_value(ai_difficulty).unwrap();
GameWasm(Game::new(seed, dictionary_text, vec!["Player".to_string()], vec![difficulty]))
}
pub fn get_tray(&self, name: &str) -> Result<JsValue, Error> {
let tray = self.0.player_states.get_tray(name);
serde_wasm_bindgen::to_value(&tray)
}
pub fn get_board_cell_types(&self) -> Result<JsValue, Error> {
let board = self.0.get_board();
let cell_types: Vec<CellType> = board.cells.iter().map(|cell| -> CellType {
cell.cell_type.clone()
}).collect();
serde_wasm_bindgen::to_value(&cell_types)
}
pub fn get_board_letters(&self) -> Result<JsValue, Error> {
let board = self.0.get_board();
let letters: Vec<Option<Letter>> = board.cells.iter().map(|cell| -> Option<Letter> {
cell.value.clone()
}).collect();
serde_wasm_bindgen::to_value(&letters)
}
pub fn receive_play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> Result<JsValue, Error> {
let tray_tile_locations: Vec<Option<PlayedTile>> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
let result = self.0.receive_play(tray_tile_locations, commit_move);
match result {
Ok((x, game_state)) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: x,
game_state: Some(game_state)
})
},
Err(e) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})
}
}
}
pub fn get_scores(&self) -> Result<JsValue, JsValue> {
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct PlayerAndScore {
name: String,
score: u32,
}
let scores: Vec<PlayerAndScore> = self.0.player_states.0.iter()
.map(|player_state| {
PlayerAndScore {
name: player_state.player.get_name().to_string(),
score: player_state.score,
}
})
.collect();
Ok(serde_wasm_bindgen::to_value(&scores)?)
}
pub fn exchange_tiles(&mut self, tray_tile_locations: JsValue) -> Result<JsValue, Error>{
let tray_tile_locations: Vec<bool> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
match self.0.exchange_tiles(tray_tile_locations) {
Ok((_, turn_action, state)) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: turn_action,
game_state: Some(state),
})
},
Err(e) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})
}
}
}
pub fn add_word(&mut self, word: String) {
self.0.add_word(word);
}
pub fn skip_turn(&mut self) -> Result<JsValue, Error>{
let result = self.0.pass();
match result {
Ok(game_state) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: "Turn passed",
game_state: Some(game_state),
})?)
},
Err(e) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})?)
}
}
}
pub fn advance_turn(&mut self) -> Result<JsValue, Error> {
let result = self.0.advance_turn();
match result {
Ok((turn_advance_result, game_state)) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: turn_advance_result,
game_state: Some(game_state),
})?)
},
Err(e) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})?)
}
}
}
pub fn get_current_player(&self) -> String {
self.0.current_player_name()
}
pub fn get_remaining_tiles(&self) -> usize {
self.0.get_remaining_tiles()
}
pub fn get_player_tile_count(&self, player: &str) -> Result<JsValue, Error> {
match self.0.get_player_tile_count(player) {
Ok(count) => {
Ok(serde_wasm_bindgen::to_value(&MyResult{
response_type: ResponseType::OK,
value: count,
game_state: None,
})?)
},
Err(msg) => {
Ok(serde_wasm_bindgen::to_value(&MyResult{
response_type: ResponseType::OK,
value: msg,
game_state: None,
})?)
}
}
}
}

1921
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,13 @@
"dependencies": { "dependencies": {
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"word_grid": "file:../wasm/pkg" "word_grid": "file:../pkg"
}, },
"devDependencies": { "devDependencies": {
"@parcel/transformer-less": "^2.9.3", "@parcel/transformer-less": "^2.9.3",
"@types/react": "^18.2.18", "@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"parcel": "^2.12.0", "parcel": "^2.9.3",
"process": "^0.11.10" "process": "^0.11.10"
}, },
"scripts": { "scripts": {

View file

@ -1,5 +1,16 @@
import * as React from "react"; import * as React from "react";
import {useEffect, useReducer, useRef, useState} from "react"; import {useEffect, useMemo, useReducer, useRef, useState} from "react";
import {
GameState,
GameWasm,
MyResult,
PlayedTile,
PlayerAndScore,
ScoreResult,
Tray,
TurnAction,
TurnAdvanceResult
} from "../../pkg/word_grid";
import { import {
Direction, Direction,
GRID_LENGTH, GRID_LENGTH,
@ -17,55 +28,29 @@ import {
} from "./utils"; } from "./utils";
import {TileExchangeModal} from "./TileExchange"; import {TileExchangeModal} from "./TileExchange";
import {Grid, Scores, TileTray} from "./UI"; import {Grid, Scores, TileTray} from "./UI";
import {API, APIState, GameState, TurnAction, Tray, PlayedTile, Letter} from "./api";
function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) { function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
newItem = React.cloneElement(newItem, {key: existingLog.length}) newItem = React.cloneElement(newItem, { key: existingLog.length })
existingLog.push(newItem); existingLog.push(newItem);
return existingLog.slice(); return existingLog.slice();
} }
export function Game(props: { export function Game(props: {
api: API, wasm: GameWasm,
settings: Settings, settings: Settings,
end_game_fn: () => void, end_game_fn: () => void,
}) { }) {
const [api_state, setAPIState] = useState<APIState>(undefined); const cellTypes = useMemo(() => {
const [isLoading, setIsLoading] = useState<boolean>(true); return props.wasm.get_board_cell_types();
}, []);
const [isGameOver, setGameOver] = useState<boolean>(false);
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1); const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
const [currentTurnNumber, setCurrentTurnNumber] = useState<number>(0);
let isGameOver = false;
if (api_state !== undefined) {
isGameOver = api_state.public_information.game_state.type === "Ended";
}
function load() {
// setAPIState(undefined);
// setIsLoading(true);
const result = props.api.load();
result.then(
(state) => {
setAPIState(state);
setIsLoading(false);
}
)
.catch((error) => {
console.log("load() failed")
console.log(error);
});
}
useEffect(() => {
load();
}, [])
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => { const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
const newLetterData = [] as HighlightableLetterData[]; const newLetterData = [] as HighlightableLetterData[];
for (let i = 0; i < GRID_LENGTH * GRID_LENGTH; i++) { for(let i=0; i<GRID_LENGTH * GRID_LENGTH; i++) {
newLetterData.push(undefined); newLetterData.push(undefined);
} }
return newLetterData; return newLetterData;
@ -73,26 +58,28 @@ export function Game(props: {
}); });
function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData { function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData {
if (update.action == GridArrowDispatchActionType.CLEAR) { console.log({update});
if(update.action == GridArrowDispatchActionType.CLEAR) {
return null; return null;
} else if (update.action == GridArrowDispatchActionType.CYCLE) { } else if (update.action == GridArrowDispatchActionType.CYCLE) {
// if there's nothing where the user clicked, we create a right arrow. // if there's nothing where the user clicked, we create a right arrow.
if (existing == null || existing.position != update.position) { if(existing == null || existing.position != update.position) {
return { return {
direction: Direction.RIGHT, position: update.position direction: Direction.RIGHT, position: update.position
} }
// if there's a right arrow, we shift to downwards // if there's a right arrow, we shift to downwards
} else if (existing.direction == Direction.RIGHT) { } else if(existing.direction == Direction.RIGHT) {
return { return {
direction: Direction.DOWN, position: existing.position direction: Direction.DOWN, position: existing.position
} }
// if there's a down arrow, we clear it // if there's a down arrow, we clear it
} else if (existing.direction == Direction.DOWN) { } else if (existing.direction == Direction.DOWN){
return null; return null;
} }
} else if (update.action == GridArrowDispatchActionType.SHIFT) { } else if (update.action == GridArrowDispatchActionType.SHIFT) {
if (existing == null) { if(existing == null) {
// no arrow to shift // no arrow to shift
return null; return null;
} else { } else {
@ -101,7 +88,7 @@ export function Game(props: {
// we loop because we want to skip over letters that are already set // we loop because we want to skip over letters that are already set
while (current_x < GRID_LENGTH && current_y < GRID_LENGTH) { while (current_x < GRID_LENGTH && current_y < GRID_LENGTH) {
if (existing.direction == Direction.RIGHT) { if(existing.direction == Direction.RIGHT) {
current_x += 1; current_x += 1;
} else { } else {
current_y += 1; current_y += 1;
@ -116,7 +103,7 @@ export function Game(props: {
} }
} }
if (current_x < GRID_LENGTH && current_y < GRID_LENGTH && boardLetters[new_position] == null && !tray_letter_at_position) { if(current_x < GRID_LENGTH && current_y < GRID_LENGTH && boardLetters[new_position] == null && !tray_letter_at_position) {
return { return {
direction: existing.direction, direction: existing.direction,
position: new_position, position: new_position,
@ -132,30 +119,38 @@ export function Game(props: {
} }
function exchangeFunction(selectedArray: Array<boolean>) { function exchangeFunction(selectedArray: Array<boolean>) {
const result = props.api.exchange(selectedArray);
result const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray);
.then(
(_api_state) => { if(result.response_type === "ERR") {
load(); logDispatch(<div><em>{(result.value as string)}</em></div>);
}) } else {
.catch((error) => { handlePlayerAction(result.value as TurnAction, props.settings.playerName);
console.error({error}); setTurnCount(turnCount + 1);
logDispatch(<div>{error}</div>);
}); if(result.game_state.type === "Ended") {
endGame(result.game_state);
}
}
} }
function addWordFn(word: string) {
props.wasm.add_word(word);
}
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null); const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
const [logInfo, logDispatch] = useReducer(addLogInfo, []); const [logInfo, logDispatch] = useReducer(addLogInfo, []);
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) { function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
if (update.action === TileDispatchActionType.RETRIEVE) {
let tray: Tray = api_state.tray; if(update.action === TileDispatchActionType.RETRIEVE) {
if (update.override) { let tray: Tray = props.wasm.get_tray("Player");
if(update.override) {
playerLetters = []; playerLetters = [];
} }
@ -165,13 +160,13 @@ export function Game(props: {
let startIndex = matchCoordinate(playerLetters, update.start); let startIndex = matchCoordinate(playerLetters, update.start);
let endIndex = matchCoordinate(playerLetters, update.end); let endIndex = matchCoordinate(playerLetters, update.end);
if (startIndex != null) { if(startIndex != null) {
let startLetter = playerLetters[startIndex]; let startLetter = playerLetters[startIndex];
startLetter.location = update.end.location; startLetter.location = update.end.location;
startLetter.index = update.end.index; startLetter.index = update.end.index;
} }
if (endIndex != null) { if(endIndex != null) {
let endLetter = playerLetters[endIndex]; let endLetter = playerLetters[endIndex];
endLetter.location = update.start.location; endLetter.location = update.start.location;
endLetter.index = update.start.index; endLetter.index = update.start.index;
@ -183,7 +178,7 @@ export function Game(props: {
} else if (update.action === TileDispatchActionType.SET_BLANK) { } else if (update.action === TileDispatchActionType.SET_BLANK) {
const blankLetter = playerLetters[update.blankIndex]; const blankLetter = playerLetters[update.blankIndex];
if (blankLetter.text !== update.newBlankValue) { if(blankLetter.text !== update.newBlankValue) {
blankLetter.text = update.newBlankValue; blankLetter.text = update.newBlankValue;
if (blankLetter.location == LocationType.GRID) { if (blankLetter.location == LocationType.GRID) {
setConfirmedScorePoints(-1); setConfirmedScorePoints(-1);
@ -195,7 +190,7 @@ export function Game(props: {
return mergeTrays(playerLetters, playerLetters); return mergeTrays(playerLetters, playerLetters);
} else if (update.action === TileDispatchActionType.MOVE_TO_ARROW) { } else if (update.action === TileDispatchActionType.MOVE_TO_ARROW) {
// let's verify that the arrow is defined, otherwise do nothing // let's verify that the arrow is defined, otherwise do nothing
if (gridArrow != null) { if(gridArrow != null) {
const end_position = { const end_position = {
location: LocationType.GRID, location: LocationType.GRID,
index: gridArrow.position, index: gridArrow.position,
@ -220,6 +215,35 @@ export function Game(props: {
} }
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []); const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
const [turnCount, setTurnCount] = useState<number>(1);
const playerAndScores: PlayerAndScore[] = useMemo(() => {
return props.wasm.get_scores();
}, [turnCount, isGameOver]);
useEffect(() => {
const newLetterData = props.wasm.get_board_letters() as HighlightableLetterData[];
// we need to go through and set 'highlight' field to either true or false
// it will always be false if the player that just went was the AI
// TODO - build in support for multiple other players
for(let i=0; i<newLetterData.length; i++) {
const newLetter = newLetterData[i];
if(boardLetters[i] === undefined && newLetter !== undefined && playerTurnName == props.settings.playerName) {
newLetter.highlight = true;
} else if(newLetter !== undefined) {
newLetter.highlight = false;
}
}
setBoardLetters(newLetterData);
}, [turnCount]);
const playerTurnName = useMemo(() => {
return props.wasm.get_current_player();
}, [turnCount]);
const logDivRef = useRef(null); const logDivRef = useRef(null);
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false); const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
@ -231,21 +255,34 @@ export function Game(props: {
} }
}, [logInfo]); }, [logInfo]);
function handlePlayerAction(action: TurnAction, playerName: string) { const remainingTiles = useMemo(() => {
return props.wasm.get_remaining_tiles();
}, [turnCount, isGameOver]);
if (action.type == "PlayTiles") { const remainingAITiles = useMemo(() => {
let result = props.wasm.get_player_tile_count(props.settings.aiName) as MyResult<number | String>;
if(result.response_type == "OK") {
return result.value as number;
} else {
console.error(result.value);
return -1;
}
}, [turnCount, isGameOver]);
function handlePlayerAction(action: TurnAction, playerName: string) {
if (action.type == "PlayTiles"){
const result = action.result; const result = action.result;
result.words.sort((a, b) => b.score - a.score); result.words.sort((a, b) => b.score - a.score);
for (let word of result.words) { for(let word of result.words) {
logDispatch(<div>{playerName} received {word.score} points for playing '{word.word}.'</div>); logDispatch(<div>{playerName} received {word.score} points for playing '{word.word}.'</div>);
} }
logDispatch(<div>{playerName} received a total of <strong>{result.total} points</strong> for their turn. logDispatch(<div>{playerName} received a total of <strong>{result.total} points</strong> for their turn.</div>);
</div>); } else if(action.type == "ExchangeTiles") {
} else if (action.type == "ExchangeTiles") { logDispatch(<div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for their turn.</div>);
logDispatch( }
<div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for else if(action.type == "Pass"){
their turn.</div>);
} else if (action.type == "Pass") {
logDispatch(<div>{playerName} passed.</div>); logDispatch(<div>{playerName} passed.</div>);
} }
@ -255,26 +292,28 @@ export function Game(props: {
function endGame(state: GameState) { function endGame(state: GameState) {
if (state.type != "InProgress") { if(state.type != "InProgress") {
setGameOver(true);
logDispatch(<h4>Scoring</h4>); logDispatch(<h4>Scoring</h4>);
const scores = api_state.public_information.players; const scores = props.wasm.get_scores() as PlayerAndScore[];
let pointsBonus = 0; let pointsBonus = 0;
for (const playerAndScore of scores) { for(const playerAndScore of scores) {
const name = playerAndScore.name; const name = playerAndScore.name;
if (name == state.finisher) { if(name == state.finisher) {
// we'll do the finisher last // we'll do the finisher last
continue continue
} }
const letters = state.remaining_tiles.get(name); const letters = state.remaining_tiles.get(name);
if (letters.length == 0) { if(letters.length == 0) {
logDispatch(<div>{name} has no remaining tiles.</div>); logDispatch(<div>{name} has no remaining tiles.</div>);
} else { } else {
let pointsLost = 0; let pointsLost = 0;
let letterListStr = ''; let letterListStr = '';
for (let i = 0; i < letters.length; i++) { for(let i=0; i<letters.length; i++) {
const letter = letters[i]; const letter = letters[i];
const letterText = letter.is_blank ? 'a blank' : letter.text; const letterText = letter.is_blank ? 'a blank' : letter.text;
pointsLost += letter.points; pointsLost += letter.points;
@ -282,13 +321,13 @@ export function Game(props: {
letterListStr += letterText; letterListStr += letterText;
// we're doing a list of 3 or more so add commas // we're doing a list of 3 or more so add commas
if (letters.length > 2) { if(letters.length > 2) {
if (i == letters.length - 2) { if(i == letters.length - 2) {
letterListStr += ', and '; letterListStr += ', and ';
} else if (i < letters.length - 2) { } else if (i < letters.length - 2) {
letterListStr += ', '; letterListStr += ', ';
} }
} else if (i == 0 && letters.length == 2) { } else if (i == 0 && letters.length == 2){
// list of 2 // list of 2
letterListStr += ' and '; letterListStr += ' and ';
} }
@ -299,7 +338,7 @@ export function Game(props: {
} }
} }
if (state.finisher != null) { if(state.finisher != null) {
logDispatch(<div>{state.finisher} receives {pointsBonus} bonus for completing first.</div>); logDispatch(<div>{state.finisher} receives {pointsBonus} bonus for completing first.</div>);
} }
@ -309,14 +348,15 @@ export function Game(props: {
.at(0); .at(0);
const playersAtHighest = scores.filter((score) => score.score == highestScore); const playersAtHighest = scores.filter((score) => score.score == highestScore);
let endGameMsg: string; let endGameMsg: string = '';
if (playersAtHighest.length > 1 && state.finisher == null) { if(playersAtHighest.length > 1 && state.finisher == null) {
endGameMsg = "Tie game!"; endGameMsg = "Tie game!";
} else if (playersAtHighest.length > 1 && state.finisher != null) { } else if (playersAtHighest.length > 1 && state.finisher != null) {
// if there's a tie then the finisher gets the win // if there's a tie then the finisher gets the win
endGameMsg = `${playersAtHighest[0].name} won by finishing first!`; endGameMsg = `${playersAtHighest[0].name} won by finishing first!`;
} else { }
else {
endGameMsg = `${playersAtHighest[0].name} won!`; endGameMsg = `${playersAtHighest[0].name} won!`;
} }
logDispatch(<h4>Game over - {endGameMsg}</h4>); logDispatch(<h4>Game over - {endGameMsg}</h4>);
@ -327,79 +367,35 @@ export function Game(props: {
} }
function updateBoardLetters(newLetters: Array<Letter>) { function runAI() {
const newLetterData = newLetters as HighlightableLetterData[]; const result: MyResult<TurnAdvanceResult> = props.wasm.advance_turn();
if(result.response_type === "OK" && result.value.type == "AIMove") {
for (let i = 0; i < newLetterData.length; i++) { handlePlayerAction(result.value.action, props.settings.aiName);
const newLetter = newLetterData[i]; if(result.game_state.type === "Ended") {
if (newLetter != null) { endGame(result.game_state);
newLetter.highlight = false;
} }
} else {
// this would be quite surprising
console.error({result});
} }
// loop through the histories backwards until we reach our player setTurnCount(turnCount + 1);
for (let j = api_state.public_information.history.length - 1; j >= 0; j--) {
const update = api_state.public_information.history[j];
if (update.player == props.settings.playerName) {
break
}
if (update.type.type === "PlayTiles") {
for (let i of update.type.locations) {
newLetterData[i].highlight = true;
}
}
}
setBoardLetters(newLetterData);
} }
useEffect(() => { useEffect(() => {
if (api_state) { trayDispatch({action: TileDispatchActionType.RETRIEVE});
console.debug(api_state); setConfirmedScorePoints(-1);
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR}); if(!isGameOver){
trayDispatch({action: TileDispatchActionType.RETRIEVE}); logDispatch(<h4>Turn {turnCount}</h4>);
setConfirmedScorePoints(-1); logDispatch(<div>{playerTurnName}'s turn</div>);
updateBoardLetters(api_state.public_information.board); if(playerTurnName != props.settings.playerName && !isGameOver) {
runAI();
for (let i = currentTurnNumber; i < api_state.public_information.history.length; i++) {
if (i > currentTurnNumber) {
logDispatch(<h4>Turn {i + 1}</h4>);
const playerAtTurn = api_state.public_information.players[i % api_state.public_information.players.length].name;
logDispatch(<div>{playerAtTurn}'s turn</div>);
}
const update = api_state.public_information.history[i];
handlePlayerAction(update.type, update.player);
} }
setCurrentTurnNumber(api_state.public_information.history.length);
if (!isGameOver) {
logDispatch(<h4>Turn {api_state.public_information.history.length + 1}</h4>);
logDispatch(<div>{api_state.public_information.current_player}'s turn</div>);
} else {
endGame(api_state.public_information.game_state);
}
} }
}, [api_state]); }, [turnCount]);
if (isLoading) {
return <div>Still loading</div>;
}
const playerAndScores = api_state.public_information.players;
const remainingTiles = api_state.public_information.remaining_tiles;
let remainingAITiles = null;
for (let player of playerAndScores) {
if (player.name == 'AI') {
remainingAITiles = player.tray_tiles;
break;
}
}
return <> return <>
<TileExchangeModal <TileExchangeModal
playerLetters={playerLetters} playerLetters={playerLetters}
@ -409,7 +405,7 @@ export function Game(props: {
/> />
<div className="board-log"> <div className="board-log">
<Grid <Grid
cellTypes={api_state.public_information.cell_types} cellTypes={cellTypes}
playerLetters={playerLetters} playerLetters={playerLetters}
boardLetters={boardLetters} boardLetters={boardLetters}
tileDispatch={trayDispatch} tileDispatch={trayDispatch}
@ -418,9 +414,9 @@ export function Game(props: {
/> />
<div className="message-log"> <div className="message-log">
<button className="end-game" <button className="end-game"
onClick={() => { onClick={() => {
props.end_game_fn(); props.end_game_fn();
}} }}
> >
End Game End Game
</button> </button>
@ -441,108 +437,101 @@ export function Game(props: {
<button <button
disabled={remainingTiles == 0 || isGameOver} disabled={remainingTiles == 0 || isGameOver}
onClick={() => { onClick={() => {
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
setIsTileExchangeOpen(true); setIsTileExchangeOpen(true);
}}>Open Tile Exchange }}>Open Tile Exchange</button>
</button>
</div> </div>
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/> <TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
<div className="player-controls"> <div className="player-controls">
<button <button
className="check" className="check"
disabled={isGameOver} disabled={isGameOver}
onClick={async () => { onClick={() => {
const playedTiles = playerLetters.map((i) => { const playedTiles = playerLetters.map((i) => {
if (i === undefined) { if (i === undefined) {
return null;
}
if (i.location === LocationType.GRID) {
let result: PlayedTile = {
index: i.index,
character: undefined
};
if (i.is_blank) {
result.character = i.text;
}
return result;
}
return null; return null;
}); }
const committing = confirmedScorePoints > -1; if (i.location === LocationType.GRID) {
const result = props.api.play(playedTiles, committing); let result: PlayedTile = {
index: i.index,
character: undefined
};
if (i.is_blank) {
result.character = i.text;
}
result return result;
.then( }
(api_state) => {
const play_tiles: TurnAction = api_state.update.type; return null;
if (play_tiles.type == "PlayTiles") { });
setConfirmedScorePoints(play_tiles.result.total);
if (committing) { const result: MyResult<{ type: "PlayTiles"; result: ScoreResult } | string> = props.wasm.receive_play(playedTiles, confirmedScorePoints > -1);
load(); console.log({result});
}
} else { if(result.response_type === "ERR") {
console.error("Inaccessible branch!!!") const message = result.value as string;
} if (message.endsWith("is not a valid word")) {
} // extract out word
) const word = message.split(" ")[0];
.catch((error) => { logDispatch(<AddWordButton word={word} addWordFn={addWordFn} />);
console.error({error}); } else {
logDispatch(<div><em>{message}</em></div>);
}
if (error.endsWith(" is not a valid word")) { } else {
const word = error.split(' ')[0];
// For whatever reason I can't pass props.api.add_to_dictionary directly const score_result = (result.value as { type: "PlayTiles"; result: ScoreResult }).result;
logDispatch(<AddWordButton word={word} const total_points = score_result.total;
addWordFn={(word) => props.api.add_to_dictionary(word)}/>);
} else {
logDispatch(<div>{error}</div>);
}
}); if (confirmedScorePoints > -1) {
handlePlayerAction({type: "PlayTiles", result: score_result}, props.settings.playerName);
setTurnCount(turnCount + 1);
}
}}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points` : "Check"}</button> if (result.game_state.type === "Ended") {
endGame(result.game_state);
}
setConfirmedScorePoints(total_points);
}
}}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points` : "Check"}</button>
<button <button
className="return" className="return"
disabled={isGameOver} disabled={isGameOver}
onClick={() => { onClick={() => {
trayDispatch({action: TileDispatchActionType.RETURN}); trayDispatch({action: TileDispatchActionType.RETURN});
}}>Return Tiles }}>Return Tiles</button>
</button>
<button <button
className="pass" className="pass"
disabled={isGameOver} disabled={isGameOver}
onClick={() => { onClick={() => {
if (window.confirm("Are you sure you want to pass?")) { if (window.confirm("Are you sure you want to pass?")) {
const result = props.api.pass(); const result = props.wasm.skip_turn() as MyResult<string>;
handlePlayerAction({type: "Pass"}, props.settings.playerName);
result setTurnCount(turnCount + 1);
.then(
(_api_state) => {
load();
})
.catch((error) => {
console.error({error});
logDispatch(<div>{error}</div>);
});
if (result.game_state.type === "Ended") {
endGame(result.game_state);
} }
}}>Pass }
</button> }}>Pass</button>
</div> </div>
</div> </div>
</>; </>;
} }
function AddWordButton(props: { word: string, addWordFn: (x: string) => void }) { function AddWordButton(props: {word: string, addWordFn: (x: string) => void}) {
const [isClicked, setIsClicked] = useState<boolean>(false); const [isClicked, setIsClicked] = useState<boolean>(false);
if (!isClicked) { if (!isClicked) {

View file

@ -1,9 +1,8 @@
import * as React from "react"; import * as React from "react";
import {useState} from "react"; import {useState} from "react";
import {Difficulty, GameWasm} from '../../pkg/word_grid';
import {Settings} from "./utils"; import {Settings} from "./utils";
import {Game} from "./Game"; import {Game} from "./Game";
import {API, Difficulty} from "./api";
import {GameWasm} from "./wasm";
export function Menu(props: {settings: Settings, dictionary_text: string}) { export function Menu(props: {settings: Settings, dictionary_text: string}) {
@ -71,8 +70,8 @@ export function Menu(props: {settings: Settings, dictionary_text: string}) {
proportion: processedProportionDictionary, proportion: processedProportionDictionary,
randomness: processedAIRandomness, randomness: processedAIRandomness,
}; };
const game_wasm: API = new GameWasm(BigInt(seed), props.dictionary_text, difficulty); const game_wasm = new GameWasm(BigInt(seed), props.dictionary_text, difficulty);
const game = <Game settings={props.settings} api={game_wasm} key={seed} end_game_fn={() => setGame(null)}/> const game = <Game settings={props.settings} wasm={game_wasm} key={seed} end_game_fn={() => setGame(null)}/>
setGame(game); setGame(game);
}}>New Game</button> }}>New Game</button>
</div> </div>

View file

@ -1,8 +1,8 @@
import {addNTimes, PlayableLetterData} from "./utils"; import {addNTimes, PlayableLetterData} from "./utils";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Modal} from "./Modal"; import {Modal} from "./Modal";
import {Letter as LetterData} from "../../pkg/word_grid";
import * as React from "react"; import * as React from "react";
import {Letter} from "./api";
export function TileExchangeModal(props: { export function TileExchangeModal(props: {
playerLetters: PlayableLetterData[], playerLetters: PlayableLetterData[],
@ -105,7 +105,7 @@ function TilesExchangedTray(props: {
} }
function DummyExchangeTile(props: { function DummyExchangeTile(props: {
letter: Letter, letter: LetterData,
isSelected: boolean, isSelected: boolean,
onClick: () => void, onClick: () => void,
}){ }){

View file

@ -1,6 +1,8 @@
import * as React from "react"; import * as React from "react";
import {ChangeEvent, JSX} from "react"; import {ChangeEvent, JSX} from "react";
import {PlayerAndScore} from "../../pkg/word_grid";
import { import {
CellType,
cellTypeToDetails, cellTypeToDetails,
CoordinateData, CoordinateData,
GridArrowData, GridArrowData,
@ -12,7 +14,6 @@ import {
TileDispatch, TileDispatch,
TileDispatchActionType, TileDispatchActionType,
} from "./utils"; } from "./utils";
import {APIPlayer, CellType} from "./api";
export function TileSlot(props: { export function TileSlot(props: {
@ -233,7 +234,7 @@ export function Grid(props: {
</div> </div>
} }
export function Scores(props: {playerScores: Array<APIPlayer>}){ export function Scores(props: {playerScores: Array<PlayerAndScore>}){
let elements = props.playerScores.map((ps) => { let elements = props.playerScores.map((ps) => {
return <div key={ps.name}> return <div key={ps.name}>
<h3>{ps.name}</h3> <h3>{ps.name}</h3>

View file

@ -1,90 +0,0 @@
export interface Tray {
letters: (Letter | undefined)[];
}
export interface ScoreResult {
words: WordResult[];
total: number;
}
export interface WordResult {
word: string;
score: number;
}
export interface PlayedTile {
index: number;
character: string | undefined;
}
export interface Difficulty {
proportion: number;
randomness: number;
}
export interface Letter {
text: string;
points: number;
ephemeral: boolean;
is_blank: boolean;
}
export type TurnAction = { type: "Pass" } | { type: "ExchangeTiles"; tiles_exchanged: number } | { type: "PlayTiles"; result: ScoreResult; locations: number[] };
export enum CellType {
Normal = "Normal",
DoubleWord = "DoubleWord",
DoubleLetter = "DoubleLetter",
TripleLetter = "TripleLetter",
TripleWord = "TripleWord",
Start = "Start",
}
export type GameState = { type: "InProgress" } | { type: "Ended"; finisher: string | undefined; remaining_tiles: Map<string, Letter[]> };
export interface APIPlayer {
name: string;
tray_tiles: number;
score: number
}
export interface PublicInformation {
game_state: GameState;
board: Array<Letter>;
cell_types: Array<CellType>;
current_player: string;
players: Array<APIPlayer>;
remaining_tiles: number;
history: Array<Update>;
}
export interface Update {
type: TurnAction,
player: string;
}
export interface APIState {
public_information: PublicInformation;
tray: Tray;
update?: Update;
}
export interface Result<A, B> {
"Ok"?: A;
"Err"?: B;
}
export function is_ok<A, B>(result: Result<A, B>): boolean {
return result["Ok"] != null;
}
export interface API {
exchange: (selection: Array<boolean>) => Promise<APIState>;
pass: () => Promise<APIState>;
play: (tiles: Array<PlayedTile>, commit: boolean) => Promise<APIState>;
add_to_dictionary: (word: string) => Promise<void>;
load: () => Promise<APIState>;
}

View file

@ -1,4 +1,4 @@
import init from 'word_grid'; import init from '../../pkg/word_grid.js';
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
import * as React from "react"; import * as React from "react";
import {Menu} from "./Menu"; import {Menu} from "./Menu";

View file

@ -1,5 +1,14 @@
import {Letter as LetterData, Letter} from "../../pkg/word_grid";
import * as React from "react"; import * as React from "react";
import {CellType, Letter as LetterData} from "./api";
export enum CellType {
Normal = "Normal",
DoubleWord = "DoubleWord",
DoubleLetter = "DoubleLetter",
TripleLetter = "TripleLetter",
TripleWord = "TripleWord",
Start = "Start",
}
export interface Settings { export interface Settings {
@ -109,7 +118,7 @@ export function addNTimes<T>(array: T[], toAdd: T, times: number) {
} }
} }
export function mergeTrays(existing: PlayableLetterData[], newer: (LetterData | undefined)[]): PlayableLetterData[] { export function mergeTrays(existing: PlayableLetterData[], newer: (Letter | undefined)[]): PlayableLetterData[] {
let trayLength = Math.max(existing.length, newer.length); let trayLength = Math.max(existing.length, newer.length);

View file

@ -1,66 +0,0 @@
import {API, APIState, Difficulty, PlayedTile, Result, is_ok} from "./api";
import {WasmAPI} from 'word_grid';
export class GameWasm implements API{
wasm: WasmAPI;
constructor(seed: bigint, dictionary_text: string, difficulty: Difficulty) {
this.wasm = new WasmAPI(seed, dictionary_text, difficulty);
}
add_to_dictionary(word: string): Promise<void> {
return new Promise((resolve, _) => {
this.wasm.add_to_dictionary(word);
resolve()
});
}
exchange(selection: Array<boolean>): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.exchange(selection);
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
load(): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.load();
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
pass(): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.pass();
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
play(tiles: Array<PlayedTile>, commit: boolean): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.play(tiles, commit);
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
}

289
wasm/Cargo.lock generated
View file

@ -1,289 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm"
version = "0.1.0"
dependencies = [
"serde",
"serde-wasm-bindgen",
"serde_json",
"wasm-bindgen",
"word_grid",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "word_grid"
version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"rand",
"serde",
"serde_json",
]

View file

@ -1,14 +0,0 @@
[package]
name = "wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde-wasm-bindgen = "0.6.5"
wasm-bindgen = "0.2.92"
word_grid = { version = "0.1.0", path = "../wordgrid" }
serde_json = "1.0"
serde = { version = "1.0.202", features = ["derive"] }

View file

@ -1,60 +0,0 @@
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use word_grid::api::APIGame;
use word_grid::game::{Game, PlayedTile};
use word_grid::player_interaction::ai::Difficulty;
#[wasm_bindgen]
pub struct WasmAPI(APIGame);
#[wasm_bindgen]
impl WasmAPI {
#[wasm_bindgen(constructor)]
pub fn new(seed: u64, dictionary_text: &str, ai_difficulty: JsValue) -> Self {
let difficulty: Difficulty = serde_wasm_bindgen::from_value(ai_difficulty).unwrap();
let game = Game::new(
seed,
dictionary_text,
vec!["Player".to_string()],
vec![difficulty],
);
WasmAPI(APIGame::new(game))
}
pub fn exchange(&mut self, selection: JsValue) -> JsValue {
let selection: Vec<bool> = serde_wasm_bindgen::from_value(selection).unwrap();
let result = self.0.exchange("Player".to_string(), selection);
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn pass(&mut self) -> JsValue {
let result = self.0.pass("Player".to_string());
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn load(&mut self) -> JsValue {
let result = self.0.load("Player".to_string());
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> JsValue {
let tray_tile_locations: Vec<Option<PlayedTile>> =
serde_wasm_bindgen::from_value(tray_tile_locations).unwrap();
let result = self
.0
.play("Player".to_string(), tray_tile_locations, commit_move);
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn add_to_dictionary(&mut self, word: String) -> JsValue {
let result = self.0.add_to_dictionary(word);
serde_wasm_bindgen::to_value(&result).unwrap()
}
}

267
wordgrid/Cargo.lock generated
View file

@ -1,267 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "word_grid"
version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"rand",
"serde",
"serde_json",
]

View file

@ -1,217 +0,0 @@
use crate::board::{CellType, Letter};
use crate::game::{
Error, Game, GameState, PlayedTile, Player, PlayerState, TurnAction, TurnAdvanceResult,
};
use crate::player_interaction::Tray;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApiPlayer {
name: String,
tray_tiles: usize,
score: u32,
}
impl ApiPlayer {
fn from(player_state: &PlayerState) -> ApiPlayer {
ApiPlayer {
name: player_state.player.get_name().to_string(),
tray_tiles: player_state.tray.count(),
score: player_state.score,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Update {
r#type: TurnAction,
player: String,
}
pub type ApiBoard = Vec<Option<Letter>>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PublicInformation {
game_state: GameState,
board: ApiBoard,
cell_types: Vec<CellType>,
current_player: String,
players: Vec<ApiPlayer>,
remaining_tiles: usize,
history: Vec<Update>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApiState {
public_information: PublicInformation,
tray: Tray,
update: Option<Update>,
}
pub struct APIGame(pub Game, Vec<Update>);
impl APIGame {
pub fn new(game: Game) -> APIGame {
APIGame(game, vec![])
}
fn is_players_turn(&self, player: &str) -> bool {
self.0.current_player_name().eq(player)
}
fn player_exists(&self, player: &str) -> bool {
self.0.player_states.get_player_state(player).is_some()
}
fn player_exists_and_turn(&self, player: &str) -> Result<(), Error> {
if !self.player_exists(&player) {
Err(Error::InvalidPlayer(player.to_string()))
} else if !self.is_players_turn(&player) {
Err(Error::WrongTurn(player.to_string()))
} else {
Ok(())
}
}
fn build_result(
&self,
tray: Tray,
game_state: Option<GameState>,
update: Option<Update>,
) -> ApiState {
ApiState {
public_information: self.build_public_information(game_state, &self.1),
tray,
update,
}
}
fn build_public_information(
&self,
game_state: Option<GameState>,
history: &Vec<Update>,
) -> PublicInformation {
let game_state = game_state.unwrap_or_else(|| self.0.get_state());
let cell_types = self
.0
.get_board()
.cells
.iter()
.map(|cell| -> CellType { cell.cell_type })
.collect::<Vec<CellType>>();
let board: Vec<Option<Letter>> = self
.0
.get_board()
.cells
.iter()
.map(|cell| -> Option<Letter> { cell.value.clone() })
.collect();
let players = self
.0
.player_states
.0
.iter()
.map(|p| ApiPlayer::from(p))
.collect::<Vec<ApiPlayer>>();
PublicInformation {
game_state,
board,
cell_types,
current_player: self.0.current_player_name(),
players,
remaining_tiles: self.0.get_remaining_tiles(),
history: history.clone(),
}
}
pub fn exchange(&mut self, player: String, tray_tiles: Vec<bool>) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?;
let (tray, turn_action, game_state) = self.0.exchange_tiles(tray_tiles)?;
let update = Update {
r#type: turn_action,
player,
};
self.1.push(update.clone());
Ok(self.build_result(tray, Some(game_state), Some(update)))
}
pub fn pass(&mut self, player: String) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?;
let game_state = self.0.pass()?;
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
let update = Update {
r#type: TurnAction::Pass,
player,
};
self.1.push(update.clone());
Ok(self.build_result(tray, Some(game_state), Some(update)))
}
pub fn load(&mut self, player: String) -> Result<ApiState, Error> {
if !self.player_exists(&player) {
Err(Error::InvalidPlayer(player))
} else {
while self.is_ai_turn() {
let (result, _) = self.0.advance_turn()?;
if let TurnAdvanceResult::AIMove { name, action } = result {
self.1.push(Update {
r#type: action,
player: name,
});
} else {
unreachable!("We already checked that the current player is AI");
}
}
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
Ok(self.build_result(tray, None, None))
}
}
pub fn play(
&mut self,
player: String,
tray_tile_locations: Vec<Option<PlayedTile>>,
commit_move: bool,
) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?;
let (turn_action, game_state) = self.0.receive_play(tray_tile_locations, commit_move)?;
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
let update = Update {
r#type: turn_action,
player,
};
if commit_move {
self.1.push(update.clone())
}
Ok(self.build_result(tray, Some(game_state), Some(update)))
}
pub fn add_to_dictionary(&mut self, word: String) {
self.0.add_word(word);
}
pub fn is_ai_turn(&self) -> bool {
let current_player = self.0.current_player_name();
matches!(
self.0
.player_states
.get_player_state(&current_player)
.unwrap()
.player,
Player::AI { .. }
)
}
}

View file

@ -1,6 +0,0 @@
pub mod api;
pub mod board;
pub mod constants;
pub mod dictionary;
pub mod game;
pub mod player_interaction;