WordGrid/src/game.rs
2023-09-20 18:59:32 -07:00

528 lines
No EOL
15 KiB
Rust

use std::collections::HashMap;
use rand::prelude::SliceRandom;
use rand::rngs::SmallRng;
use rand::SeedableRng;
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{
name: String,
difficulty: Difficulty,
object: AI,
}
}
impl Player {
pub fn get_name(&self) -> &str {
match &self {
Player::Human(name) => {name}
Player::AI { name, .. } => {name}
}
}
}
pub struct PlayerState {
pub player: Player,
pub score: u32,
pub tray: Tray
}
#[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)> {
let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new();
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() {
let played_tile = played_tile.unwrap();
let mut letter: Letter = tray.letters.get(i).unwrap().unwrap();
let coord = Coordinates::new_from_index(played_tile.index);
if letter.is_blank {
match played_tile.character {
None => {
panic!("You can't play a blank character without providing a letter value")
}
Some(x) => {
// TODO - check that x is a valid alphabet letter
letter.text = x;
}
}
}
played_letters.push((letter, coord));
}
}
played_letters
}
}
#[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct WordResult {
word: String,
score: u32,
}
#[derive(Debug, Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct ScoreResult {
words: Vec<WordResult>,
total: u32,
}
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();
state.player.get_name()
}
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
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()
.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, Tsify, Debug, Clone)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")]
pub enum GameState {
InProgress,
Ended {
finisher: Option<String>,
remaining_tiles: HashMap<String, Vec<Letter>>,
},
}
pub struct Game{
pub tile_pool: Vec<Letter>,
rng: SmallRng,
board: Board,
pub player_states: PlayerStates,
dictionary: DictionaryImpl,
turn_order: usize,
turns_not_played: usize,
state: GameState,
}
impl Game {
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()
.map(|name| {
let mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters);
PlayerState {
player: Player::Human(name.clone()),
score: 0,
tray,
}
})
.collect();
let ai_length = ai_difficulties.len();
for (i, ai_difficulty) in ai_difficulties.into_iter().enumerate() {
let ai = AI::new(ai_difficulty, &dictionary);
let mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters);
let ai_player_name = if ai_length > 1 {
format!("AI {}", i+1)
} else {
"AI".to_string()
};
player_states.push(PlayerState {
player: Player::AI {
name: ai_player_name,
difficulty: ai_difficulty,
object: ai,
},
score: 0,
tray,
});
}
// let's shuffle player_states so that humans don't always go first
player_states.shuffle(&mut rng);
Game {
tile_pool: letters,
rng,
board: Board::new(),
player_states: PlayerStates(player_states),
dictionary,
turn_order: 0,
turns_not_played: 0,
state: GameState::InProgress,
}
}
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){
for state in self.player_states.0.iter_mut() {
let tray = &mut state.tray;
tray.fill(&mut self.tile_pool);
}
}
pub fn get_dictionary(&self) -> &DictionaryImpl {
&self.dictionary
}
fn verify_game_in_progress(&self) -> Result<(), String> {
if !matches!(self.state, GameState::InProgress) {
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), String> {
self.verify_game_in_progress()?;
let player = self.current_player_name();
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);
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() {
*tray.letters.get_mut(i).unwrap() = None;
}
}
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 {
word: word.to_string(),
score: *score
}
})
.collect();
if commit_move {
let player_state = self.player_states.get_player_state_mut(&player).unwrap();
player_state.score += total_score;
player_state.tray = tray;
board_instance.fix_tiles();
self.set_board(board_instance);
self.fill_trays();
self.increment_turn(true);
if self.get_player_tile_count(&player).unwrap() == 0 {
// game is over
self.end_game(Some(player));
}
}
Ok((TurnAction::PlayTiles {
result: ScoreResult {
words,
total: total_score,
},
}, self.state.clone()))
}
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(format!("Player {} not found", player))}
Some(x) => {x}
};
if tray.letters.len() != tray_tile_locations.len() {
return Err("Incoming tray and existing tray have different lengths".to_string());
}
let tile_pool = &mut self.tile_pool;
let mut tiles_exchanged = 0;
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if *played_tile {
let letter = tray.letters.get_mut(i).unwrap();
if letter.is_some() {
tile_pool.push(letter.unwrap().clone());
*letter = None;
tiles_exchanged += 1;
}
}
}
tile_pool.shuffle(&mut self.rng);
tray.fill(&mut self.tile_pool);
let tray = tray.clone();
let state = self.increment_turn(false);
Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }, state.clone()))
}
pub fn add_word(&mut self, word: String) {
let word = word.to_uppercase();
self.dictionary.insert(word, -1.0);
}
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{
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() {
self.end_game(None);
}
} else {
self.turns_not_played = 0;
}
&self.state
}
fn end_game(&mut self, finisher: Option<String>) {
let mut finished_letters_map = HashMap::new();
let mut points_forfeit = 0;
for player in self.player_states.0.iter_mut() {
let tray = &mut player.tray;
let mut letters_remaining = Vec::new();
let mut player_points_lost = 0;
for letter in tray.letters.iter_mut() {
if let Some(letter) = letter {
player_points_lost += letter.points;
letters_remaining.push(letter.clone());
}
*letter = None;
}
points_forfeit += player_points_lost;
player.score -= player_points_lost;
finished_letters_map.insert(player.player.get_name().to_string(), letters_remaining);
}
if let Some(finisher) = &finisher {
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
};
}
pub fn current_player_name(&self) -> String {
self.player_states.get_player_name_by_turn_id(self.turn_order).to_string()
}
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("There should be a player available")?;
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);
//let best_move: Option<CompleteMove> = None;
match best_move {
None => {
// no available moves; exchange all tiles
let mut to_exchange = Vec::with_capacity(TRAY_LENGTH as usize);
for tile_spot in tray.letters.iter() {
match tile_spot {
None => {
to_exchange.push(false);
},
Some(_) => {
to_exchange.push(true);
}
}
}
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()))
} else {
let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
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))
}
}
} else {
Ok((TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()}, self.state.clone()))
}
}
pub fn get_remaining_tiles(&self) -> usize {
self.tile_pool.len()
}
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}
};
Ok(
tray.count()
)
}
}
#[derive(Serialize, Deserialize, Tsify, Debug)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")]
pub enum TurnAction {
Pass,
ExchangeTiles{
tiles_exchanged: usize
},
PlayTiles{
result: ScoreResult
},
}
#[derive(Serialize, Deserialize, Tsify, Debug)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")]
pub enum TurnAdvanceResult {
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;
#[test]
fn test_game() {
let seed = 124;
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 current_player = game.current_player_name();
println!("Current player is {current_player}");
assert_eq!(current_player, "Player");
assert_eq!(0, game.turns_not_played);
game.increment_turn(false);
assert_eq!(1, game.turns_not_played);
assert_eq!(game.current_player_name(), "AI");
let result = game.advance_turn();
println!("AI move is {result:?}");
assert_eq!(game.current_player_name(), "Player");
assert_eq!(0, game.turns_not_played);
}
}