Add rudimentry but functional AI integration into UI
Also fixed some bugs that could cause AI process to either produce invalid words or crash.
This commit is contained in:
parent
9b22f1301d
commit
8fd250170b
6 changed files with 194 additions and 48 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
124
src/game.rs
124
src/game.rs
|
@ -41,6 +41,35 @@ pub struct PlayedTile {
|
|||
pub character: Option<char>, // we only set this if PlayedTile is a blank
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[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<TurnAdvanceResult, String> {
|
||||
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<CompleteMove> = 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:?}");
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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<Option<Letter>>, 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());
|
||||
|
||||
}
|
||||
|
||||
}
|
14
src/wasm.rs
14
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<JsValue, Error> {
|
||||
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)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Tray | string> = props.wasm.exchange_tiles("Player", selectedArray);
|
||||
const result: MyResult<Tray | string> = 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<ScoreResult | string> = props.wasm.receive_play("Player", playedTiles, confirmedScorePoints > -1);
|
||||
const result: MyResult<ScoreResult | string> = props.wasm.receive_play(playedTiles, confirmedScorePoints > -1);
|
||||
console.log({result});
|
||||
|
||||
if(result.response_type === "ERR") {
|
||||
|
@ -223,6 +223,16 @@ export function Game(props: {
|
|||
<button onClick={() => {
|
||||
trayDispatch({action: TileDispatchActionType.RETURN});
|
||||
}}>Return Tiles</button>
|
||||
<button onClick={() => {
|
||||
props.wasm.skip_turn();
|
||||
logDispatch(<div><em>Player skipped their turn</em></div>);
|
||||
setTurnCount(turnCount + 1);
|
||||
}}>Skip Turn</button>
|
||||
<button onClick={() => {
|
||||
const result: TurnAdvanceResult = props.wasm.advance_turn();
|
||||
console.info({result});
|
||||
setTurnCount(turnCount + 1);
|
||||
}}>Run AI</button>
|
||||
</>;
|
||||
|
||||
}
|
||||
|
|
|
@ -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}) {
|
|||
<div className="selection-buttons">
|
||||
<button onClick={() => {
|
||||
const seed = new Date().getTime();
|
||||
const game_wasm = new GameWasm(BigInt(seed), props.dictionary_text);
|
||||
const difficulty: Difficulty = {
|
||||
proportion: processedProportionDictionary,
|
||||
randomness: processedAIRandomness,
|
||||
};
|
||||
const game_wasm = new GameWasm(BigInt(seed), props.dictionary_text, difficulty);
|
||||
const game = <Game settings={props.settings} wasm={game_wasm} key={seed} end_game_fn={() => setGame(null)}/>
|
||||
setGame(game);
|
||||
}}>New Game</button>
|
||||
|
|
Loading…
Reference in a new issue