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, // we only set this if PlayedTile is a blank } impl PlayedTile { pub fn convert_tray(tray_tile_locations: &Vec>, 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, total: u32, } pub struct PlayerStates(pub Vec); 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, remaining_tiles: HashMap>, }, } pub struct Game{ pub tile_pool: Vec, 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, ai_difficulties: Vec) -> 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 = 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>, 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 = 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) -> 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 { 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) { 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(¤t_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 = 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 { 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); } }