2023-08-16 02:35:23 +00:00
|
|
|
use rand::prelude::SliceRandom;
|
2023-08-06 01:59:30 +00:00
|
|
|
use rand::rngs::SmallRng;
|
|
|
|
use rand::SeedableRng;
|
2023-08-19 01:14:27 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use tsify::Tsify;
|
|
|
|
use crate::board::{Board, Coordinates, Letter};
|
2023-08-06 01:59:30 +00:00
|
|
|
use crate::constants::{standard_tile_pool, TRAY_LENGTH};
|
2023-08-13 20:08:37 +00:00
|
|
|
use crate::dictionary::{Dictionary, DictionaryImpl};
|
2023-09-10 22:17:36 +00:00
|
|
|
use crate::player_interaction::ai::{AI, CompleteMove, Difficulty};
|
2023-08-06 01:59:30 +00:00
|
|
|
use crate::player_interaction::Tray;
|
|
|
|
|
|
|
|
pub enum Player {
|
|
|
|
Human(String),
|
|
|
|
AI{
|
|
|
|
name: String,
|
|
|
|
difficulty: Difficulty,
|
2023-09-10 22:17:36 +00:00
|
|
|
object: AI,
|
2023-08-06 01:59:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-16 02:35:23 +00:00
|
|
|
impl Player {
|
|
|
|
|
|
|
|
pub fn get_name(&self) -> &str {
|
|
|
|
match &self {
|
|
|
|
Player::Human(name) => {name}
|
|
|
|
Player::AI { name, .. } => {name}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-06 01:59:30 +00:00
|
|
|
pub struct PlayerState {
|
2023-08-16 02:35:23 +00:00
|
|
|
pub player: Player,
|
|
|
|
pub score: u32,
|
2023-08-17 02:37:32 +00:00
|
|
|
pub tray: Tray
|
2023-08-06 01:59:30 +00:00
|
|
|
}
|
|
|
|
|
2023-09-08 02:44:07 +00:00
|
|
|
#[derive(Deserialize, Tsify, Copy, Clone, Debug)]
|
2023-08-19 01:14:27 +00:00
|
|
|
#[tsify(from_wasm_abi)]
|
|
|
|
pub struct PlayedTile {
|
2023-09-08 02:44:07 +00:00
|
|
|
pub index: usize,
|
2023-09-09 00:50:54 +00:00
|
|
|
pub character: Option<char>, // we only set this if PlayedTile is a blank
|
2023-08-19 01:14:27 +00:00
|
|
|
}
|
|
|
|
|
2023-09-13 05:13:36 +00:00
|
|
|
impl PlayedTile {
|
|
|
|
pub fn convert_tray(tray_tile_locations: &Vec<Option<PlayedTile>>, 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-19 01:14:27 +00:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
|
|
|
#[tsify(from_wasm_abi)]
|
|
|
|
pub struct WordResult {
|
|
|
|
word: String,
|
|
|
|
score: u32,
|
|
|
|
}
|
|
|
|
|
2023-08-25 03:36:41 +00:00
|
|
|
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
|
|
|
#[tsify(from_wasm_abi)]
|
|
|
|
pub struct ScoreResult {
|
|
|
|
words: Vec<WordResult>,
|
|
|
|
total: u32,
|
|
|
|
}
|
|
|
|
|
2023-08-23 03:52:03 +00:00
|
|
|
pub struct PlayerStates(pub Vec<PlayerState>);
|
|
|
|
impl PlayerStates {
|
2023-09-10 22:17:36 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2023-08-23 03:52:03 +00:00
|
|
|
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)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-13 20:08:37 +00:00
|
|
|
pub struct Game{
|
2023-08-17 02:37:32 +00:00
|
|
|
pub tile_pool: Vec<Letter>,
|
2023-08-23 03:52:03 +00:00
|
|
|
rng: SmallRng,
|
2023-08-10 03:00:14 +00:00
|
|
|
board: Board,
|
2023-08-23 03:52:03 +00:00
|
|
|
pub player_states: PlayerStates,
|
2023-08-13 20:08:37 +00:00
|
|
|
dictionary: DictionaryImpl,
|
2023-09-10 22:17:36 +00:00
|
|
|
turn_order: usize,
|
2023-08-06 01:59:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Game {
|
2023-09-10 22:17:36 +00:00
|
|
|
pub fn new(seed: u64, dictionary_text: &str, player_names: Vec<String>, ai_difficulties: Vec<Difficulty>) -> Self {
|
2023-08-06 01:59:30 +00:00
|
|
|
let mut rng = SmallRng::seed_from_u64(seed);
|
|
|
|
let mut letters = standard_tile_pool(Some(&mut rng));
|
|
|
|
|
2023-09-10 22:17:36 +00:00
|
|
|
let dictionary = DictionaryImpl::create_from_str(dictionary_text);
|
|
|
|
|
|
|
|
let mut player_states: Vec<PlayerState> = player_names.iter()
|
2023-08-16 02:35:23 +00:00
|
|
|
.map(|name| {
|
|
|
|
let mut tray = Tray::new(TRAY_LENGTH);
|
|
|
|
tray.fill(&mut letters);
|
|
|
|
PlayerState {
|
|
|
|
player: Player::Human(name.clone()),
|
|
|
|
score: 0,
|
|
|
|
tray,
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect();
|
2023-08-06 01:59:30 +00:00
|
|
|
|
2023-09-10 22:17:36 +00:00
|
|
|
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);
|
|
|
|
|
2023-08-06 01:59:30 +00:00
|
|
|
Game {
|
2023-08-10 03:00:14 +00:00
|
|
|
tile_pool: letters,
|
2023-08-23 03:52:03 +00:00
|
|
|
rng,
|
2023-08-10 03:00:14 +00:00
|
|
|
board: Board::new(),
|
2023-08-23 03:52:03 +00:00
|
|
|
player_states: PlayerStates(player_states),
|
2023-09-10 22:17:36 +00:00
|
|
|
dictionary,
|
|
|
|
turn_order: 0,
|
2023-08-06 01:59:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-16 02:35:23 +00:00
|
|
|
|
2023-08-10 03:00:14 +00:00
|
|
|
|
|
|
|
pub fn get_board(&self) -> &Board {&self.board}
|
|
|
|
|
2023-08-17 02:37:32 +00:00
|
|
|
pub fn set_board(&mut self, new_board: Board) {
|
|
|
|
self.board = new_board;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn fill_trays(&mut self){
|
2023-08-23 03:52:03 +00:00
|
|
|
for state in self.player_states.0.iter_mut() {
|
2023-08-17 02:37:32 +00:00
|
|
|
let tray = &mut state.tray;
|
|
|
|
tray.fill(&mut self.tile_pool);
|
|
|
|
}
|
2023-08-13 20:08:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_dictionary(&self) -> &DictionaryImpl {
|
|
|
|
&self.dictionary
|
|
|
|
}
|
|
|
|
|
2023-09-15 01:53:47 +00:00
|
|
|
pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<TurnAction, String> {
|
2023-09-10 22:17:36 +00:00
|
|
|
|
|
|
|
let player = self.current_player_name();
|
2023-08-19 01:14:27 +00:00
|
|
|
|
|
|
|
let mut board_instance = self.get_board().clone();
|
2023-09-10 22:17:36 +00:00
|
|
|
let mut tray = self.player_states.get_tray(&player).unwrap().clone();
|
2023-08-19 01:14:27 +00:00
|
|
|
|
2023-09-13 05:13:36 +00:00
|
|
|
let played_letters: Vec<(Letter, Coordinates)> = PlayedTile::convert_tray(&tray_tile_locations, &tray);
|
2023-08-19 01:14:27 +00:00
|
|
|
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())?;
|
2023-08-25 03:36:41 +00:00
|
|
|
let total_score = x.1;
|
2023-08-19 01:14:27 +00:00
|
|
|
|
|
|
|
let words: Vec<WordResult> = x.0.iter()
|
|
|
|
.map(|(word, score)| {
|
|
|
|
WordResult {
|
|
|
|
word: word.to_string(),
|
|
|
|
score: *score
|
|
|
|
}
|
|
|
|
|
|
|
|
})
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
if commit_move {
|
2023-09-15 01:53:47 +00:00
|
|
|
let player_state = self.player_states.get_player_state_mut(&player).unwrap();
|
2023-08-25 03:36:41 +00:00
|
|
|
player_state.score += total_score;
|
2023-08-19 01:14:27 +00:00
|
|
|
player_state.tray = tray;
|
|
|
|
|
|
|
|
board_instance.fix_tiles();
|
|
|
|
self.set_board(board_instance);
|
2023-09-10 22:17:36 +00:00
|
|
|
self.fill_trays();
|
|
|
|
|
|
|
|
self.increment_turn();
|
2023-08-19 01:14:27 +00:00
|
|
|
}
|
|
|
|
|
2023-09-15 01:53:47 +00:00
|
|
|
Ok(TurnAction::PlayTiles {
|
|
|
|
result: ScoreResult {
|
|
|
|
words,
|
|
|
|
total: total_score,
|
|
|
|
},
|
2023-08-25 03:36:41 +00:00
|
|
|
})
|
2023-08-19 01:14:27 +00:00
|
|
|
}
|
2023-08-13 20:08:37 +00:00
|
|
|
|
2023-09-15 01:53:47 +00:00
|
|
|
pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<(Tray, TurnAction), String> {
|
2023-09-10 22:17:36 +00:00
|
|
|
let player = self.current_player_name();
|
|
|
|
let tray = match self.player_states.get_tray_mut(&player) {
|
2023-08-23 03:52:03 +00:00
|
|
|
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;
|
2023-09-15 01:53:47 +00:00
|
|
|
let mut tiles_exchanged = 0;
|
2023-08-23 03:52:03 +00:00
|
|
|
|
|
|
|
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;
|
2023-09-15 01:53:47 +00:00
|
|
|
tiles_exchanged += 1;
|
2023-08-23 03:52:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
tile_pool.shuffle(&mut self.rng);
|
|
|
|
tray.fill(&mut self.tile_pool);
|
|
|
|
|
2023-09-10 22:17:36 +00:00
|
|
|
let tray = tray.clone();
|
|
|
|
|
|
|
|
self.increment_turn();
|
|
|
|
|
2023-09-15 01:53:47 +00:00
|
|
|
Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }))
|
2023-08-23 03:52:03 +00:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-08-25 03:29:08 +00:00
|
|
|
pub fn add_word(&mut self, word: String) {
|
|
|
|
let word = word.to_uppercase();
|
|
|
|
|
|
|
|
self.dictionary.insert(word, -1.0);
|
|
|
|
}
|
|
|
|
|
2023-09-10 22:17:36 +00:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2023-09-13 05:13:36 +00:00
|
|
|
pub fn advance_turn(&mut self) -> Result<TurnAdvanceResult, String> {
|
2023-09-10 22:17:36 +00:00
|
|
|
let current_player = self.current_player_name();
|
2023-09-13 05:13:36 +00:00
|
|
|
let state = self.player_states.get_player_state_mut(¤t_player).ok_or("There should be a player available")?;
|
2023-09-10 22:17:36 +00:00
|
|
|
|
|
|
|
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);
|
2023-09-13 05:13:36 +00:00
|
|
|
//let best_move: Option<CompleteMove> = None;
|
2023-09-10 22:17:36 +00:00
|
|
|
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(){
|
2023-09-15 01:53:47 +00:00
|
|
|
self.increment_turn();
|
2023-09-13 05:13:36 +00:00
|
|
|
Ok(TurnAdvanceResult::AIMove {
|
2023-09-10 22:17:36 +00:00
|
|
|
name: current_player,
|
|
|
|
action: TurnAction::Pass,
|
2023-09-13 05:13:36 +00:00
|
|
|
})
|
2023-09-10 22:17:36 +00:00
|
|
|
} else {
|
2023-09-15 01:53:47 +00:00
|
|
|
let (_, action) = self.exchange_tiles(to_exchange)?;
|
2023-09-13 05:13:36 +00:00
|
|
|
Ok(TurnAdvanceResult::AIMove {
|
2023-09-10 22:17:36 +00:00
|
|
|
name: current_player,
|
2023-09-15 01:53:47 +00:00
|
|
|
action,
|
2023-09-13 05:13:36 +00:00
|
|
|
})
|
2023-09-10 22:17:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
Some(best_move) => {
|
|
|
|
let play = best_move.convert_to_play(tray);
|
2023-09-15 01:53:47 +00:00
|
|
|
let action = self.receive_play(play, true)?;
|
2023-09-13 05:13:36 +00:00
|
|
|
Ok(TurnAdvanceResult::AIMove {
|
2023-09-10 22:17:36 +00:00
|
|
|
name: current_player,
|
2023-09-15 01:53:47 +00:00
|
|
|
action,
|
2023-09-13 05:13:36 +00:00
|
|
|
})
|
2023-09-10 22:17:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2023-09-13 05:13:36 +00:00
|
|
|
Ok(TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()})
|
2023-09-10 22:17:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2023-09-13 05:13:36 +00:00
|
|
|
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
2023-09-10 22:17:36 +00:00
|
|
|
#[tsify(from_wasm_abi)]
|
2023-09-13 05:13:36 +00:00
|
|
|
#[serde(tag = "type")]
|
2023-09-10 22:17:36 +00:00
|
|
|
pub enum TurnAction {
|
|
|
|
Pass,
|
2023-09-13 05:13:36 +00:00
|
|
|
ExchangeTiles{
|
|
|
|
tiles_exchanged: usize
|
|
|
|
},
|
|
|
|
PlayTiles{
|
|
|
|
result: ScoreResult
|
|
|
|
},
|
2023-09-10 22:17:36 +00:00
|
|
|
}
|
|
|
|
|
2023-09-13 05:13:36 +00:00
|
|
|
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
2023-09-10 22:17:36 +00:00
|
|
|
#[tsify(from_wasm_abi)]
|
2023-09-13 05:13:36 +00:00
|
|
|
#[serde(tag = "type")]
|
2023-09-10 22:17:36 +00:00
|
|
|
pub enum TurnAdvanceResult {
|
2023-09-13 05:13:36 +00:00
|
|
|
HumanInputRequired{
|
|
|
|
name: String
|
|
|
|
},
|
2023-09-10 22:17:36 +00:00
|
|
|
AIMove{
|
|
|
|
name: String,
|
|
|
|
action: TurnAction,
|
|
|
|
}
|
2023-09-13 05:13:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[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:?}");
|
|
|
|
|
2023-09-15 01:53:47 +00:00
|
|
|
assert_eq!(game.current_player_name(), "Player");
|
|
|
|
|
2023-09-13 05:13:36 +00:00
|
|
|
}
|
|
|
|
|
2023-08-06 01:59:30 +00:00
|
|
|
}
|