use crate::grid::{
Cell::{LetterBonus, WordBonus},
Grid,
};
use crate::tiles::TryIntoLetters;
use crate::tilesets::{Language, TileSet};
use crate::wordlist::{LetterSet, RowData, Wordlist};
use crate::{Cell, Codec, Error, Item, ItemList, Letter, Letters, List, Row, Tile, Word};
#[cfg(feature = "flame_it")]
use flamer::flame;
#[cfg(feature = "rayon")]
use rayon::prelude::*;
use std::fmt;
const N: usize = 15;
type State = [Row; N];
#[derive(Debug, Clone, Copy)]
pub struct Score {
pub x: usize,
pub y: usize,
pub horizontal: bool,
pub word: Word,
pub score: u32,
}
impl<'a> fmt::Display for Board<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let repr = self
.horizontal
.iter()
.map(|&row| self.wordlist.decode(row).replace(" ", "."))
.collect::<Vec<String>>()
.join("\n");
write!(f, "{}", repr)
}
}
impl<'a> Default for Board<'a> {
fn default() -> Self {
Self::new(Language::EN)
}
}
#[derive(Debug, Clone)]
pub struct Board<'a> {
board: Grid,
empty_row: Row,
horizontal: State,
vertical: State,
rowdata: [[RowData; N]; 2],
tileset: TileSet<'a>,
wordlist: Wordlist,
}
impl<'a> Board<'a> {
#[must_use]
pub fn new(language: Language) -> Board<'a> {
let tileset = TileSet::new(language);
let wordlist = Wordlist::from_words(&[], tileset.codec()).unwrap();
let grid = Grid::default();
let empty_row = wordlist.encode(" ").unwrap();
let mut empty_rowdata = RowData::new();
for _ in 0..N {
empty_rowdata.push((LetterSet::new(), false));
}
Board {
board: grid,
empty_row,
horizontal: [empty_row; N],
vertical: [empty_row; N],
rowdata: [[empty_rowdata; N], [empty_rowdata; N]],
tileset,
wordlist,
}
}
pub fn set_wordlist(&mut self, wordlist: Wordlist) {
self.wordlist = wordlist;
self.set_rowdata();
}
pub fn with_wordlist_from_file(mut self, wordfile: &str) -> Result<Board<'a>, Error> {
self.set_wordlist(Wordlist::from_file(wordfile, self.codec())?);
Ok(self)
}
pub fn with_wordlist_from_words(mut self, words: &[&str]) -> Result<Board<'a>, Error> {
self.set_wordlist(Wordlist::from_words(words, self.codec())?);
Ok(self)
}
#[cfg(feature = "bincode")]
pub fn with_wordlist_deserialize_from(mut self, wordfile: &str) -> Result<Board<'a>, Error> {
self.set_wordlist(Wordlist::deserialize_from(wordfile)?);
Ok(self)
}
pub fn state_from_strings<S: AsRef<str>>(&mut self, rows: &[S]) -> Result<State, Error> {
if rows.len() != N {
return Err(Error::InvalidRowCount(rows.len()));
}
let mut state = [Row::new(); N];
for (i, row) in rows.iter().enumerate() {
let encoded = self.wordlist.encode(row.as_ref())?;
if encoded.len() != N {
return Err(Error::InvalidRowLength(
String::from(row.as_ref()),
encoded.len(),
));
}
state[i] = encoded;
}
Ok(state)
}
pub fn set_state_from_strings<S: AsRef<str>>(&mut self, rows: &[S]) -> Result<(), Error> {
let state = self.state_from_strings(rows.as_ref())?;
self.set_state(&state);
Ok(())
}
pub fn with_state_from_strings<S: AsRef<str>>(
mut self,
rows: &[S],
) -> Result<Board<'a>, Error> {
self.set_state_from_strings(rows)?;
Ok(self)
}
pub fn set_state(&mut self, rows: &State) {
self.horizontal = *rows;
for i in 0..N {
for j in 0..N {
self.vertical[j][i] = self.horizontal[i][j];
}
}
self.set_rowdata();
}
pub fn set_grid_from_strings<S: AsRef<str>>(&mut self, grid: &[S]) -> Result<(), Error> {
self.board = Grid::from_strings(grid)?;
Ok(())
}
pub fn with_grid_from_strings<S: AsRef<str>>(mut self, grid: &[S]) -> Result<Board<'a>, Error> {
self.set_grid_from_strings(grid)?;
Ok(self)
}
pub fn wordlist(&self) -> &Wordlist {
&self.wordlist
}
pub fn horizontal(&self) -> State {
self.horizontal
}
pub fn vertical(&self) -> State {
self.vertical
}
pub fn grid(&self) -> Grid {
self.board.clone()
}
pub fn tileset(&self) -> &'a TileSet {
&self.tileset
}
pub fn is_occupied(&self, x: usize, y: usize) -> bool {
if (x < N) && (y < N) {
self.vertical[x][y].tile().is_some()
} else {
false
}
}
fn calc_rowdata(&self, horizontal: bool, i: usize) -> RowData {
let sw = self.surrounding_words(horizontal, i);
let labelsets = sw
.iter()
.map(|surrounding| self.wordlist.get_legal_characters(surrounding))
.collect::<Vec<_>>();
let mut connected = sw
.iter()
.map(|surrounding| !surrounding.is_empty_cell())
.collect::<Vec<_>>();
if i == 7 {
connected[7] = true;
}
let rowdata: RowData = labelsets
.iter()
.zip(connected)
.map(|(&l, c)| (l, c))
.collect();
rowdata
}
fn set_rowdata(&mut self) {
for i in 0..N {
self.rowdata[0][i] = self.calc_rowdata(false, i);
self.rowdata[1][i] = self.calc_rowdata(true, i);
}
}
pub fn play_word(
&mut self,
word: &str,
x: usize,
y: usize,
horizontal: bool,
modify: bool,
) -> Result<String, Error> {
let word = self.encode(word)?;
let used_letters = self.try_word(word, x, y, horizontal)?;
if modify {
self.play_word_unchecked(word, x, y, horizontal);
}
Ok(self.decode(used_letters))
}
fn play_word_unchecked(&mut self, word: Word, x: usize, y: usize, horizontal: bool) {
let mut x = x;
let mut y = y;
let (dx, dy) = if horizontal { (1, 0) } else { (0, 1) };
for tile in word {
self.horizontal[y][x] = tile.into_cell();
x += dx;
y += dy;
}
self.set_state(&self.horizontal.clone());
}
fn try_word(&self, word: Word, x: usize, y: usize, horizontal: bool) -> Result<Letters, Error> {
let mut x = x;
let mut y = y;
let len = word.len();
let (dx, dy) = if horizontal { (1, 0) } else { (0, 1) };
if (x + len * dx > N) || (y + len * dy > N) {
return Err(Error::TilePlacementError {
x,
y,
horizontal,
len,
});
}
let mut used_letters = Letters::new();
for tile in word {
let c = self.horizontal[y][x];
if c.tile().is_none() {
used_letters.push(Letter::from_tile(tile));
} else if c.tile() != Some(tile) {
return Err(Error::TileReplaceError { x, y });
}
x += dx;
y += dy;
}
Ok(used_letters)
}
#[cfg_attr(feature = "flame_it", flame)]
fn surrounding_words(&self, horizontal: bool, i: usize) -> Vec<Row> {
let mut res = Vec::new();
let crossing_rows = if horizontal {
self.vertical
} else {
self.horizontal
};
for row in &crossing_rows {
res.push(row.surrounding_word(i));
}
res
}
pub fn calc_word_points(
&self,
word: &Word,
x: usize,
y: usize,
horizontal: bool,
include_crossing_words: bool,
) -> Result<u32, Error> {
let (dx, dy) = if horizontal { (1, 0) } else { (0, 1) };
let len = word.len();
if (x + len * dx > N) || (y + len * dy > N) {
return Err(Error::TilePlacementError {
x,
y,
horizontal,
len,
});
}
Ok(self.calc_word_points_unchecked(word, x, y, horizontal, include_crossing_words))
}
fn calc_word_points_unchecked(
&self,
word: &Word,
x0: usize,
y0: usize,
horizontal: bool,
include_crossing_words: bool,
) -> u32 {
let mut word_multiplicator = 1;
let mut word_points = 0;
let mut tiles_used = 0;
let mut total_points = 0;
let (mut x, mut y) = (x0, y0);
let (dx, dy) = if horizontal { (1, 0) } else { (0, 1) };
for tile in word.into_iter() {
let letter = tile.code();
let mut letter_points = self.tileset.points(letter);
if self.horizontal[y][x].tile().is_none() {
tiles_used += 1;
let square_bonus = self.board[y][x];
match square_bonus {
LetterBonus(n) => {
letter_points *= n;
}
WordBonus(n) => {
word_multiplicator *= n;
}
_ => {}
}
if include_crossing_words {
let (crow, ci) = if horizontal {
(self.vertical[x], y)
} else {
(self.horizontal[y], x)
};
let row = crow;
let (s, e) = row.start_end(ci);
if e - s > 1 {
let (cx, cy) = if horizontal { (x, s) } else { (s, y) };
let cword =
Word::from(&row.replace(s, e, Cell::EMPTY, Cell::from_tile(tile)));
total_points +=
self.calc_word_points_unchecked(&cword, cx, cy, !horizontal, false);
}
}
}
word_points += letter_points;
x += dx;
y += dy;
}
total_points += word_points * word_multiplicator;
if tiles_used >= 7 {
total_points += 40;
}
total_points
}
pub fn words(
&self,
row: &Row,
horizontal: bool,
i: usize,
letters: Letters,
) -> Vec<(usize, Word)> {
let rowdata = self.rowdata[horizontal as usize][i];
self.wordlist
.words(&row, &rowdata, &letters, None)
.collect()
}
pub fn calc_all_word_scores<T: TryIntoLetters>(&self, letters: T) -> Result<Vec<Score>, Error> {
let letters = letters.try_into_letters(&self.codec())?;
self.calc_all_word_scores_inner(letters)
}
fn calc_all_word_scores_inner(&self, letters: Letters) -> Result<Vec<Score>, Error> {
let mut scores: Vec<Score> = Vec::new();
let hor_scores = |(i, row)| {
let words = self.words(row, true, i, letters);
let mut scores: Vec<Score> = Vec::new();
for (x, word) in words {
let points = self.calc_word_points_unchecked(&word, x, i, true, true);
scores.push(Score {
x,
y: i,
horizontal: true,
word,
score: points,
});
}
scores
};
let ver_scores = |(i, row)| {
let words = self.words(row, false, i, letters);
let mut scores: Vec<Score> = Vec::new();
for (y, word) in words {
let points = self.calc_word_points_unchecked(&word, i, y, false, true);
scores.push(Score {
x: i,
y,
horizontal: false,
word,
score: points,
});
}
scores
};
{
scores.extend(self.horizontal.iter().enumerate().map(hor_scores).flatten());
scores.extend(self.vertical.iter().enumerate().map(ver_scores).flatten());
}
Ok(scores)
}
pub fn tile_at(&self, y: usize, x: usize) -> Option<Tile> {
if x < N && y < N {
return self.horizontal[y][x].tile();
}
None
}
fn codec(&self) -> &Codec {
&self.tileset.codec()
}
pub fn encode<T: Item>(&self, word: &str) -> Result<ItemList<T>, Error> {
self.wordlist.encode(word)
}
pub fn decode<T: Item>(&self, word: ItemList<T>) -> String {
self.wordlist.decode(word)
}
fn evaluate_opponent_scores(
&self,
letters: Letters,
our_tile_score: u32,
in_endgame: bool,
) -> Result<(u32, bool), Error> {
let mut result = self.calc_all_word_scores(letters)?;
if !in_endgame {
if result.is_empty() {
return Ok((0, false));
}
result.sort_by(|a, b| (b.score).cmp(&a.score));
return Ok((result[0].score, false));
}
let mut scores = vec![0];
let mut exit_flag = false;
for s in result {
let played = self.try_word(s.word, s.x, s.y, s.horizontal)?;
let score = if played.len() == letters.len() {
exit_flag = true;
s.score + our_tile_score
} else {
s.score
};
scores.push(score);
}
scores.sort_by_key(|n| u32::MAX - n);
Ok((scores[0], exit_flag))
}
#[cfg(not(feature = "rayon"))]
pub fn sample_scores(
&mut self,
racks: &[Letters],
our_tile_score: u32,
in_endgame: bool,
) -> Result<Vec<(u32, bool)>, Error> {
let opp_scores = racks
.iter()
.map(|letters| self.evaluate_opponent_scores(letters, our_tile_score, in_endgame))
.collect::<Result<Vec<_>, Error>>();
opp_scores
}
#[cfg(feature = "rayon")]
pub fn sample_scores<T: TryIntoLetters + Copy>(
&self,
racks: &[T],
our_tile_score: u32,
in_endgame: bool,
) -> Result<Vec<(u32, bool)>, Error> {
let racks: Vec<Letters> = racks
.iter()
.map(|&rack| rack.try_into_letters(self.codec()))
.collect::<Result<Vec<_>, _>>()?;
let opp_scores = racks
.par_iter()
.map(|&letters| self.evaluate_opponent_scores(letters, our_tile_score, in_endgame))
.collect::<Result<Vec<_>, Error>>();
opp_scores
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Instant;
type Result<T> = std::result::Result<T, Error>;
const TEST_STATE: &[&str] = &[
" t c f",
" e he o",
" r bis g k",
" u bol te v",
" gepof dimme",
" la vree e",
" qua ene ",
" Spoelen ",
" s a n ",
" c d we ",
" hadden ",
" nu o y ",
" wrat siJzen ",
" k os ",
" zerk g ",
];
fn board_nl<'a>() -> Board<'a> {
Board::new(Language::NL)
}
#[test]
fn test_state() -> Result<()> {
let mut board = board_nl().with_state_from_strings(&TEST_STATE)?;
assert!(board.is_occupied(4, 0));
assert!(!board.is_occupied(0, 0));
assert!(board.is_occupied(14, 4));
board.play_word("ster", 3, 0, true, true)?;
assert_eq!(board.decode(board.horizontal[0]), "...ster...c...f");
Ok(())
}
#[test]
fn test_surrounding_words() -> Result<()> {
let board = board_nl().with_state_from_strings(&TEST_STATE)?;
let sw = board
.surrounding_words(true, 8)
.iter()
.map(|&s| board.decode(s))
.collect::<Vec<String>>();
let expect = [
".", ".", ".", ".", ".", "schut", "plas.", "paddos", "o.", "e.we", "drel.en", "tienen",
"gemeen.", ".", ".",
];
println!("{:?}", expect);
assert_eq!(sw, expect);
Ok(())
}
#[test]
fn test_calc_word_points() -> Result<()> {
let board = board_nl().with_state_from_strings(&TEST_STATE)?;
let word = board.encode("ster")?;
let points = board.calc_word_points(&word, 3, 0, true, true)?;
assert_eq!(7, points);
let word = board.encode("abel")?;
let points = board.calc_word_points(&word, 3, 6, false, true)?;
assert_eq!(32, points);
Ok(())
}
#[test]
fn test_words() -> Result<()> {
let words = &["af", "ja"];
let board = board_nl()
.with_wordlist_from_words(words)?
.with_state_from_strings(&TEST_STATE)?;
let letters: Letters = board.encode("j*")?;
let i = 0;
let horizontal = true;
println!("{:?}", board.rowdata[1][0]);
let words = board.words(&board.horizontal[0], horizontal, i, letters);
assert_eq!(words.len(), 1);
for (x, word) in words {
println!("{} {} {}", x, i, board.decode(word));
}
Ok(())
}
#[test]
fn test_calc_all_word_scores() -> Result<()> {
let words = &[
"af", "ah", "al", "aar", "aas", "be", "bi", "bo", "bar", "bes", "bel",
];
let board = board_nl()
.with_wordlist_from_words(words)?
.with_state_from_strings(&TEST_STATE)?;
let letters = "abel";
let res = board.calc_all_word_scores(letters)?;
let expect: &[(usize, usize, bool, &str, u32)] = &[
(13, 0, true, "af", 5),
(3, 1, true, "be", 5),
(3, 1, true, "bel", 14),
(13, 1, true, "bo", 9),
(2, 2, true, "bar", 14),
(3, 8, true, "bes", 8),
(8, 6, false, "bo", 5),
];
assert_eq!(expect.len(), res.len());
for (&r, &(ex, ey, ehor, eword, escore)) in res.iter().zip(expect) {
assert_eq!(r.x, ex);
assert_eq!(r.y, ey);
assert_eq!(r.horizontal, ehor);
assert_eq!(&board.decode(r.word), eword);
assert_eq!(r.score, escore);
}
Ok(())
}
#[test]
fn test_board() {
let board = board_nl().with_state_from_strings(&TEST_STATE).unwrap();
println!("{}", board);
}
#[test]
fn test_bingo() -> Result<()> {
let board = board_nl();
let word = board.encode("hoentje")?;
let score = board.calc_word_points(&word, 7, 7, true, true)?;
assert_eq!(score, 68);
Ok(())
}
#[test]
fn test_main() -> Result<()> {
let mut board = board_nl().with_wordlist_from_words(&["rust", "rest"])?;
let letters = "rusta";
let scores = board.calc_all_word_scores(letters)?;
for s in scores {
println!(
"{} {} {} {} {}",
s.x,
s.y,
s.horizontal,
board.decode(s.word),
s.score
);
}
board.play_word("rust", 7, 7, true, true)?;
println!("{}", board);
Ok(())
}
#[test]
#[should_panic(expected = "TileReplaceError { x: 7, y: 7 }")]
fn test_tile_replace_error() {
let mut board = Board::default();
board.play_word("rust", 7, 7, true, true).unwrap();
board.play_word("bar", 7, 6, false, true).unwrap();
}
#[test]
#[should_panic(expected = "TilePlacementError { x: 12, y: 7, horizontal: true, len: 4 }")]
fn test_tile_placement_error() {
let mut board = Board::default();
board.play_word("rust", 12, 7, true, true).unwrap();
}
#[test]
#[ignore]
fn test_sample_scores() -> Result<()> {
let board = board_nl()
.with_wordlist_from_file("../wordlists/wordlist-nl.txt")?
.with_state_from_strings(&TEST_STATE)?;
let racks: Vec<Letters> = [
"ddenuvw", "eeijkvy", "abeinuy", "ceeehtv", "*bjjoov", "bbeeotu", "eghknrt", "ddnosuw",
"aaadeln", "abeinnz", "aceeiin", "deilnoz", "eeeimmo", "eefioou", "aefnnnt", "*ejlmrr",
"addeent", "adgimno", "emprstt", "bceekpy",
]
.iter()
.map(|&s| board.encode(s).unwrap())
.collect();
let now = Instant::now();
let opp_scores = board.sample_scores(&racks, 0, false)?;
println!("took {:?}", now.elapsed());
println!("{:?}", opp_scores);
Ok(())
}
}