arimaa-engine 0.1.0

A bitboard-based Arimaa game engine: move generation, game state, and Zobrist hashing.
Documentation
use itertools::Itertools;
use lazy_static::lazy_static;
use std::convert::TryFrom;
use std::iter::repeat_with;
use tinyvec::*;

use super::{Direction, MoveDirection, Path};

pub type PathPermutations = ArrayVec<Paths>;
type Paths = [(ArrayVec<[Direction; 4]>, Path); 6];

type DirectionsAndPaths = Vec<DirectionsAndPath>;
type DirectionsAndPath = (MoveDirection, PathPermutations);

lazy_static! {
    static ref PATH_PERMUTATIONS_1: Vec<PathPermutations> = get_path_permutations_inner(1).into_iter().map(|(_, path)| path).collect();
    static ref PATH_PERMUTATIONS_2: Vec<PathPermutations> = get_path_permutations_inner(2).into_iter().map(|(_, path)| path).collect();
    static ref PATH_PERMUTATIONS_3: Vec<PathPermutations> = get_path_permutations_inner(3).into_iter().map(|(_, path)| path).collect();
    static ref PATH_PERMUTATIONS_4: Vec<PathPermutations> = get_path_permutations_inner(4).into_iter().map(|(_, path)| path).collect();
    static ref PATH_PERMUTATIONS_EXACT_STEPS_1: Vec<PathPermutations> = get_path_permutations_with_exact_steps_inner(1);
    static ref PATH_PERMUTATIONS_EXACT_STEPS_2: Vec<PathPermutations> = get_path_permutations_with_exact_steps_inner(2);
    static ref PATH_PERMUTATIONS_EXACT_STEPS_3: Vec<PathPermutations> = get_path_permutations_with_exact_steps_inner(3);
    static ref PATH_PERMUTATIONS_EXACT_STEPS_4: Vec<PathPermutations> = get_path_permutations_with_exact_steps_inner(4);
    static ref PATH_PERMUTATIONS_BY_MOVE_DIRECTION: Vec<(MoveDirection, ArrayVec<[Path; 6]>)> = get_path_permutations_by_move_direction_inner();
}

pub fn get_path_permutations(num_steps: usize) -> &'static [PathPermutations] {
    match num_steps {
        1 => &PATH_PERMUTATIONS_1,
        2 => &PATH_PERMUTATIONS_2,
        3 => &PATH_PERMUTATIONS_3,
        4 => &PATH_PERMUTATIONS_4,
        _ => panic!("No matching move direction for {} steps.", num_steps),
    }
}

pub fn get_path_permutations_with_exact_steps(exact_num_steps: usize) -> &'static [PathPermutations] {
    match exact_num_steps {
        1 => &PATH_PERMUTATIONS_EXACT_STEPS_1,
        2 => &PATH_PERMUTATIONS_EXACT_STEPS_2,
        3 => &PATH_PERMUTATIONS_EXACT_STEPS_3,
        4 => &PATH_PERMUTATIONS_EXACT_STEPS_4,
        _ => panic!("No matching move direction for {} steps.", exact_num_steps),
    }
}

pub fn get_path_permutations_by_move_direction() -> &'static [(MoveDirection, ArrayVec<[Path; 6]>)] {
    &PATH_PERMUTATIONS_BY_MOVE_DIRECTION
}

fn get_path_permutations_by_move_direction_inner() -> Vec<(MoveDirection, ArrayVec<[Path; 6]>)> {
    let perms = get_path_permutations_inner(4);

    MoveDirection::move_directions()
        .into_iter()
        .map(|dir| perms.iter().find(|(d, _a)| dir == *d).expect("Should always contain a matching move direction"))
        .map(|(dir, a)| (*dir, a.iter().map(|(_, path)| *path).collect::<ArrayVec<_>>()))
        .collect()
}

fn get_path_permutations_with_exact_steps_inner(exact_num_steps: usize) -> Vec<PathPermutations> {
    get_path_permutations(exact_num_steps)
        .iter()
        .map(|moves| moves.iter().filter(|(steps, _)| steps.len() == exact_num_steps).cloned().collect())
        .filter(|moves: &PathPermutations| !moves.is_empty())
        .collect()
}

fn get_path_permutations_inner(num_steps: usize) -> DirectionsAndPaths {
    let dirs = [Direction::Up, Direction::Right, Direction::Down, Direction::Left];
    let prods = |n: usize| repeat_with(|| dirs.iter().cloned()).take(n).multi_cartesian_product();

    let dir_products = (1..=num_steps).flat_map(prods).filter(|p| !has_redundant_directions(p.as_slice()));

    let pairs = dir_products.map(|dirs| (MoveDirection::try_from(dirs.as_slice()).unwrap(), dirs));

    pairs.fold(Vec::<_>::new(), |mut map, (k, dirs)| {
        let entry = map.iter_mut().find(|(key, _)| *key == k);
        if entry.is_none() {
            map.push((k, ArrayVec::new()));
        }
        let entry = map.iter_mut().find(|(key, _)| *key == k).unwrap();

        entry.1.push((ArrayVec::try_from(dirs.as_slice()).unwrap(), Path::from(dirs.as_slice())));
        map
    })
}

fn has_redundant_directions(dirs: &[Direction]) -> bool {
    // For dirs with a 2 step action, subsequent duplicates should be exluded. I.E. (E W)
    // Prevent a 3 step action that could occur using 1 step. I.E. (E N W) -> N
    // For dirs with a length of 4, check for subsequent dirs being inverses. I.E. (E W S S) -> S S
    for window in (2..=3).flat_map(|len| dirs.windows(len)) {
        if contains_opposite_dirs(window) {
            return true;
        }
    }

    false
}

fn contains_opposite_dirs(dirs: &[Direction]) -> bool {
    if dirs.contains(&Direction::Up) && dirs.contains(&Direction::Down) {
        return true;
    }

    if dirs.contains(&Direction::Right) && dirs.contains(&Direction::Left) {
        return true;
    }

    false
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_verify_number_of_permutations_by_step() {
        let get_sum_of_all_path_permutations = |paths: &[PathPermutations]| -> usize { paths.iter().map(|path| path.len()).sum() };

        let perms = get_path_permutations(1);
        assert_eq!(perms.len(), 4);
        assert_eq!(get_sum_of_all_path_permutations(perms), 4);

        let perms = get_path_permutations(2);
        assert_eq!(perms.len(), 12);
        assert_eq!(get_sum_of_all_path_permutations(perms), 16);

        let perms = get_path_permutations(3);
        assert_eq!(perms.len(), 24);
        assert_eq!(get_sum_of_all_path_permutations(perms), 44);

        let perms = get_path_permutations(4);
        assert_eq!(perms.len(), 40);
        assert_eq!(get_sum_of_all_path_permutations(perms), 112);
    }

    #[test]
    fn test_verify_number_of_permutations_exact_step() {
        let get_sum_of_all_path_permutations = |paths: &[PathPermutations]| -> usize { paths.iter().map(|path| path.len()).sum() };

        let perms = get_path_permutations_with_exact_steps(1);
        assert_eq!(perms.len(), 4);
        assert_eq!(get_sum_of_all_path_permutations(perms), 4);

        let perms = get_path_permutations_with_exact_steps(2);
        assert_eq!(perms.len(), 8);
        assert_eq!(get_sum_of_all_path_permutations(perms), 12);

        let perms = get_path_permutations_with_exact_steps(3);
        assert_eq!(perms.len(), 12);
        assert_eq!(get_sum_of_all_path_permutations(perms), 28);

        let perms = get_path_permutations_with_exact_steps(4);
        assert_eq!(perms.len(), 20);
        assert_eq!(get_sum_of_all_path_permutations(perms), 68);
    }
}