Add basic AI support (Rust only)
This commit is contained in:
parent
cdfd8b5ee9
commit
4b86c031ed
2 changed files with 926 additions and 13 deletions
24
src/board.rs
24
src/board.rs
|
@ -9,12 +9,12 @@ use crate::dictionary::DictionaryImpl;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum Direction {
|
pub enum Direction {
|
||||||
Row, Column
|
Row, Column
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Direction {
|
impl Direction {
|
||||||
fn invert(&self) -> Self {
|
pub fn invert(&self) -> Self {
|
||||||
match &self {
|
match &self {
|
||||||
Direction::Row => {Direction::Column}
|
Direction::Row => {Direction::Column}
|
||||||
Direction::Column => {Direction::Row}
|
Direction::Column => {Direction::Row}
|
||||||
|
@ -22,7 +22,7 @@ impl Direction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||||
pub struct Coordinates (pub u8, pub u8);
|
pub struct Coordinates (pub u8, pub u8);
|
||||||
|
|
||||||
impl Coordinates {
|
impl Coordinates {
|
||||||
|
@ -47,20 +47,20 @@ impl Coordinates {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn increment(&self, direction: Direction) -> Option<Self>{
|
pub fn increment(&self, direction: Direction) -> Option<Self>{
|
||||||
self.add(direction, 1)
|
self.add(direction, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrement(&self, direction: Direction) -> Option<Self>{
|
pub fn decrement(&self, direction: Direction) -> Option<Self>{
|
||||||
self.add(direction, -1)
|
self.add(direction, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_to_index(&self) -> usize {
|
pub fn map_to_index(&self) -> usize {
|
||||||
(self.0 + GRID_LENGTH*self.1) as usize
|
(self.0 + GRID_LENGTH*self.1) as usize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash)]
|
||||||
#[tsify(from_wasm_abi)]
|
#[tsify(from_wasm_abi)]
|
||||||
pub struct Letter {
|
pub struct Letter {
|
||||||
pub text: char,
|
pub text: char,
|
||||||
|
@ -143,7 +143,7 @@ impl<'a> ToString for Word<'a> {
|
||||||
|
|
||||||
impl <'a> Word<'a> {
|
impl <'a> Word<'a> {
|
||||||
|
|
||||||
fn calculate_score(&self) -> u32{
|
pub fn calculate_score(&self) -> u32{
|
||||||
let mut multiplier = 1;
|
let mut multiplier = 1;
|
||||||
let mut unmultiplied_score = 0;
|
let mut unmultiplied_score = 0;
|
||||||
|
|
||||||
|
@ -330,11 +330,9 @@ impl Board {
|
||||||
let starting_row = *rows_played.iter().min().unwrap();
|
let starting_row = *rows_played.iter().min().unwrap();
|
||||||
let starting_column = *columns_played.iter().min().unwrap();
|
let starting_column = *columns_played.iter().min().unwrap();
|
||||||
|
|
||||||
let mut starting_coords = Coordinates(starting_row, starting_column);
|
let starting_coords = Coordinates(starting_row, starting_column);
|
||||||
let main_word = self.find_word_at_position(starting_coords, direction).unwrap();
|
let main_word = self.find_word_at_position(starting_coords, direction).unwrap();
|
||||||
|
|
||||||
starting_coords = main_word.coords;
|
|
||||||
|
|
||||||
let mut words = Vec::new();
|
let mut words = Vec::new();
|
||||||
let mut observed_tiles_played = 0;
|
let mut observed_tiles_played = 0;
|
||||||
|
|
||||||
|
@ -389,7 +387,7 @@ impl Board {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option<Word> {
|
pub fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option<Word> {
|
||||||
// let's see how far we can backtrack to the start of the word
|
// let's see how far we can backtrack to the start of the word
|
||||||
let mut times_moved = 0;
|
let mut times_moved = 0;
|
||||||
loop {
|
loop {
|
||||||
|
@ -443,7 +441,7 @@ impl Board {
|
||||||
pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> {
|
pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> {
|
||||||
for (mut letter, coords) in play {
|
for (mut letter, coords) in play {
|
||||||
{
|
{
|
||||||
let mut cell = match self.get_cell_mut(coords) {
|
let cell = match self.get_cell_mut(coords) {
|
||||||
Ok(cell) => {cell}
|
Ok(cell) => {cell}
|
||||||
Err(e) => {return Err(e.to_string())}
|
Err(e) => {return Err(e.to_string())}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,921 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use rand::Rng;
|
||||||
|
use crate::board::{Board, CellType, Coordinates, Direction, Letter};
|
||||||
|
use crate::constants::GRID_LENGTH;
|
||||||
|
use crate::dictionary::DictionaryImpl;
|
||||||
|
use crate::player_interaction::Tray;
|
||||||
|
|
||||||
|
const ALPHABET: [char; 26] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
||||||
|
|
||||||
|
struct CoordinateLineMapper {
|
||||||
|
direction: Direction,
|
||||||
|
fixed: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CoordinateLineMapper {
|
||||||
|
fn line_to_coord(&self, variable: u8) -> Coordinates {
|
||||||
|
match self.direction {
|
||||||
|
Direction::Row => {
|
||||||
|
Coordinates(variable, self.fixed)
|
||||||
|
}
|
||||||
|
Direction::Column => {
|
||||||
|
Coordinates(self.fixed, variable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
pub struct Difficulty {
|
pub struct Difficulty {
|
||||||
proportion: f64,
|
proportion: f64,
|
||||||
randomness: f64,
|
randomness: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AI {
|
||||||
|
difficulty: Difficulty,
|
||||||
|
dictionary: HashSet<String>,
|
||||||
|
substrings: HashSet<String>,
|
||||||
|
|
||||||
|
board_view: Board,
|
||||||
|
column_cross_tiles: CrossTiles,
|
||||||
|
row_cross_tiles: CrossTiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
type MoveMap = HashMap<char, u32>;
|
||||||
|
type CrossTiles = Vec<Option<MoveMap>>;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||||
|
pub struct CompleteMove {
|
||||||
|
moves: Vec<MoveComponent>,
|
||||||
|
score: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct MoveScoring {
|
||||||
|
main_scoring: u32,
|
||||||
|
cross_scoring: u32,
|
||||||
|
multiplier: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MoveScoring {
|
||||||
|
fn new() -> Self {
|
||||||
|
MoveScoring {
|
||||||
|
main_scoring: 0,
|
||||||
|
cross_scoring: 0,
|
||||||
|
multiplier: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
|
struct MoveComponent {
|
||||||
|
coordinates: Coordinates,
|
||||||
|
letter: Letter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AI {
|
||||||
|
|
||||||
|
pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self{
|
||||||
|
let mut ai_dictionary = HashSet::new();
|
||||||
|
let mut substrings = HashSet::new();
|
||||||
|
|
||||||
|
for (word, score) in dictionary.iter() {
|
||||||
|
if *score >= difficulty.proportion { // TODO - may need to reverse
|
||||||
|
|
||||||
|
ai_dictionary.insert(word.clone());
|
||||||
|
substrings.insert(word.clone());
|
||||||
|
|
||||||
|
for i in 0..word.len() {
|
||||||
|
for j in i+1..word.len() {
|
||||||
|
substrings.insert(word[i..j].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let board_view = Board::new();
|
||||||
|
let mut column_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
|
||||||
|
let mut row_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
|
||||||
|
|
||||||
|
board_view.cells.iter().for_each(|_| {
|
||||||
|
column_cross_tiles.push(None);
|
||||||
|
row_cross_tiles.push(None);
|
||||||
|
});
|
||||||
|
|
||||||
|
AI {
|
||||||
|
difficulty,
|
||||||
|
dictionary: ai_dictionary,
|
||||||
|
substrings,
|
||||||
|
|
||||||
|
board_view,
|
||||||
|
column_cross_tiles,
|
||||||
|
row_cross_tiles,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_best_move<R: Rng>(&mut self, tray: &Tray, grid: &Board, rng: &mut R) -> Option<CompleteMove>{
|
||||||
|
let move_set = self.find_all_moves(tray, grid);
|
||||||
|
|
||||||
|
let mut best_move: Option<(CompleteMove, f64)> = None;
|
||||||
|
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
possible_move.score as f64
|
||||||
|
};
|
||||||
|
|
||||||
|
match &best_move {
|
||||||
|
None => {
|
||||||
|
best_move = Some((possible_move, move_score));
|
||||||
|
}
|
||||||
|
Some((_, current_score)) => {
|
||||||
|
if move_score > *current_score {
|
||||||
|
best_move = Some((possible_move, move_score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return match best_move {
|
||||||
|
None => {None}
|
||||||
|
Some((best_move, _)) => {Some(best_move)}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> {
|
||||||
|
self.update_state(grid);
|
||||||
|
|
||||||
|
let mut all_moves: HashSet<CompleteMove> = HashSet::new();
|
||||||
|
|
||||||
|
self.find_moves_in_direction(tray, Direction::Row, &mut all_moves);
|
||||||
|
self.find_moves_in_direction(tray, Direction::Column, &mut all_moves);
|
||||||
|
|
||||||
|
all_moves
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_moves_in_direction(
|
||||||
|
&self,
|
||||||
|
tray: &Tray,
|
||||||
|
direction: Direction,
|
||||||
|
all_moves: &mut HashSet<CompleteMove>) {
|
||||||
|
|
||||||
|
// If you're building a word in one direction, you need to form valid words in the cross-direction
|
||||||
|
let cross_tiles = match direction {
|
||||||
|
Direction::Column => {&self.row_cross_tiles}
|
||||||
|
Direction::Row => {&self.column_cross_tiles}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tray_letters = tray.letters.iter()
|
||||||
|
.filter(|letter| letter.is_some())
|
||||||
|
.map(|letter| letter.unwrap())
|
||||||
|
.collect::<Vec<Letter>>();
|
||||||
|
|
||||||
|
for k in 0..GRID_LENGTH {
|
||||||
|
let mut line_letters = Vec::with_capacity(GRID_LENGTH as usize);
|
||||||
|
let mut line_cross_letters = Vec::with_capacity(GRID_LENGTH as usize);
|
||||||
|
|
||||||
|
let coord_mapper = CoordinateLineMapper {
|
||||||
|
direction,
|
||||||
|
fixed: k
|
||||||
|
};
|
||||||
|
|
||||||
|
for p in 0..GRID_LENGTH {
|
||||||
|
let coords = coord_mapper.line_to_coord(p);
|
||||||
|
line_letters.push(self.board_view.get_cell(coords).unwrap().value);
|
||||||
|
line_cross_letters.push(cross_tiles.get(coords.map_to_index()).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
for l in 0..GRID_LENGTH {
|
||||||
|
let coords = coord_mapper.line_to_coord(l);
|
||||||
|
|
||||||
|
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row) ||
|
||||||
|
check_if_anchored(&self.board_view, coords, Direction::Column);
|
||||||
|
|
||||||
|
if is_anchored &&
|
||||||
|
line_letters.get(l as usize).unwrap().is_none() // it's duplicate work to check here when we'll already check at either free side
|
||||||
|
{
|
||||||
|
|
||||||
|
self.evaluate_spot_heading_left(
|
||||||
|
&line_letters,
|
||||||
|
&line_cross_letters,
|
||||||
|
l as i8,
|
||||||
|
&coord_mapper,
|
||||||
|
tray_letters.clone(),
|
||||||
|
&Vec::new(),
|
||||||
|
1,
|
||||||
|
&MoveScoring::new(),
|
||||||
|
|
||||||
|
all_moves
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_spot_heading_left(
|
||||||
|
&self,
|
||||||
|
line_letters: &Vec<Option<Letter>>,
|
||||||
|
line_cross_letters: &Vec<&Option<MoveMap>>,
|
||||||
|
line_index: i8,
|
||||||
|
|
||||||
|
coord_mapper: &CoordinateLineMapper,
|
||||||
|
available_letters: Vec<Letter>,
|
||||||
|
current_play: &Vec<MoveComponent>,
|
||||||
|
min_length: usize,
|
||||||
|
current_points: &MoveScoring,
|
||||||
|
|
||||||
|
all_moves: &mut HashSet<CompleteMove>
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we want to keep heading left until we get to a blank space
|
||||||
|
match line_letters.get(line_index as usize).unwrap() {
|
||||||
|
Some(_) => {
|
||||||
|
// there's a letter here; need to take a step left if we can
|
||||||
|
|
||||||
|
if !(line_index >= 1 &&
|
||||||
|
line_letters.get((line_index-1) as usize).unwrap().is_some() &&
|
||||||
|
min_length == 1
|
||||||
|
) {
|
||||||
|
// if-statement is basically saying that if we're at the start of the process (min_length==1) and there's a word still to our left,
|
||||||
|
// just stop. Other versions of the for-loops that call this function will have picked up that case.
|
||||||
|
self.evaluate_spot_heading_left(
|
||||||
|
line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index - 1,
|
||||||
|
coord_mapper,
|
||||||
|
available_letters,
|
||||||
|
current_play,
|
||||||
|
min_length,
|
||||||
|
current_points,
|
||||||
|
all_moves
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// there's no word to the left so we can start trying to play
|
||||||
|
for letter in available_letters.iter() {
|
||||||
|
self.evaluate_letter_at_spot(
|
||||||
|
line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index,
|
||||||
|
current_play,
|
||||||
|
min_length,
|
||||||
|
current_points,
|
||||||
|
coord_mapper,
|
||||||
|
|
||||||
|
&available_letters,
|
||||||
|
letter.clone(),
|
||||||
|
all_moves
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// after trying those plays, maybe we can try a longer word by moving another to the left?
|
||||||
|
self.evaluate_spot_heading_left(
|
||||||
|
line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index - 1,
|
||||||
|
coord_mapper,
|
||||||
|
available_letters,
|
||||||
|
current_play,
|
||||||
|
min_length + 1,
|
||||||
|
current_points,
|
||||||
|
all_moves
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_letter_at_spot(
|
||||||
|
&self,
|
||||||
|
line_letters: &Vec<Option<Letter>>,
|
||||||
|
line_cross_letters: &Vec<&Option<MoveMap>>,
|
||||||
|
line_index: i8,
|
||||||
|
current_play: &Vec<MoveComponent>,
|
||||||
|
min_length: usize,
|
||||||
|
current_points: &MoveScoring,
|
||||||
|
coord_mapper: &CoordinateLineMapper,
|
||||||
|
|
||||||
|
|
||||||
|
available_letters: &Vec<Letter>,
|
||||||
|
letter: Letter,
|
||||||
|
all_moves: &mut HashSet<CompleteMove>
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
if letter.is_blank {
|
||||||
|
// need to loop through alphabet
|
||||||
|
for alpha in ALPHABET {
|
||||||
|
let mut letter = letter.clone();
|
||||||
|
letter.text = alpha;
|
||||||
|
|
||||||
|
self.evaluate_non_blank_letter_at_spot(
|
||||||
|
line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index,
|
||||||
|
current_play.clone(),
|
||||||
|
min_length,
|
||||||
|
current_points,
|
||||||
|
coord_mapper,
|
||||||
|
available_letters,
|
||||||
|
letter,
|
||||||
|
all_moves
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.evaluate_non_blank_letter_at_spot(
|
||||||
|
line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index,
|
||||||
|
current_play.clone(),
|
||||||
|
min_length,
|
||||||
|
current_points,
|
||||||
|
coord_mapper,
|
||||||
|
available_letters,
|
||||||
|
letter.clone(),
|
||||||
|
all_moves
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_non_blank_letter_at_spot(
|
||||||
|
&self,
|
||||||
|
line_letters: &Vec<Option<Letter>>,
|
||||||
|
line_cross_letters: &Vec<&Option<MoveMap>>,
|
||||||
|
line_index: i8,
|
||||||
|
mut current_play: Vec<MoveComponent>,
|
||||||
|
min_length: usize,
|
||||||
|
current_points: &MoveScoring,
|
||||||
|
|
||||||
|
|
||||||
|
coord_mapper: &CoordinateLineMapper,
|
||||||
|
available_letters: &Vec<Letter>,
|
||||||
|
letter: Letter,
|
||||||
|
|
||||||
|
all_moves: &mut HashSet<CompleteMove>,
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
assert!(!letter.is_blank); // we should have handled blanks in evaluate_letter_at_spot
|
||||||
|
|
||||||
|
// let's now assign the letter to this spot
|
||||||
|
let mut line_letters = line_letters.clone();
|
||||||
|
*line_letters.get_mut(line_index as usize).unwrap() = Some(letter);
|
||||||
|
current_play.push(MoveComponent {
|
||||||
|
coordinates: coord_mapper.line_to_coord(line_index as u8),
|
||||||
|
letter,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if we form at least part of a valid word, otherwise we can just skip forward
|
||||||
|
let word = find_word_on_line(&line_letters, line_index);
|
||||||
|
if !self.substrings.contains(&word) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cross_letters = line_cross_letters.get(line_index as usize).unwrap();
|
||||||
|
|
||||||
|
// let's first verify that `letter` makes a valid cross-word.
|
||||||
|
match cross_letters {
|
||||||
|
None => {}
|
||||||
|
Some(map) => {
|
||||||
|
if !map.contains_key(&letter.text) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time to start scoring what this move might look like
|
||||||
|
|
||||||
|
// making copy
|
||||||
|
let mut current_points = current_points.clone();
|
||||||
|
let cell_type = self.board_view.get_cell(coord_mapper.line_to_coord(line_index as u8)).unwrap().cell_type;
|
||||||
|
|
||||||
|
// first we score cross letters
|
||||||
|
if let Some(map) = cross_letters {
|
||||||
|
let mut cross_word_score: u32 = 0;
|
||||||
|
let mut cross_letter_score = *map.get(&letter.text).unwrap(); // I can unwrap because I earlier confirmed that letter is in the map
|
||||||
|
|
||||||
|
match cell_type {
|
||||||
|
CellType::DoubleLetter => {
|
||||||
|
cross_letter_score *= 2;
|
||||||
|
}
|
||||||
|
CellType::TripleLetter => {
|
||||||
|
cross_letter_score *= 3;
|
||||||
|
}
|
||||||
|
_ => {} // no letter multiplier
|
||||||
|
};
|
||||||
|
|
||||||
|
cross_word_score += cross_letter_score;
|
||||||
|
|
||||||
|
// now we see if we multiply the word
|
||||||
|
match cell_type {
|
||||||
|
CellType::DoubleWord | CellType::Start => {
|
||||||
|
cross_word_score *= 2;
|
||||||
|
}
|
||||||
|
CellType::TripleWord => {
|
||||||
|
cross_word_score *= 3;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
current_points.cross_scoring += cross_word_score;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we score on the main axis
|
||||||
|
match cell_type {
|
||||||
|
CellType::DoubleWord | CellType::Start => {
|
||||||
|
current_points.multiplier *= 2;
|
||||||
|
current_points.main_scoring += letter.points;
|
||||||
|
}
|
||||||
|
CellType::TripleWord => {
|
||||||
|
current_points.multiplier *= 3;
|
||||||
|
current_points.main_scoring += letter.points;
|
||||||
|
}
|
||||||
|
CellType::Normal => {
|
||||||
|
current_points.main_scoring += letter.points;
|
||||||
|
}
|
||||||
|
CellType::DoubleLetter => {
|
||||||
|
current_points.main_scoring += letter.points * 2;
|
||||||
|
}
|
||||||
|
CellType::TripleLetter => {
|
||||||
|
current_points.main_scoring += letter.points * 3;}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
moves: current_play.clone(),
|
||||||
|
score: current_points.cross_scoring + current_points.main_scoring*current_points.multiplier,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the first instance of letter from available_letters
|
||||||
|
let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1);
|
||||||
|
let mut skipped_one = false;
|
||||||
|
|
||||||
|
available_letters.iter()
|
||||||
|
.for_each(|in_tray| {
|
||||||
|
if skipped_one || *in_tray != letter {
|
||||||
|
new_available_letters.push(*in_tray);
|
||||||
|
} else {
|
||||||
|
skipped_one = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.evaluate_spot_heading_right(
|
||||||
|
&line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index+1,
|
||||||
|
current_play,
|
||||||
|
min_length,
|
||||||
|
¤t_points,
|
||||||
|
|
||||||
|
coord_mapper,
|
||||||
|
&new_available_letters,
|
||||||
|
|
||||||
|
all_moves
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_spot_heading_right(
|
||||||
|
&self,
|
||||||
|
line_letters: &Vec<Option<Letter>>,
|
||||||
|
line_cross_letters: &Vec<&Option<MoveMap>>,
|
||||||
|
line_index: i8,
|
||||||
|
current_play: Vec<MoveComponent>,
|
||||||
|
min_length: usize,
|
||||||
|
current_points: &MoveScoring,
|
||||||
|
|
||||||
|
|
||||||
|
coord_mapper: &CoordinateLineMapper,
|
||||||
|
available_letters: &Vec<Letter>,
|
||||||
|
|
||||||
|
all_moves: &mut HashSet<CompleteMove>,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// out-of-bounds check
|
||||||
|
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match line_letters.get(line_index as usize).unwrap() {
|
||||||
|
Some(letter) => {
|
||||||
|
// there's a letter already here so let's move again to the right
|
||||||
|
let mut current_points = current_points.clone();
|
||||||
|
current_points.main_scoring += letter.points;
|
||||||
|
self.evaluate_spot_heading_right(
|
||||||
|
line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index + 1,
|
||||||
|
current_play,
|
||||||
|
min_length,
|
||||||
|
¤t_points,
|
||||||
|
coord_mapper,
|
||||||
|
available_letters,
|
||||||
|
all_moves
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// there's a blank spot, so let's loop through every available letter and evaluate at this spot
|
||||||
|
for letter in available_letters {
|
||||||
|
self.evaluate_letter_at_spot(
|
||||||
|
line_letters,
|
||||||
|
line_cross_letters,
|
||||||
|
line_index,
|
||||||
|
¤t_play.clone(),
|
||||||
|
min_length,
|
||||||
|
current_points,
|
||||||
|
coord_mapper,
|
||||||
|
available_letters,
|
||||||
|
|
||||||
|
letter.clone(),
|
||||||
|
all_moves
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_state(&mut self, new: &Board) {
|
||||||
|
let mut updated_rows = HashSet::new();
|
||||||
|
let mut updated_columns = HashSet::new();
|
||||||
|
|
||||||
|
for (x, y) in new.cells.iter().zip(self.board_view.cells.iter_mut()) {
|
||||||
|
if !(x.value.eq(&y.value)) {
|
||||||
|
y.value = x.value;
|
||||||
|
let coords = x.coordinates;
|
||||||
|
updated_rows.insert(coords.1);
|
||||||
|
updated_columns.insert(coords.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut check_for_valid_moves = |coords: Coordinates| {
|
||||||
|
|
||||||
|
let cell = new.get_cell(coords).unwrap();
|
||||||
|
if cell.value.is_none() {
|
||||||
|
let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords);
|
||||||
|
let valid_column_moves = self.get_letters_that_make_words(new, Direction::Column, coords);
|
||||||
|
|
||||||
|
let existing_row_moves = self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
|
||||||
|
*existing_row_moves = valid_row_moves;
|
||||||
|
|
||||||
|
let existing_column_moves = self.column_cross_tiles.get_mut(coords.map_to_index()).unwrap();
|
||||||
|
*existing_column_moves = valid_column_moves;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updated_rows.iter().for_each(|&j| {
|
||||||
|
for i in 0..GRID_LENGTH {
|
||||||
|
let coords = Coordinates(i, j);
|
||||||
|
check_for_valid_moves(coords);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updated_columns.iter().for_each(|&i| {
|
||||||
|
for j in 0..GRID_LENGTH {
|
||||||
|
let coords = Coordinates(i, j);
|
||||||
|
check_for_valid_moves(coords);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_letters_that_make_words(&self, new: &Board, direction: Direction, coords: Coordinates) -> Option<MoveMap> {
|
||||||
|
let is_anchored = check_if_anchored(new, coords, direction);
|
||||||
|
if !is_anchored {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_map = MoveMap::new();
|
||||||
|
|
||||||
|
let mut copy_grid = new.clone();
|
||||||
|
for letter in ALPHABET {
|
||||||
|
let cell = copy_grid.get_cell_mut(coords).unwrap();
|
||||||
|
cell.value = Some(Letter {
|
||||||
|
text: letter,
|
||||||
|
points: 0, // points are accounted for later
|
||||||
|
ephemeral: false, // so that tile bonuses are ignored
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let word = copy_grid.find_word_at_position(coords, direction);
|
||||||
|
match word {
|
||||||
|
None => {}
|
||||||
|
Some(word) => {
|
||||||
|
if self.dictionary.contains(&word.to_string()) {
|
||||||
|
let score = word.calculate_score();
|
||||||
|
new_map.insert(letter, score);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(new_map)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool{
|
||||||
|
|
||||||
|
// Middle tile is always considered anchored
|
||||||
|
if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_letter = |alt_coord: Option<Coordinates>| -> bool {
|
||||||
|
match alt_coord {
|
||||||
|
None => {false}
|
||||||
|
Some(alt_coord) => {
|
||||||
|
let cell = grid.get_cell(alt_coord).unwrap();
|
||||||
|
cell.value.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return has_letter(coords.increment(direction)) || has_letter(coords.decrement(direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> String {
|
||||||
|
let mut start_word = line_index;
|
||||||
|
let mut end_word = line_index;
|
||||||
|
|
||||||
|
// start moving left until we hit an empty tile
|
||||||
|
while start_word >= 0 {
|
||||||
|
if start_word < 1 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if line_letters.get((start_word - 1) as usize).unwrap().is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start_word -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start moving right until we hit an empty tile
|
||||||
|
while end_word < GRID_LENGTH as i8 {
|
||||||
|
if end_word + 1 >= GRID_LENGTH as i8 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if line_letters.get((end_word + 1) as usize).unwrap().is_none() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
end_word += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut str = String::new();
|
||||||
|
line_letters[(start_word as usize)..((end_word + 1) as usize)]
|
||||||
|
.iter().for_each(|letter| {
|
||||||
|
str.push(letter.unwrap().text);
|
||||||
|
});
|
||||||
|
|
||||||
|
str
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
use rand::SeedableRng;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) {
|
||||||
|
let cell = board.get_cell_mut(Coordinates(x, y)).unwrap();
|
||||||
|
cell.value = Some(Letter {
|
||||||
|
text: letter,
|
||||||
|
points,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dictionary() {
|
||||||
|
let difficulty = Difficulty {
|
||||||
|
proportion: 0.8, // restrict yourself to words with this proportion OR HIGHER
|
||||||
|
randomness: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dictionary = DictionaryImpl::new();
|
||||||
|
dictionary.insert("APP".to_string(), 0.5);
|
||||||
|
dictionary.insert("APPLE".to_string(), 0.9);
|
||||||
|
|
||||||
|
let ai = AI::new(difficulty, &dictionary);
|
||||||
|
|
||||||
|
assert_eq!(1, ai.dictionary.len());
|
||||||
|
assert!(ai.dictionary.contains("APPLE"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_blank_board() {
|
||||||
|
let board = Board::new();
|
||||||
|
|
||||||
|
let difficulty = Difficulty {
|
||||||
|
proportion: 0.8, // restrict yourself to words with this proportion OR HIGHER
|
||||||
|
randomness: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dictionary = DictionaryImpl::new();
|
||||||
|
dictionary.insert("APP".to_string(), 0.5);
|
||||||
|
dictionary.insert("APPLE".to_string(), 0.9);
|
||||||
|
|
||||||
|
let mut tray = Tray::new(7);
|
||||||
|
tray.letters[0] = Some(Letter{
|
||||||
|
text: 'A',
|
||||||
|
points: 5,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
tray.letters[1] = Some(Letter{
|
||||||
|
text: 'P',
|
||||||
|
points: 4,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
tray.letters[2] = Some(Letter{
|
||||||
|
text: 'P',
|
||||||
|
points: 4,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
tray.letters[3] = Some(Letter{
|
||||||
|
text: 'L',
|
||||||
|
points: 3,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
tray.letters[4] = Some(Letter{
|
||||||
|
text: 'E',
|
||||||
|
points: 4,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut ai = AI::new(difficulty, &dictionary);
|
||||||
|
|
||||||
|
//let rng = SmallRng::seed_from_u64(123);
|
||||||
|
|
||||||
|
let moves = ai.find_all_moves(&tray, &board);
|
||||||
|
|
||||||
|
println!("Moves are {:?}", moves);
|
||||||
|
|
||||||
|
assert_eq!(moves.len(), 10);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cross_words() {
|
||||||
|
let mut board = Board::new();
|
||||||
|
|
||||||
|
set_cell(&mut board, 7, 7, 'B', 5);
|
||||||
|
set_cell(&mut board, 7, 8, 'O', 1);
|
||||||
|
set_cell(&mut board, 7, 9, 'A', 1);
|
||||||
|
set_cell(&mut board, 7, 10, 'T', 1);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let difficulty = Difficulty {
|
||||||
|
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
||||||
|
randomness: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dictionary = DictionaryImpl::new();
|
||||||
|
dictionary.insert("BOATS".to_string(), 0.5);
|
||||||
|
dictionary.insert("SLAM".to_string(), 0.9);
|
||||||
|
|
||||||
|
let mut tray = Tray::new(7);
|
||||||
|
tray.letters[0] = Some(Letter{
|
||||||
|
text: 'S',
|
||||||
|
points: 1,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
tray.letters[1] = Some(Letter{
|
||||||
|
text: 'L',
|
||||||
|
points: 1,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
tray.letters[2] = Some(Letter{
|
||||||
|
text: 'A',
|
||||||
|
points: 1,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
tray.letters[3] = Some(Letter{
|
||||||
|
text: 'M',
|
||||||
|
points: 3,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut ai = AI::new(difficulty, &dictionary);
|
||||||
|
|
||||||
|
ai.update_state(&board);
|
||||||
|
|
||||||
|
let end_of_boat = ai.column_cross_tiles.get(Coordinates(7, 11).map_to_index()).unwrap();
|
||||||
|
assert!(end_of_boat.is_some());
|
||||||
|
assert_eq!(end_of_boat.as_ref().unwrap().len(), 1);
|
||||||
|
|
||||||
|
//let rng = SmallRng::seed_from_u64(123);
|
||||||
|
|
||||||
|
let moves = ai.find_all_moves(&tray, &board);
|
||||||
|
|
||||||
|
println!("Moves are {:?}", moves);
|
||||||
|
|
||||||
|
|
||||||
|
// 2 possible moves -
|
||||||
|
// 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS'
|
||||||
|
// 2. Put 'S' at end of 'BOAT'
|
||||||
|
// 3. Put 'SL' to left of 'A' in 'BOAT' and then 'M' to right of it
|
||||||
|
assert_eq!(moves.len(), 3);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cross_tiles() {
|
||||||
|
let mut board = Board::new();
|
||||||
|
|
||||||
|
set_cell(&mut board, 7, 7, 'B', 5);
|
||||||
|
|
||||||
|
let difficulty = Difficulty {
|
||||||
|
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
||||||
|
randomness: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dictionary = DictionaryImpl::new();
|
||||||
|
dictionary.insert("AB".to_string(), 0.5);
|
||||||
|
|
||||||
|
let mut ai = AI::new(difficulty, &dictionary);
|
||||||
|
ai.update_state(&board);
|
||||||
|
|
||||||
|
let above_cell_coords = Coordinates(7, 6);
|
||||||
|
let left_cell_cords = Coordinates(6, 7);
|
||||||
|
|
||||||
|
let row_cross_tiles = ai.row_cross_tiles.get(left_cell_cords.map_to_index()).unwrap();
|
||||||
|
let column_cross_tiles = ai.column_cross_tiles.get(above_cell_coords.map_to_index()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(row_cross_tiles.as_ref().unwrap().len(), 1);
|
||||||
|
assert_eq!(column_cross_tiles.as_ref().unwrap().len(), 1);
|
||||||
|
|
||||||
|
let far_off_tiles = ai.row_cross_tiles.get(0).unwrap();
|
||||||
|
assert!(far_off_tiles.is_none());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_moves() {
|
||||||
|
let mut board = Board::new();
|
||||||
|
|
||||||
|
set_cell(&mut board, 7-3, 7+3, 'Z', 1);
|
||||||
|
set_cell(&mut board, 6-3, 8+3, 'A', 1);
|
||||||
|
set_cell(&mut board, 7-3, 8+3, 'A', 1);
|
||||||
|
set_cell(&mut board, 7-3, 9+3, 'Z', 1);
|
||||||
|
|
||||||
|
let difficulty = Difficulty {
|
||||||
|
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
||||||
|
randomness: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dictionary = DictionaryImpl::new();
|
||||||
|
dictionary.insert("AA".to_string(), 0.5);
|
||||||
|
|
||||||
|
let mut tray = Tray::new(7);
|
||||||
|
tray.letters[0] = Some(Letter{
|
||||||
|
text: 'A',
|
||||||
|
points: 1,
|
||||||
|
ephemeral: false,
|
||||||
|
is_blank: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut ai = AI::new(difficulty, &dictionary);
|
||||||
|
|
||||||
|
let moves = ai.find_all_moves(&tray, &board);
|
||||||
|
|
||||||
|
println!("Moves are {:?}", moves);
|
||||||
|
|
||||||
|
assert!(moves.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in a new issue