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)]
|
||||
enum Direction {
|
||||
pub enum Direction {
|
||||
Row, Column
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
fn invert(&self) -> Self {
|
||||
pub fn invert(&self) -> Self {
|
||||
match &self {
|
||||
Direction::Row => {Direction::Column}
|
||||
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);
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn decrement(&self, direction: Direction) -> Option<Self>{
|
||||
pub fn decrement(&self, direction: Direction) -> Option<Self>{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
pub struct Letter {
|
||||
pub text: char,
|
||||
|
@ -143,7 +143,7 @@ impl<'a> ToString for Word<'a> {
|
|||
|
||||
impl <'a> Word<'a> {
|
||||
|
||||
fn calculate_score(&self) -> u32{
|
||||
pub fn calculate_score(&self) -> u32{
|
||||
let mut multiplier = 1;
|
||||
let mut unmultiplied_score = 0;
|
||||
|
||||
|
@ -330,11 +330,9 @@ impl Board {
|
|||
let starting_row = *rows_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();
|
||||
|
||||
starting_coords = main_word.coords;
|
||||
|
||||
let mut words = Vec::new();
|
||||
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 mut times_moved = 0;
|
||||
loop {
|
||||
|
@ -443,7 +441,7 @@ impl Board {
|
|||
pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> {
|
||||
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}
|
||||
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 {
|
||||
proportion: 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