Add basic AI support (Rust only)

This commit is contained in:
Joel Therrien 2023-09-06 19:59:45 -07:00
parent cdfd8b5ee9
commit 4b86c031ed
2 changed files with 926 additions and 13 deletions

View file

@ -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())}
};

View file

@ -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,
&current_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,
&current_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,
&current_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());
}
}