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
.idea/
pkg/
.nvmrc
pkg/

View file

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

View file

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

View file

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

View file

@ -1,90 +1,31 @@
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::rngs::SmallRng;
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 {
Human(String),
AI {
AI{
name: String,
difficulty: Difficulty,
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 {
pub fn get_name(&self) -> &str {
match &self {
Player::Human(name) => name,
Player::AI { name, .. } => name,
Player::Human(name) => {name}
Player::AI { name, .. } => {name}
}
}
}
@ -92,20 +33,18 @@ impl Player {
pub struct PlayerState {
pub player: Player,
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 index: usize,
pub character: Option<char>, // we only set this if PlayedTile is a blank
}
impl PlayedTile {
pub fn convert_tray(
tray_tile_locations: &Vec<Option<PlayedTile>>,
tray: &Tray,
) -> Vec<(Letter, Coordinates)> {
pub fn convert_tray(tray_tile_locations: &Vec<Option<PlayedTile>>, tray: &Tray) -> Vec<(Letter, Coordinates)> {
let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new();
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() {
@ -116,9 +55,7 @@ impl PlayedTile {
if letter.is_blank {
match played_tile.character {
None => {
panic!(
"You can't play a blank character without providing a letter value"
)
panic!("You can't play a blank character without providing a letter value")
}
Some(x) => {
// TODO - check that x is a valid alphabet letter
@ -127,6 +64,7 @@ impl PlayedTile {
}
}
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 {
word: String,
score: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct ScoreResult {
words: Vec<WordResult>,
total: u32,
@ -148,6 +88,7 @@ pub struct ScoreResult {
pub struct PlayerStates(pub Vec<PlayerState>);
impl PlayerStates {
fn get_player_name_by_turn_id(&self, id: usize) -> &str {
let id_mod = id % self.0.len();
let state = self.0.get(id_mod).unwrap();
@ -156,33 +97,37 @@ impl PlayerStates {
}
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
self.0
.iter()
self.0.iter()
.filter(|state| state.player.get_name().eq(name))
.nth(0)
}
pub fn get_player_state_mut(&mut self, name: &str) -> Option<&mut PlayerState> {
self.0
.iter_mut()
self.0.iter_mut()
.filter(|state| state.player.get_name().eq(name))
.nth(0)
}
pub fn get_tray(&self, name: &str) -> Option<&Tray> {
let player = self.get_player_state(name)?;
Some(&player.tray)
}
pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> {
let player = self.get_player_state_mut(name)?;
Some(&mut player.tray)
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Tsify, Debug, Clone)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")]
pub enum GameState {
InProgress,
@ -192,7 +137,7 @@ pub enum GameState {
},
}
pub struct Game {
pub struct Game{
pub tile_pool: Vec<Letter>,
rng: SmallRng,
board: Board,
@ -203,20 +148,15 @@ pub struct Game {
state: GameState,
}
impl Game {
pub fn new(
seed: u64,
dictionary_text: &str,
player_names: Vec<String>,
ai_difficulties: Vec<Difficulty>,
) -> Self {
pub fn new(seed: u64, dictionary_text: &str, player_names: Vec<String>, ai_difficulties: Vec<Difficulty>) -> Self {
let mut rng = SmallRng::seed_from_u64(seed);
let mut letters = standard_tile_pool(Some(&mut rng));
let dictionary = DictionaryImpl::create_from_str(dictionary_text);
let mut player_states: Vec<PlayerState> = player_names
.iter()
let mut player_states: Vec<PlayerState> = player_names.iter()
.map(|name| {
let mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters);
@ -234,7 +174,7 @@ impl Game {
let mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters);
let ai_player_name = if ai_length > 1 {
format!("AI {}", i + 1)
format!("AI {}", i+1)
} else {
"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) {
self.board = new_board;
}
fn fill_trays(&mut self) {
fn fill_trays(&mut self){
for state in self.player_states.0.iter_mut() {
let tray = &mut state.tray;
tray.fill(&mut self.tile_pool);
@ -283,18 +223,14 @@ impl Game {
&self.dictionary
}
fn verify_game_in_progress(&self) -> Result<(), Error> {
fn verify_game_in_progress(&self) -> Result<(), String> {
if !matches!(self.state, GameState::InProgress) {
return Err(Error::GameFinished);
return Err("Moves cannot be made after a game has finished".to_string());
}
Ok(())
}
pub fn receive_play(
&mut self,
tray_tile_locations: Vec<Option<PlayedTile>>,
commit_move: bool,
) -> Result<(TurnAction, GameState), Error> {
pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<(TurnAction, GameState), String> {
self.verify_game_in_progress()?;
let player = self.current_player_name();
@ -302,8 +238,7 @@ impl Game {
let mut board_instance = self.get_board().clone();
let mut tray = self.player_states.get_tray(&player).unwrap().clone();
let played_letters: Vec<(Letter, Coordinates)> =
PlayedTile::convert_tray(&tray_tile_locations, &tray);
let played_letters: Vec<(Letter, Coordinates)> = PlayedTile::convert_tray(&tray_tile_locations, &tray);
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() {
*tray.letters.get_mut(i).unwrap() = None;
@ -312,16 +247,19 @@ impl Game {
board_instance.receive_play(played_letters)?;
let x = board_instance.calculate_scores(self.get_dictionary())?;
let total_score = x.1;
let words: Vec<WordResult> =
x.0.iter()
.map(|(word, score)| WordResult {
let words: Vec<WordResult> = x.0.iter()
.map(|(word, score)| {
WordResult {
word: word.to_string(),
score: *score,
})
.collect();
score: *score
}
})
.collect();
if commit_move {
let player_state = self.player_states.get_player_state_mut(&player).unwrap();
@ -338,42 +276,28 @@ impl Game {
// game is over
self.end_game(Some(player));
}
}
let locations = tray_tile_locations
.iter()
.filter_map(|x| x.as_ref())
.map(|x| x.index)
.collect::<Vec<usize>>();
Ok((
TurnAction::PlayTiles {
result: ScoreResult {
words,
total: total_score,
},
locations,
Ok((TurnAction::PlayTiles {
result: ScoreResult {
words,
total: total_score,
},
self.get_state(),
))
}, self.state.clone()))
}
pub fn exchange_tiles(
&mut self,
tray_tile_locations: Vec<bool>,
) -> Result<(Tray, TurnAction, GameState), Error> {
pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<(Tray, TurnAction, GameState), String> {
self.verify_game_in_progress()?;
let player = self.current_player_name();
let tray = match self.player_states.get_tray_mut(&player) {
None => return Err(Error::InvalidPlayer(player)),
Some(x) => x,
None => {return Err(format!("Player {} not found", player))}
Some(x) => {x}
};
if tray.letters.len() != tray_tile_locations.len() {
return Err(Error::Other(
"Incoming tray and existing tray have different lengths".to_string(),
));
return Err("Incoming tray and existing tray have different lengths".to_string());
}
let tile_pool = &mut self.tile_pool;
@ -397,11 +321,8 @@ impl Game {
let state = self.increment_turn(false);
Ok((
tray,
TurnAction::ExchangeTiles { tiles_exchanged },
state.clone(),
))
Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }, state.clone()))
}
pub fn add_word(&mut self, word: String) {
@ -410,20 +331,21 @@ impl Game {
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()?;
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;
if !played {
self.turns_not_played += 1;
// 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);
}
} else {
self.turns_not_played = 0;
}
@ -432,6 +354,7 @@ impl Game {
}
fn end_game(&mut self, finisher: Option<String>) {
let mut finished_letters_map = HashMap::new();
let mut points_forfeit = 0;
@ -453,30 +376,25 @@ impl Game {
}
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;
}
self.state = GameState::Ended {
finisher,
remaining_tiles: finished_letters_map,
remaining_tiles: finished_letters_map
};
}
pub fn current_player_name(&self) -> String {
self.player_states
.get_player_name_by_turn_id(self.turn_order)
.to_string()
self.player_states.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 state = self
.player_states
.get_player_state_mut(&current_player)
.ok_or(Error::InvalidPlayer(current_player.clone()))?;
let state = self.player_states.get_player_state_mut(&current_player).ok_or("There should be a player available")?;
if let Player::AI { object, .. } = &mut state.player {
if let Player::AI {object, .. } = &mut state.player {
let tray = &mut state.tray;
let best_move = object.find_best_move(tray, &self.board, &mut self.rng);
@ -489,53 +407,43 @@ impl Game {
match tile_spot {
None => {
to_exchange.push(false);
}
},
Some(_) => {
to_exchange.push(true);
}
}
}
if self.tile_pool.is_empty() {
if self.tile_pool.is_empty(){
let game_state = self.increment_turn(false);
Ok((
TurnAdvanceResult::AIMove {
name: current_player,
action: TurnAction::Pass,
},
game_state.clone(),
))
Ok((TurnAdvanceResult::AIMove {
name: current_player,
action: TurnAction::Pass,
}, game_state.clone()))
} else {
let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
Ok((
TurnAdvanceResult::AIMove {
name: current_player,
action,
},
game_state,
))
Ok((TurnAdvanceResult::AIMove {
name: current_player,
action,
}, game_state))
}
}
Some(best_move) => {
let play = best_move.convert_to_play(tray);
let (action, game_state) = self.receive_play(play, true)?;
Ok((
TurnAdvanceResult::AIMove {
name: current_player,
action,
},
game_state,
))
Ok((TurnAdvanceResult::AIMove {
name: current_player,
action,
}, game_state))
}
}
} else {
Ok((
TurnAdvanceResult::HumanInputRequired {
name: self.current_player_name(),
},
self.get_state(),
))
Ok((TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()}, self.state.clone()))
}
}
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> {
let tray = match self.player_states.get_tray(&player) {
None => return Err(format!("Player {} not found", player)),
Some(x) => x,
None => {return Err(format!("Player {} not found", player))}
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")]
pub enum TurnAction {
Pass,
ExchangeTiles {
tiles_exchanged: usize,
ExchangeTiles{
tiles_exchanged: usize
},
PlayTiles {
result: ScoreResult,
locations: Vec<usize>,
PlayTiles{
result: ScoreResult
},
}
#[derive(Serialize, Deserialize, Debug)]
#[derive(Serialize, Deserialize, Tsify, Debug)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")]
pub enum TurnAdvanceResult {
HumanInputRequired { name: String },
AIMove { name: String, action: TurnAction },
HumanInputRequired{
name: String
},
AIMove{
name: String,
action: TurnAction,
}
}
#[cfg(test)]
mod tests {
use std::fs;
use crate::game::Game;
use crate::player_interaction::ai::Difficulty;
use std::fs;
#[test]
fn test_game() {
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 mut game = Game::new(
seed,
&dictionary_string,
vec!["Player".to_string()],
vec![Difficulty {
proportion: 0.5,
randomness: 0.0,
}],
);
let mut game = Game::new(seed, &dictionary_string, vec!["Player".to_string()], vec![Difficulty{proportion: 0.5, randomness: 0.0}]);
let current_player = game.current_player_name();
println!("Current player is {current_player}");
@ -615,5 +522,7 @@ mod tests {
assert_eq!(game.current_player_name(), "Player");
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 tsify::Tsify;
use crate::board::Letter;
pub mod ai;
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Debug, Serialize, Deserialize, Tsify, Clone)]
#[tsify(from_wasm_abi)]
pub struct Tray {
pub letters: Vec<Option<Letter>>,
pub letters: Vec<Option<Letter>>
}
impl Tray {
@ -14,7 +17,9 @@ impl Tray {
for _ in 0..tray_length {
letters.push(None);
}
Tray { letters }
Tray {
letters
}
}
pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) {
@ -32,20 +37,25 @@ impl Tray {
}
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)]
mod tests {
use super::*;
#[test]
fn test_tray() {
let mut letters = vec![
Letter::new(Some('E'), 3),
Letter::new(Some('O'), 2),
Letter::new(Some('J'), 1),
Letter::new(Some('J'), 1)
];
let mut tray = Tray::new(5);
@ -79,5 +89,8 @@ mod tests {
assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E');
assert!(tray.letters.get(3).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::constants::GRID_LENGTH;
use crate::dictionary::DictionaryImpl;
use crate::game::PlayedTile;
use crate::player_interaction::Tray;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
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',
];
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'];
struct CoordinateLineMapper {
direction: Direction,
@ -20,13 +18,18 @@ struct CoordinateLineMapper {
impl CoordinateLineMapper {
fn line_to_coord(&self, variable: u8) -> Coordinates {
match self.direction {
Direction::Row => Coordinates(variable, self.fixed),
Direction::Column => Coordinates(self.fixed, variable),
Direction::Row => {
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 proportion: f64,
pub randomness: f64,
@ -45,6 +48,7 @@ pub struct AI {
type MoveMap = HashMap<char, u32>;
type CrossTiles = Vec<Option<MoveMap>>;
#[derive(Debug, Eq, PartialEq, Hash)]
pub struct CompleteMove {
moves: Vec<MoveComponent>,
@ -52,11 +56,10 @@ pub struct CompleteMove {
}
impl CompleteMove {
pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> {
let mut played_tiles = Vec::with_capacity(tray.letters.len());
let mut moves = self
.moves
.iter()
let mut moves = self.moves.iter()
.map(|m| Some(m.clone()))
.collect::<Vec<Option<MoveComponent>>>();
@ -85,7 +88,7 @@ impl CompleteMove {
found_match = true;
break;
}
}
},
None => {}
}
}
@ -124,28 +127,29 @@ struct MoveComponent {
}
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 substrings = HashSet::new();
for (word, score) in dictionary.iter() {
if *score >= difficulty.proportion {
// TODO - may need to reverse
if *score >= difficulty.proportion { // TODO - may need to reverse
ai_dictionary.insert(word.clone());
substrings.insert(word.clone());
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());
}
}
}
}
let board_view = Board::new();
let mut column_cross_tiles =
CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
let mut column_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(|_| {
@ -164,20 +168,15 @@ impl AI {
}
}
pub fn find_best_move<R: Rng>(
&mut self,
tray: &Tray,
grid: &Board,
rng: &mut R,
) -> Option<CompleteMove> {
pub fn find_best_move<R: Rng>(&mut self, tray: &Tray, grid: &Board, rng: &mut R) -> Option<CompleteMove>{
let move_set = self.find_all_moves(tray, grid);
let mut best_move: Option<(CompleteMove, f64)> = None;
for possible_move in move_set {
let move_score = if self.difficulty.randomness > 0.0 {
(1.0 - self.difficulty.randomness) * (possible_move.score as f64)
+ self.difficulty.randomness * rng.gen_range(0.0..1.0)
let move_score =
if self.difficulty.randomness > 0.0 {
(1.0 - self.difficulty.randomness) * (possible_move.score as f64) + self.difficulty.randomness * rng.gen_range(0.0..1.0)
} else {
possible_move.score as f64
};
@ -195,9 +194,10 @@ impl AI {
}
return match best_move {
None => None,
Some((best_move, _)) => Some(best_move),
None => {None}
Some((best_move, _)) => {Some(best_move)}
};
}
fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> {
@ -215,17 +215,15 @@ impl AI {
&self,
tray: &Tray,
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
let cross_tiles = match direction {
Direction::Column => &self.row_cross_tiles,
Direction::Row => &self.column_cross_tiles,
Direction::Column => {&self.row_cross_tiles}
Direction::Row => {&self.column_cross_tiles}
};
let tray_letters = tray
.letters
.iter()
let tray_letters = tray.letters.iter()
.filter(|letter| letter.is_some())
.map(|letter| letter.unwrap())
.collect::<Vec<Letter>>();
@ -236,7 +234,7 @@ impl AI {
let coord_mapper = CoordinateLineMapper {
direction,
fixed: k,
fixed: k
};
for p in 0..GRID_LENGTH {
@ -248,12 +246,13 @@ impl AI {
for l in 0..GRID_LENGTH {
let coords = coord_mapper.line_to_coord(l);
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row)
|| check_if_anchored(&self.board_view, coords, Direction::Column);
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row) ||
check_if_anchored(&self.board_view, coords, Direction::Column);
if is_anchored && 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
if is_anchored &&
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(
&line_letters,
&line_cross_letters,
@ -263,11 +262,17 @@ impl AI {
&Vec::new(),
1,
&MoveScoring::new(),
all_moves,
);
all_moves
);
}
}
}
}
fn evaluate_spot_heading_left(
@ -282,8 +287,10 @@ impl AI {
min_length: usize,
current_points: &MoveScoring,
all_moves: &mut HashSet<CompleteMove>,
all_moves: &mut HashSet<CompleteMove>
) {
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
return;
}
@ -293,13 +300,10 @@ impl AI {
Some(_) => {
// there's a letter here; need to take a step left if we can
if !(line_index >= 1
&& line_letters
.get((line_index - 1) as usize)
.unwrap()
.is_some()
&& min_length == 1)
{
if !(line_index >= 1 &&
line_letters.get((line_index-1) as usize).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,
// just stop. Other versions of the for-loops that call this function will have picked up that case.
self.evaluate_spot_heading_left(
@ -311,8 +315,9 @@ impl AI {
current_play,
min_length,
current_points,
all_moves,
all_moves
);
}
}
None => {
@ -326,9 +331,10 @@ impl AI {
min_length,
current_points,
coord_mapper,
&available_letters,
letter.clone(),
all_moves,
all_moves
);
}
@ -342,10 +348,12 @@ impl AI {
current_play,
min_length + 1,
current_points,
all_moves,
all_moves
);
}
}
}
fn evaluate_letter_at_spot(
@ -358,10 +366,13 @@ impl AI {
current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>,
letter: Letter,
all_moves: &mut HashSet<CompleteMove>,
all_moves: &mut HashSet<CompleteMove>
) {
if letter.is_blank {
// need to loop through alphabet
for alpha in ALPHABET {
@ -378,7 +389,8 @@ impl AI {
coord_mapper,
available_letters,
letter,
all_moves,
all_moves
);
}
} else {
@ -392,9 +404,10 @@ impl AI {
coord_mapper,
available_letters,
letter.clone(),
all_moves,
all_moves
);
}
}
fn evaluate_non_blank_letter_at_spot(
@ -406,12 +419,15 @@ impl AI {
min_length: usize,
current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>,
letter: Letter,
all_moves: &mut HashSet<CompleteMove>,
) {
// let's now assign the letter to this spot
let mut line_letters = line_letters.clone();
*line_letters.get_mut(line_index as usize).unwrap() = Some(letter);
@ -442,11 +458,7 @@ impl AI {
// making copy
let mut current_points = current_points.clone();
let cell_type = self
.board_view
.get_cell(coord_mapper.line_to_coord(line_index as u8))
.unwrap()
.cell_type;
let cell_type = self.board_view.get_cell(coord_mapper.line_to_coord(line_index as u8)).unwrap().cell_type;
// first we score cross letters
if let Some(map) = cross_letters {
@ -496,8 +508,7 @@ impl AI {
current_points.main_scoring += letter.points * 2;
}
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
@ -505,8 +516,7 @@ impl AI {
if word.len() >= min_length && self.dictionary.contains(&word) {
let new_move = CompleteMove {
moves: current_play.clone(),
score: current_points.cross_scoring
+ current_points.main_scoring * current_points.multiplier,
score: current_points.cross_scoring + current_points.main_scoring*current_points.multiplier,
};
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 skipped_one = false;
available_letters.iter().for_each(|in_tray| {
if skipped_one || !in_tray.partial_match(&letter) {
new_available_letters.push(*in_tray);
} else {
skipped_one = true;
}
});
available_letters.iter()
.for_each(|in_tray| {
if skipped_one || !in_tray.partial_match(&letter) {
new_available_letters.push(*in_tray);
} else {
skipped_one = true;
}
});
assert!(skipped_one); // at least one should have been removed
self.evaluate_spot_heading_right(
&line_letters,
line_cross_letters,
line_index + 1,
line_index+1,
current_play,
min_length,
&current_points,
coord_mapper,
&new_available_letters,
all_moves,
all_moves
);
}
@ -547,11 +560,13 @@ impl AI {
min_length: usize,
current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>,
all_moves: &mut HashSet<CompleteMove>,
) {
// out-of-bounds check
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
return;
@ -571,11 +586,11 @@ impl AI {
&current_points,
coord_mapper,
available_letters,
all_moves,
all_moves
);
}
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 {
self.evaluate_letter_at_spot(
line_letters,
@ -586,8 +601,10 @@ impl AI {
current_points,
coord_mapper,
available_letters,
letter.clone(),
all_moves,
all_moves
)
}
}
@ -608,29 +625,25 @@ impl AI {
}
let mut check_for_valid_moves = |coords: Coordinates| {
let cell = new.get_cell(coords).unwrap();
if cell.value.is_none() {
let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords);
let valid_column_moves =
self.get_letters_that_make_words(new, Direction::Column, coords);
let valid_column_moves = self.get_letters_that_make_words(new, Direction::Column, coords);
let existing_row_moves =
self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
let existing_row_moves = self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
*existing_row_moves = valid_row_moves;
let existing_column_moves = self
.column_cross_tiles
.get_mut(coords.map_to_index())
.unwrap();
let existing_column_moves = self.column_cross_tiles.get_mut(coords.map_to_index()).unwrap();
*existing_column_moves = valid_column_moves;
}
};
updated_rows.iter().for_each(|&j| {
for i in 0..GRID_LENGTH {
let coords = Coordinates(i, j);
check_for_valid_moves(coords);
}
for i in 0..GRID_LENGTH {
let coords = Coordinates(i, j);
check_for_valid_moves(coords);
}
});
updated_columns.iter().for_each(|&i| {
@ -639,14 +652,10 @@ impl AI {
check_for_valid_moves(coords);
}
});
}
fn get_letters_that_make_words(
&self,
new: &Board,
direction: Direction,
coords: Coordinates,
) -> Option<MoveMap> {
fn get_letters_that_make_words(&self, new: &Board, direction: Direction, coords: Coordinates) -> Option<MoveMap> {
let is_anchored = check_if_anchored(new, coords, direction);
if !is_anchored {
return None;
@ -659,7 +668,7 @@ impl AI {
let cell = copy_grid.get_cell_mut(coords).unwrap();
cell.value = Some(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
is_blank: false,
});
@ -678,9 +687,11 @@ impl AI {
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
if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 {
return true;
@ -688,7 +699,7 @@ fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) ->
let has_letter = |alt_coord: Option<Coordinates>| -> bool {
match alt_coord {
None => false,
None => {false}
Some(alt_coord) => {
let cell = grid.get_cell(alt_coord).unwrap();
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 {
break;
}
if line_letters
.get((start_word - 1) as usize)
.unwrap()
.is_none()
{
if line_letters.get((start_word - 1) as usize).unwrap().is_none() {
break;
}
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();
line_letters[(start_word as usize)..((end_word + 1) as usize)]
.iter()
.for_each(|letter| {
str.push(letter.unwrap().text);
});
.iter().for_each(|letter| {
str.push(letter.unwrap().text);
});
str
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dictionary::Dictionary;
use rand::rngs::SmallRng;
use rand::SeedableRng;
use crate::dictionary::Dictionary;
use super::*;
fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) {
let cell = board.get_cell_mut(Coordinates(x, y)).unwrap();
@ -787,31 +793,31 @@ mod tests {
dictionary.insert("APPLE".to_string(), 0.9);
let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter {
tray.letters[0] = Some(Letter{
text: 'A',
points: 5,
ephemeral: false,
is_blank: false,
});
tray.letters[1] = Some(Letter {
tray.letters[1] = Some(Letter{
text: 'P',
points: 4,
ephemeral: false,
is_blank: false,
});
tray.letters[2] = Some(Letter {
tray.letters[2] = Some(Letter{
text: 'P',
points: 4,
ephemeral: false,
is_blank: false,
});
tray.letters[3] = Some(Letter {
tray.letters[3] = Some(Letter{
text: 'L',
points: 3,
ephemeral: false,
is_blank: false,
});
tray.letters[4] = Some(Letter {
tray.letters[4] = Some(Letter{
text: 'E',
points: 4,
ephemeral: false,
@ -826,12 +832,14 @@ mod tests {
assert_eq!(moves.len(), 10);
tray.letters[4] = Some(Letter {
text: ' ',
points: 0,
ephemeral: false,
is_blank: true,
});
tray.letters[4] = Some(
Letter {
text: ' ',
points: 0,
ephemeral: false,
is_blank: true,
}
);
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, 10, 'T', 1);
let difficulty = Difficulty {
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
randomness: 0.0,
@ -859,25 +869,25 @@ mod tests {
dictionary.insert("SLAM".to_string(), 0.9);
let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter {
tray.letters[0] = Some(Letter{
text: 'S',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[1] = Some(Letter {
tray.letters[1] = Some(Letter{
text: 'L',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[6] = Some(Letter {
tray.letters[6] = Some(Letter{
text: 'A',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[3] = Some(Letter {
tray.letters[3] = Some(Letter{
text: 'M',
points: 3,
ephemeral: false,
@ -888,10 +898,7 @@ mod tests {
ai.update_state(&board);
let end_of_boat = ai
.column_cross_tiles
.get(Coordinates(7, 11).map_to_index())
.unwrap();
let end_of_boat = ai.column_cross_tiles.get(Coordinates(7, 11).map_to_index()).unwrap();
assert!(end_of_boat.is_some());
assert_eq!(end_of_boat.as_ref().unwrap().len(), 1);
@ -899,6 +906,7 @@ mod tests {
println!("Moves are {:?}", moves);
// 3 possible moves -
// 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS'
// 2. Put 'S' at end of 'BOAT'
@ -912,6 +920,7 @@ mod tests {
let play = best_move.convert_to_play(&tray);
println!("Play is {:?}", play);
}
#[test]
@ -934,30 +943,25 @@ mod tests {
let above_cell_coords = Coordinates(7, 6);
let left_cell_cords = Coordinates(6, 7);
let row_cross_tiles = ai
.row_cross_tiles
.get(left_cell_cords.map_to_index())
.unwrap();
let column_cross_tiles = ai
.column_cross_tiles
.get(above_cell_coords.map_to_index())
.unwrap();
let row_cross_tiles = ai.row_cross_tiles.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!(column_cross_tiles.as_ref().unwrap().len(), 1);
let far_off_tiles = ai.row_cross_tiles.get(0).unwrap();
assert!(far_off_tiles.is_none());
}
#[test]
fn test_valid_moves() {
let mut board = Board::new();
set_cell(&mut board, 7 - 3, 7 + 3, 'Z', 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, 9 + 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, 7-3, 8+3, 'A', 1);
set_cell(&mut board, 7-3, 9+3, 'Z', 1);
let difficulty = Difficulty {
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);
let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter {
tray.letters[0] = Some(Letter{
text: 'A',
points: 1,
ephemeral: false,
@ -988,52 +992,50 @@ mod tests {
fn test_starting_move() {
let mut board = Board::new();
let difficulty = Difficulty {
proportion: 0.1,
randomness: 0.0,
};
let difficulty = Difficulty{proportion: 0.1, randomness: 0.0};
let mut dictionary = DictionaryImpl::new();
dictionary.insert("TWEETED".to_string(), 0.2);
let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter {
tray.letters[0] = Some(Letter{
text: 'I',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[1] = Some(Letter {
tray.letters[1] = Some(Letter{
text: 'R',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[2] = Some(Letter {
tray.letters[2] = Some(Letter{
text: 'W',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[3] = Some(Letter {
tray.letters[3] = Some(Letter{
text: 'I',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[4] = Some(Letter {
tray.letters[4] = Some(Letter{
text: 'T',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[5] = Some(Letter {
tray.letters[5] = Some(Letter{
text: 'E',
points: 1,
ephemeral: false,
is_blank: false,
});
tray.letters[6] = Some(Letter {
tray.letters[6] = Some(Letter{
text: 'D',
points: 1,
ephemeral: false,
@ -1046,5 +1048,7 @@ mod tests {
println!("Available moves are {:?}", all_moves);
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": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"word_grid": "file:../wasm/pkg"
"word_grid": "file:../pkg"
},
"devDependencies": {
"@parcel/transformer-less": "^2.9.3",
"@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7",
"parcel": "^2.12.0",
"parcel": "^2.9.3",
"process": "^0.11.10"
},
"scripts": {

View file

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

View file

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

View file

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

View file

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

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;