WIP integration of AI into game logic
This commit is contained in:
parent
60fabb0214
commit
9b22f1301d
3 changed files with 149 additions and 29 deletions
155
src/game.rs
155
src/game.rs
|
@ -6,7 +6,7 @@ use tsify::Tsify;
|
||||||
use crate::board::{Board, Coordinates, Letter};
|
use crate::board::{Board, Coordinates, Letter};
|
||||||
use crate::constants::{standard_tile_pool, TRAY_LENGTH};
|
use crate::constants::{standard_tile_pool, TRAY_LENGTH};
|
||||||
use crate::dictionary::{Dictionary, DictionaryImpl};
|
use crate::dictionary::{Dictionary, DictionaryImpl};
|
||||||
use crate::player_interaction::ai::Difficulty;
|
use crate::player_interaction::ai::{AI, CompleteMove, Difficulty};
|
||||||
use crate::player_interaction::Tray;
|
use crate::player_interaction::Tray;
|
||||||
|
|
||||||
pub enum Player {
|
pub enum Player {
|
||||||
|
@ -14,6 +14,7 @@ pub enum Player {
|
||||||
AI{
|
AI{
|
||||||
name: String,
|
name: String,
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
|
object: AI,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +57,14 @@ pub struct ScoreResult {
|
||||||
|
|
||||||
pub struct PlayerStates(pub Vec<PlayerState>);
|
pub struct PlayerStates(pub Vec<PlayerState>);
|
||||||
impl PlayerStates {
|
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> {
|
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
|
||||||
self.0.iter()
|
self.0.iter()
|
||||||
.filter(|state| state.player.get_name().eq(name))
|
.filter(|state| state.player.get_name().eq(name))
|
||||||
|
@ -91,26 +100,18 @@ pub struct Game{
|
||||||
board: Board,
|
board: Board,
|
||||||
pub player_states: PlayerStates,
|
pub player_states: PlayerStates,
|
||||||
dictionary: DictionaryImpl,
|
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<RefCell<Tray>>?
|
|
||||||
|
|
||||||
// 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 {
|
impl Game {
|
||||||
pub fn new(seed: u64, dictionary_text: &str, mut player_names: Vec<String>) -> Self {
|
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 rng = SmallRng::seed_from_u64(seed);
|
||||||
|
|
||||||
let mut letters = standard_tile_pool(Some(&mut rng));
|
let mut letters = standard_tile_pool(Some(&mut rng));
|
||||||
|
|
||||||
player_names.shuffle(&mut rng);
|
let dictionary = DictionaryImpl::create_from_str(dictionary_text);
|
||||||
let player_states: Vec<PlayerState> = player_names.iter()
|
|
||||||
|
let mut player_states: Vec<PlayerState> = player_names.iter()
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
let mut tray = Tray::new(TRAY_LENGTH);
|
let mut tray = Tray::new(TRAY_LENGTH);
|
||||||
tray.fill(&mut letters);
|
tray.fill(&mut letters);
|
||||||
|
@ -122,12 +123,31 @@ impl Game {
|
||||||
})
|
})
|
||||||
.collect();
|
.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 {
|
Game {
|
||||||
tile_pool: letters,
|
tile_pool: letters,
|
||||||
rng,
|
rng,
|
||||||
board: Board::new(),
|
board: Board::new(),
|
||||||
player_states: PlayerStates(player_states),
|
player_states: PlayerStates(player_states),
|
||||||
dictionary: DictionaryImpl::create_from_str(dictionary_text)
|
dictionary,
|
||||||
|
turn_order: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,10 +170,12 @@ impl Game {
|
||||||
&self.dictionary
|
&self.dictionary
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive_play(&mut self, player: &str, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<ScoreResult, String> {
|
pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<ScoreResult, String> {
|
||||||
|
|
||||||
|
let player = self.current_player_name();
|
||||||
|
|
||||||
let mut board_instance = self.get_board().clone();
|
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();
|
let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new();
|
||||||
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
|
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
|
||||||
|
@ -196,13 +218,15 @@ impl Game {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if commit_move {
|
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.score += total_score;
|
||||||
player_state.tray = tray;
|
player_state.tray = tray;
|
||||||
|
|
||||||
board_instance.fix_tiles();
|
board_instance.fix_tiles();
|
||||||
self.set_board(board_instance);
|
self.set_board(board_instance);
|
||||||
self.fill_trays()
|
self.fill_trays();
|
||||||
|
|
||||||
|
self.increment_turn();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ScoreResult {
|
Ok(ScoreResult {
|
||||||
|
@ -211,8 +235,9 @@ impl Game {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exchange_tiles(&mut self, player: &str, tray_tile_locations: Vec<bool>) -> Result<Tray, String> {
|
pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<Tray, String> {
|
||||||
let tray = match self.player_states.get_tray_mut(player) {
|
let player = self.current_player_name();
|
||||||
|
let tray = match self.player_states.get_tray_mut(&player) {
|
||||||
None => {return Err(format!("Player {} not found", player))}
|
None => {return Err(format!("Player {} not found", player))}
|
||||||
Some(x) => {x}
|
Some(x) => {x}
|
||||||
};
|
};
|
||||||
|
@ -236,7 +261,11 @@ impl Game {
|
||||||
tile_pool.shuffle(&mut self.rng);
|
tile_pool.shuffle(&mut self.rng);
|
||||||
tray.fill(&mut self.tile_pool);
|
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);
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tsify::Tsify;
|
||||||
use crate::board::{Board, CellType, Coordinates, Direction, Letter};
|
use crate::board::{Board, CellType, Coordinates, Direction, Letter};
|
||||||
use crate::constants::GRID_LENGTH;
|
use crate::constants::GRID_LENGTH;
|
||||||
use crate::dictionary::DictionaryImpl;
|
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 {
|
pub struct Difficulty {
|
||||||
proportion: f64,
|
proportion: f64,
|
||||||
randomness: f64,
|
randomness: f64,
|
||||||
|
|
18
src/wasm.rs
18
src/wasm.rs
|
@ -5,6 +5,7 @@ use wasm_bindgen::JsValue;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
use crate::board::{CellType, Letter};
|
use crate::board::{CellType, Letter};
|
||||||
use crate::game::{Game, PlayedTile};
|
use crate::game::{Game, PlayedTile};
|
||||||
|
use crate::player_interaction::ai::Difficulty;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct GameWasm(Game);
|
pub struct GameWasm(Game);
|
||||||
|
@ -29,8 +30,11 @@ pub struct MyResult<E: Serialize> {
|
||||||
impl GameWasm {
|
impl GameWasm {
|
||||||
|
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub fn new(seed: u64, dictionary_text: &str) -> GameWasm {
|
pub fn new(seed: u64, dictionary_text: &str, ai_difficulty: JsValue) -> GameWasm {
|
||||||
GameWasm(Game::new(seed, dictionary_text, vec!["Player".to_string(), "AI".to_string()]))
|
|
||||||
|
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<JsValue, Error> {
|
pub fn get_tray(&self, name: &str) -> Result<JsValue, Error> {
|
||||||
|
@ -59,10 +63,10 @@ impl GameWasm {
|
||||||
serde_wasm_bindgen::to_value(&letters)
|
serde_wasm_bindgen::to_value(&letters)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive_play(&mut self, player: &str, tray_tile_locations: JsValue, commit_move: bool) -> Result<JsValue, Error> {
|
pub fn receive_play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> Result<JsValue, Error> {
|
||||||
let tray_tile_locations: Vec<Option<PlayedTile>> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
|
let tray_tile_locations: Vec<Option<PlayedTile>> = 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 {
|
match result {
|
||||||
Ok(x) => {
|
Ok(x) => {
|
||||||
|
@ -104,11 +108,11 @@ impl GameWasm {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exchange_tiles(&mut self, player: &str, tray_tile_locations: JsValue) -> Result<JsValue, Error>{
|
pub fn exchange_tiles(&mut self, tray_tile_locations: JsValue) -> Result<JsValue, Error>{
|
||||||
|
|
||||||
let tray_tile_locations: Vec<bool> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
|
let tray_tile_locations: Vec<bool> = 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) => {
|
Ok(tray) => {
|
||||||
serde_wasm_bindgen::to_value(&MyResult {
|
serde_wasm_bindgen::to_value(&MyResult {
|
||||||
response_type: ResponseType::OK,
|
response_type: ResponseType::OK,
|
||||||
|
@ -130,5 +134,7 @@ impl GameWasm {
|
||||||
self.0.add_word(word);
|
self.0.add_word(word);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn skip_turn(&mut self) {self.0.increment_turn()}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue