diff --git a/src/board.rs b/src/board.rs index 2d12dd9..5228f9f 100644 --- a/src/board.rs +++ b/src/board.rs @@ -101,7 +101,7 @@ impl Letter { } pub fn partial_match(&self, other: &Letter) -> bool { - self == other || (self.is_blank == other.is_blank && self.points == other.points) + self == other || (self.is_blank && other.is_blank && self.points == other.points) } } diff --git a/src/game.rs b/src/game.rs index c3decd3..b023f8a 100644 --- a/src/game.rs +++ b/src/game.rs @@ -41,6 +41,35 @@ pub struct PlayedTile { 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 { @@ -177,27 +206,10 @@ impl Game { 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(); + 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() { - 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 - check that x is a valid alphabet letter - letter.text = x; - } - } - } - played_letters.push((letter, coord)); - } } @@ -283,14 +295,15 @@ impl Game { self.player_states.get_player_name_by_turn_id(self.turn_order).to_string() } - pub fn advance_turn(&mut self) -> TurnAdvanceResult { + pub fn advance_turn(&mut self) -> Result { let current_player = self.current_player_name(); - let state = self.player_states.get_player_state_mut(¤t_player).unwrap(); + 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 @@ -309,31 +322,32 @@ impl Game { } if self.tile_pool.is_empty(){ - TurnAdvanceResult::AIMove { + Ok(TurnAdvanceResult::AIMove { name: current_player, action: TurnAction::Pass, - } + }) } else { - self.exchange_tiles(to_exchange).unwrap(); - TurnAdvanceResult::AIMove { + self.exchange_tiles(to_exchange)?; + Ok(TurnAdvanceResult::AIMove { name: current_player, - action: TurnAction::ExchangeTiles(tiles_exchanged), - } + 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 { + let score_result = self.receive_play(play, true)?; + Ok(TurnAdvanceResult::AIMove { name: current_player, - action: TurnAction::PlayTiles(score_result), - } + action: TurnAction::PlayTiles{result: score_result}, + }) } } } else { - TurnAdvanceResult::HumanInputRequired(self.current_player_name()) + Ok(TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()}) + } @@ -341,20 +355,58 @@ impl Game { } -#[derive(Deserialize, Tsify, Copy, Clone)] +#[derive(Serialize, Deserialize, Tsify, Debug)] #[tsify(from_wasm_abi)] +#[serde(tag = "type")] pub enum TurnAction { Pass, - ExchangeTiles(usize), - PlayTiles(ScoreResult), + ExchangeTiles{ + tiles_exchanged: usize + }, + PlayTiles{ + result: ScoreResult + }, } -#[derive(Deserialize, Tsify, Copy, Clone)] +#[derive(Serialize, Deserialize, Tsify, Debug)] #[tsify(from_wasm_abi)] +#[serde(tag = "type")] pub enum TurnAdvanceResult { - HumanInputRequired(String), + 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"); + + game.increment_turn(); + assert_eq!(game.current_player_name(), "AI 0"); + + let result = game.advance_turn(); + println!("AI move is {result:?}"); + + } + } \ No newline at end of file diff --git a/src/player_interaction/ai.rs b/src/player_interaction/ai.rs index 74b8d20..78f6de2 100644 --- a/src/player_interaction/ai.rs +++ b/src/player_interaction/ai.rs @@ -31,8 +31,8 @@ impl CoordinateLineMapper { #[derive(Copy, Clone, Serialize, Deserialize, Tsify)] #[tsify(from_wasm_abi)] pub struct Difficulty { - proportion: f64, - randomness: f64, + pub proportion: f64, + pub randomness: f64, } pub struct AI { @@ -92,7 +92,9 @@ impl CompleteMove { None => {} } } - assert!(found_match); + if !found_match { + played_tiles.push(None); + } } } }); @@ -174,7 +176,7 @@ impl AI { 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) + (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 }; @@ -512,10 +514,11 @@ impl AI { // finally, while we know that we're in a valid substring we should check if this is a valid word or not // so that we can possible submit our move if word.len() >= min_length && self.dictionary.contains(&word) { - all_moves.insert(CompleteMove { + let new_move = CompleteMove { moves: current_play.clone(), score: current_points.cross_scoring + current_points.main_scoring*current_points.multiplier, - }); + }; + all_moves.insert(new_move); } // remove the first instance of letter from available_letters @@ -746,6 +749,7 @@ fn find_word_on_line(line_letters: &Vec>, line_index: i8) -> Stri mod tests { 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) { @@ -984,6 +988,67 @@ mod tests { assert!(moves.is_empty()); } + #[test] + fn test_starting_move() { + let mut board = Board::new(); + 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{ + text: 'I', + points: 1, + ephemeral: false, + is_blank: false, + }); + tray.letters[1] = Some(Letter{ + text: 'R', + points: 1, + ephemeral: false, + is_blank: false, + }); + tray.letters[2] = Some(Letter{ + text: 'W', + points: 1, + ephemeral: false, + is_blank: false, + }); + tray.letters[3] = Some(Letter{ + text: 'I', + points: 1, + ephemeral: false, + is_blank: false, + }); + tray.letters[4] = Some(Letter{ + text: 'T', + points: 1, + ephemeral: false, + is_blank: false, + }); + tray.letters[5] = Some(Letter{ + text: 'E', + points: 1, + ephemeral: false, + is_blank: false, + }); + tray.letters[6] = Some(Letter{ + text: 'D', + points: 1, + ephemeral: false, + is_blank: false, + }); + + let mut ai = AI::new(difficulty, &dictionary); + + let all_moves = ai.find_all_moves(&tray, &board); + println!("Available moves are {:?}", all_moves); + + assert!(all_moves.is_empty()); + + } } \ No newline at end of file diff --git a/src/wasm.rs b/src/wasm.rs index 6ed9fad..1b128e6 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -136,5 +136,19 @@ impl GameWasm { pub fn skip_turn(&mut self) {self.0.increment_turn()} + pub fn advance_turn(&mut self) -> Result { + let result = self.0.advance_turn(); + + match result { + Ok(x) => { + Ok(serde_wasm_bindgen::to_value(&x)?) + }, + Err(e) => { + Ok(serde_wasm_bindgen::to_value(&e)?) + } + } + + + } } \ No newline at end of file diff --git a/ui/src/Game.tsx b/ui/src/Game.tsx index 61d676a..f12d74c 100644 --- a/ui/src/Game.tsx +++ b/ui/src/Game.tsx @@ -6,7 +6,7 @@ import { PlayedTile, PlayerAndScore, ScoreResult, - Tray + Tray, TurnAdvanceResult } from "word_grid"; import { LocationType, @@ -124,7 +124,7 @@ export function Game(props: { } }) - const result: MyResult = props.wasm.exchange_tiles("Player", selectedArray); + const result: MyResult = props.wasm.exchange_tiles(selectedArray); console.log({result}); if(result.response_type === "ERR") { @@ -181,7 +181,7 @@ export function Game(props: { return null; }); - const result: MyResult = props.wasm.receive_play("Player", playedTiles, confirmedScorePoints > -1); + const result: MyResult = props.wasm.receive_play(playedTiles, confirmedScorePoints > -1); console.log({result}); if(result.response_type === "ERR") { @@ -223,6 +223,16 @@ export function Game(props: { + + ; } diff --git a/ui/src/Menu.tsx b/ui/src/Menu.tsx index 344333e..becb895 100644 --- a/ui/src/Menu.tsx +++ b/ui/src/Menu.tsx @@ -1,6 +1,6 @@ import * as React from "react"; import {useState} from "react"; -import {GameWasm} from 'word_grid'; +import {Difficulty, GameWasm} from 'word_grid'; import {Settings} from "./utils"; import {Game} from "./Game"; @@ -15,6 +15,7 @@ export function Menu(props: {settings: Settings, dictionary_text: string}) { // aiRandomness = log(1 + x*(n-1))/log(n) when x, the user input, ranges between 0 and 1 const logBase: number = 10000; const processedAIRandomness = Math.log(1 + (logBase - 1)*aiRandomness/100) / Math.log(logBase); + const processedProportionDictionary = 1.0 - proportionDictionary / 100; if (game == null) { @@ -64,7 +65,11 @@ export function Menu(props: {settings: Settings, dictionary_text: string}) {