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::Difficulty; use crate::player_interaction::Tray; pub enum Player { Human(String), AI{ name: String, difficulty: Difficulty, } } 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)] #[tsify(from_wasm_abi)] pub struct PlayedTile { index: usize, character: Option, } #[derive(Debug, Serialize, Deserialize, Tsify)] #[tsify(from_wasm_abi)] pub struct WordResult { word: String, score: u32, } pub struct PlayerStates(pub Vec); impl PlayerStates { 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) } } pub struct Game{ pub tile_pool: Vec, rng: SmallRng, board: Board, pub player_states: PlayerStates, dictionary: DictionaryImpl, } // Problem - I want to provide a UI to the player // Ideally they would get some kind of 'tray' object with methods to use and run // However I want the main game state to live in Rust, not in JS. // Does this mean I provide Rc>? // Other option - what if I just have one Game object that exposes all methods. // At no point do they get a Tray reference that auto-updates, they need to handle that // I just provide read-only JSON of everything, and they call the methods for updates // This will later work out well when I build it out as an API for multiplayer. impl Game { pub fn new(seed: u64, dictionary_text: &str, mut player_names: Vec) -> Self { let mut rng = SmallRng::seed_from_u64(seed); let mut letters = standard_tile_pool(Some(&mut rng)); player_names.shuffle(&mut rng); let 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(); Game { tile_pool: letters, rng, board: Board::new(), player_states: PlayerStates(player_states), dictionary: DictionaryImpl::create_from_str(dictionary_text) } } pub fn get_board(&self) -> &Board {&self.board} pub fn set_board(&mut self, new_board: Board) { self.board = new_board; } pub 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 } pub fn receive_play(&mut self, player: &str, tray_tile_locations: Vec>, commit_move: bool) -> Result, String> { let mut board_instance = self.get_board().clone(); let mut tray = self.player_states.get_tray(player).unwrap().clone(); 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(); *tray.letters.get_mut(i).unwrap() = None; 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 - I should check that the character is a valid letter letter.text = x; } } } played_letters.push((letter, coord)); } } board_instance.receive_play(played_letters)?; let x = board_instance.calculate_scores(self.get_dictionary())?; let words: Vec = x.0.iter() .map(|(word, score)| { WordResult { word: word.to_string(), score: *score } }) .collect(); if commit_move { let mut player_state = self.player_states.get_player_state_mut(player).unwrap(); player_state.score += x.1; player_state.tray = tray; board_instance.fix_tiles(); self.set_board(board_instance); self.fill_trays() } Ok(words) } pub fn exchange_tiles(&mut self, player: &str, tray_tile_locations: Vec) -> Result { 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; 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; } } } tile_pool.shuffle(&mut self.rng); tray.fill(&mut self.tile_pool); Ok(tray.clone()) } }