xwords 0.2.0

Tooling to fill crossword puzzles
Documentation
use std::{collections::HashSet, hash::BuildHasherDefault, time::Instant};

use rustc_hash::FxHasher;

use crate::{
    crossword::{Crossword, CrosswordWordIterator},
    parse::parse_word_boundaries,
    trie::Trie,
};

use super::{
    build_lookup,
    cache::{CachedIsViable, CachedWords},
    fill_one_word, is_viable_reuse, orthogonals, Filler,
};

pub struct SimpleFiller<'s> {
    word_cache: CachedWords,
    is_viable_cache: CachedIsViable,

    trie: &'s Trie,
}

impl<'s> SimpleFiller<'s> {
    pub fn new(trie: &'s Trie) -> SimpleFiller<'s> {
        SimpleFiller {
            word_cache: CachedWords::default(),
            is_viable_cache: CachedIsViable::default(),
            trie,
        }
    }
}

impl<'s> Filler for SimpleFiller<'s> {
    fn fill(&mut self, initial_crossword: &Crossword) -> Result<Crossword, String> {
        let thread_start = Instant::now();
        let mut candidate_count = 0;

        let word_boundaries = parse_word_boundaries(&initial_crossword);
        let mut already_used = HashSet::with_capacity_and_hasher(
            word_boundaries.len(),
            BuildHasherDefault::<FxHasher>::default(),
        );

        let mut candidates = vec![initial_crossword.to_owned()];

        let word_boundary_lookup = build_lookup(&word_boundaries);

        while !candidates.is_empty() {
            let candidate = candidates.pop().unwrap();
            candidate_count += 1;

            if candidate_count % 10_000 == 0 {
                println!("{}", candidate);
                println!(
                    "Throughput: {}",
                    candidate_count as f32 / thread_start.elapsed().as_millis() as f32
                );
            }

            let to_fill = word_boundaries
                .iter()
                .map(|word_boundary| CrosswordWordIterator::new(&candidate, word_boundary))
                .filter(|iter| iter.clone().any(|c| c == ' '))
                .min_by_key(|iter| {
                    (
                        self.word_cache.words(iter.clone(), self.trie).len(),
                        iter.word_boundary.start_row,
                        iter.word_boundary.start_col,
                    )
                })
                .unwrap();

            let orthogonals = orthogonals(&to_fill.word_boundary, &word_boundary_lookup);

            let potential_fills = self.word_cache.words(to_fill.clone(), self.trie);

            for potential_fill in potential_fills {
                let new_candidate = fill_one_word(&candidate, &to_fill.clone(), &potential_fill);

                let (viable, tmp) = is_viable_reuse(
                    &new_candidate,
                    &orthogonals,
                    self.trie,
                    already_used,
                    &mut self.is_viable_cache,
                );
                already_used = tmp;
                already_used.clear();

                if viable {
                    if !new_candidate.contents.contains(' ') {
                        return Ok(new_candidate);
                    }
                    candidates.push(new_candidate);
                }
            }
        }

        Err(String::from("We failed"))
    }
}

#[cfg(test)]
mod tests {

    use crate::{fill::Filler, Trie};

    use crate::Crossword;

    use std::{cmp::Ordering, time::Instant};

    use super::SimpleFiller;

    #[test]
    fn test() {
        assert_eq!((1, 2).cmp(&(3, 4)), Ordering::Less)
    }

    #[test]
    fn medium_grid() {
        let grid = Crossword::square(String::from(
            "
    ***
    ***
    ***
       
***    
***    
***    
",
        ))
        .unwrap();

        let now = Instant::now();
        let trie = Trie::load_default().expect("Failed to load trie");
        let mut filler = SimpleFiller::new(&trie);
        let filled_puz = filler.fill(&grid).unwrap();
        println!("Filled in {} seconds.", now.elapsed().as_secs());
        println!("{}", filled_puz);
    }
}