From 9b22f1301d7089db309e2e5c51fba6fc56fbbe75 Mon Sep 17 00:00:00 2001 From: Joel Therrien Date: Sun, 10 Sep 2023 15:17:36 -0700 Subject: [PATCH] WIP integration of AI into game logic --- src/game.rs | 155 ++++++++++++++++++++++++++++++----- src/player_interaction/ai.rs | 5 +- src/wasm.rs | 18 ++-- 3 files changed, 149 insertions(+), 29 deletions(-) diff --git a/src/game.rs b/src/game.rs index 66203e2..c3decd3 100644 --- a/src/game.rs +++ b/src/game.rs @@ -6,7 +6,7 @@ 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::ai::{AI, CompleteMove, Difficulty}; use crate::player_interaction::Tray; pub enum Player { @@ -14,6 +14,7 @@ pub enum Player { AI{ name: String, difficulty: Difficulty, + object: AI, } } @@ -56,6 +57,14 @@ pub struct ScoreResult { 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)) @@ -91,26 +100,18 @@ pub struct Game{ board: Board, pub player_states: PlayerStates, dictionary: DictionaryImpl, + turn_order: usize, } -// 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 { + 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)); - player_names.shuffle(&mut rng); - let player_states: Vec = player_names.iter() + 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); @@ -122,12 +123,31 @@ impl Game { }) .collect(); + 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); + player_states.push(PlayerState { + player: Player::AI { + name: format!("AI {}", i), + 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: DictionaryImpl::create_from_str(dictionary_text) + dictionary, + turn_order: 0, } } @@ -150,10 +170,12 @@ impl Game { &self.dictionary } - pub fn receive_play(&mut self, player: &str, tray_tile_locations: Vec>, commit_move: bool) -> Result { + pub fn receive_play(&mut self, tray_tile_locations: Vec>, commit_move: bool) -> Result { + + 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 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() { @@ -196,13 +218,15 @@ impl Game { .collect(); if commit_move { - let mut player_state = self.player_states.get_player_state_mut(player).unwrap(); + let mut 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.fill_trays(); + + self.increment_turn(); } Ok(ScoreResult { @@ -211,8 +235,9 @@ impl Game { }) } - pub fn exchange_tiles(&mut self, player: &str, tray_tile_locations: Vec) -> Result { - let tray = match self.player_states.get_tray_mut(player) { + pub fn exchange_tiles(&mut self, tray_tile_locations: Vec) -> Result { + 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} }; @@ -236,7 +261,11 @@ impl Game { tile_pool.shuffle(&mut self.rng); tray.fill(&mut self.tile_pool); - Ok(tray.clone()) + let tray = tray.clone(); + + self.increment_turn(); + + Ok(tray) } @@ -246,4 +275,86 @@ impl Game { self.dictionary.insert(word, -1.0); } + pub fn increment_turn(&mut self) { + self.turn_order += 1; + } + + 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) -> TurnAdvanceResult { + let current_player = self.current_player_name(); + let state = self.player_states.get_player_state_mut(¤t_player).unwrap(); + + 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); + match best_move { + None => { + // no available moves; exchange all tiles + let mut to_exchange = Vec::with_capacity(TRAY_LENGTH as usize); + let mut tiles_exchanged = 0; + for tile_spot in tray.letters.iter() { + match tile_spot { + None => { + to_exchange.push(false); + }, + Some(_) => { + to_exchange.push(true); + tiles_exchanged += 1; + } + } + } + + if self.tile_pool.is_empty(){ + TurnAdvanceResult::AIMove { + name: current_player, + action: TurnAction::Pass, + } + } else { + self.exchange_tiles(to_exchange).unwrap(); + TurnAdvanceResult::AIMove { + name: current_player, + action: TurnAction::ExchangeTiles(tiles_exchanged), + } + } + + + } + Some(best_move) => { + let play = best_move.convert_to_play(tray); + let score_result = self.receive_play(play, true).unwrap(); + TurnAdvanceResult::AIMove { + name: current_player, + action: TurnAction::PlayTiles(score_result), + } + } + } + } else { + TurnAdvanceResult::HumanInputRequired(self.current_player_name()) + } + + + } + +} + +#[derive(Deserialize, Tsify, Copy, Clone)] +#[tsify(from_wasm_abi)] +pub enum TurnAction { + Pass, + ExchangeTiles(usize), + PlayTiles(ScoreResult), +} + +#[derive(Deserialize, Tsify, Copy, Clone)] +#[tsify(from_wasm_abi)] +pub enum TurnAdvanceResult { + HumanInputRequired(String), + AIMove{ + name: String, + action: TurnAction, + } } \ No newline at end of file diff --git a/src/player_interaction/ai.rs b/src/player_interaction/ai.rs index 81262d2..74b8d20 100644 --- a/src/player_interaction/ai.rs +++ b/src/player_interaction/ai.rs @@ -1,5 +1,7 @@ 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; @@ -26,7 +28,8 @@ impl CoordinateLineMapper { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Serialize, Deserialize, Tsify)] +#[tsify(from_wasm_abi)] pub struct Difficulty { proportion: f64, randomness: f64, diff --git a/src/wasm.rs b/src/wasm.rs index cc42b0a..6ed9fad 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -5,6 +5,7 @@ use wasm_bindgen::JsValue; use wasm_bindgen::prelude::wasm_bindgen; use crate::board::{CellType, Letter}; use crate::game::{Game, PlayedTile}; +use crate::player_interaction::ai::Difficulty; #[wasm_bindgen] pub struct GameWasm(Game); @@ -29,8 +30,11 @@ pub struct MyResult { impl GameWasm { #[wasm_bindgen(constructor)] - pub fn new(seed: u64, dictionary_text: &str) -> GameWasm { - GameWasm(Game::new(seed, dictionary_text, vec!["Player".to_string(), "AI".to_string()])) + 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 { @@ -59,10 +63,10 @@ impl GameWasm { serde_wasm_bindgen::to_value(&letters) } - pub fn receive_play(&mut self, player: &str, tray_tile_locations: JsValue, commit_move: bool) -> Result { + pub fn receive_play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> Result { let tray_tile_locations: Vec> = serde_wasm_bindgen::from_value(tray_tile_locations)?; - let result = self.0.receive_play(player, tray_tile_locations, commit_move); + let result = self.0.receive_play(tray_tile_locations, commit_move); match result { Ok(x) => { @@ -104,11 +108,11 @@ impl GameWasm { } - pub fn exchange_tiles(&mut self, player: &str, tray_tile_locations: JsValue) -> Result{ + pub fn exchange_tiles(&mut self, tray_tile_locations: JsValue) -> Result{ let tray_tile_locations: Vec = serde_wasm_bindgen::from_value(tray_tile_locations)?; - match self.0.exchange_tiles(player, tray_tile_locations) { + match self.0.exchange_tiles(tray_tile_locations) { Ok(tray) => { serde_wasm_bindgen::to_value(&MyResult { response_type: ResponseType::OK, @@ -130,5 +134,7 @@ impl GameWasm { self.0.add_word(word); } + pub fn skip_turn(&mut self) {self.0.increment_turn()} + } \ No newline at end of file