ai-connect4 0.1.1

A compact Connect Four rules and alpha-beta search engine
Documentation
use ai_connect4::verification::{ReferencePosition, reference_perft};
use ai_connect4::{Cell, Column, GameResult, Move, MoveError, Position, Side, perft};
use std::collections::HashMap;

fn moves(notation: &[&str]) -> Vec<Move> {
    notation
        .iter()
        .map(|value| value.parse().expect("valid test move"))
        .collect()
}

#[test]
fn notation_and_center_first_order_are_stable() {
    for (index, notation) in ["a", "b", "c", "d", "e", "f", "g"].iter().enumerate() {
        let column: Column = notation.parse().unwrap();
        assert_eq!(column.index(), index);
        assert_eq!(column.to_string(), *notation);
        assert_eq!((index + 1).to_string().parse(), Ok(column));
    }
    let order: Vec<String> = Position::start()
        .legal_moves()
        .into_iter()
        .map(|mv| mv.to_string())
        .collect();
    assert_eq!(order, ["d", "c", "e", "b", "f", "a", "g"]);
}

#[test]
fn the_addition_trick_finds_one_playable_cell_per_column() {
    let position = Position::start();
    assert_eq!(position.playable_bits().count_ones(), 7);
    for column in Column::all() {
        let cell = Cell::new(column, 0);
        assert_ne!(position.playable_bits() & (1u64 << cell.bit_index()), 0);
    }
}

#[test]
fn full_columns_are_rejected_and_every_move_unmakes_exactly() {
    let mut position = Position::start();
    let start = position;
    let mut undos = Vec::new();
    for _ in 0..6 {
        undos.push(position.make_move("a".parse().unwrap()).unwrap());
    }
    assert!(!position.can_play(Column::new(0)));
    assert_eq!(
        position.make_move("a".parse().unwrap()),
        Err(MoveError::Full(Column::new(0)))
    );
    while let Some(undo) = undos.pop() {
        position.unmake_move(undo);
    }
    assert_eq!(position, start);
}

#[test]
fn vertical_horizontal_and_both_diagonal_wins_are_detected() {
    let vertical = Position::from_moves(&moves(&["a", "b", "a", "b", "a", "b", "a"])).unwrap();
    assert_eq!(vertical.result(), GameResult::Win(Side::Red));
    assert_eq!(
        vertical.winning_cells(),
        moves(&["a", "a", "a", "a"])
            .iter()
            .enumerate()
            .map(|(row, mv)| Cell::new(mv.column(), row as u8))
            .collect::<Vec<_>>()
    );

    let horizontal = Position::from_moves(&moves(&["a", "a", "b", "b", "c", "c", "d"])).unwrap();
    assert_eq!(horizontal.result(), GameResult::Win(Side::Red));

    let rising = Position::from_moves(&moves(&[
        "a", "b", "b", "c", "c", "d", "c", "d", "d", "g", "d",
    ]))
    .unwrap();
    assert_eq!(rising.result(), GameResult::Win(Side::Red));
    let falling = Position::from_moves(
        &moves(&["a", "b", "b", "c", "c", "d", "c", "d", "d", "g", "d"])
            .into_iter()
            .map(Move::mirrored)
            .collect::<Vec<_>>(),
    )
    .unwrap();
    assert_eq!(falling.result(), GameResult::Win(Side::Red));
}

#[test]
fn mirror_is_an_involution_and_preserves_the_result() {
    let position = Position::from_moves(&moves(&["d", "c", "d", "e", "b", "f", "a"])).unwrap();
    let mirror = position.mirrored();
    assert_eq!(mirror.mirrored(), position);
    assert_eq!(mirror.result(), position.result());
    for column in Column::all() {
        for row in 0..Cell::ROWS as u8 {
            let cell = Cell::new(column, row);
            assert_eq!(position.side_at(cell), mirror.side_at(cell.mirrored()));
        }
    }
}

#[test]
fn perft_signatures_lock_move_generation_and_terminal_stopping() {
    let expected = [1u64, 7, 49, 343, 2_401, 16_807, 117_649, 823_536, 5_673_234];
    let mut position = Position::start();
    let mut reference = ReferencePosition::start();
    for (depth, nodes) in expected.into_iter().enumerate() {
        assert_eq!(perft(&mut position, depth as u8), nodes, "depth {depth}");
        assert_eq!(
            reference_perft(&mut reference, depth as u8),
            nodes,
            "reference depth {depth}"
        );
        assert_eq!(position, Position::start());
        assert_eq!(reference, ReferencePosition::start());
    }
}

#[test]
fn bitboard_matches_the_reference_tree() {
    fn compare(fast: &mut Position, reference: &mut ReferencePosition, depth: u8) {
        fn compare_with_keys(
            fast: &mut Position,
            reference: &mut ReferencePosition,
            depth: u8,
            keys: &mut HashMap<u64, Position>,
        ) {
            if let Some(existing) = keys.insert(fast.key(), *fast) {
                assert_eq!(existing, *fast, "compact key collision");
            }
            assert_eq!(fast.side_to_move(), reference.side_to_move());
            assert_eq!(fast.result(), reference.result());
            assert_eq!(
                fast.legal_moves().as_slice(),
                reference.legal_moves().as_slice()
            );
            for column in Column::all() {
                for row in 0..Cell::ROWS as u8 {
                    let cell = Cell::new(column, row);
                    assert_eq!(fast.side_at(cell), reference.side_at(cell));
                }
            }
            if depth == 0 {
                return;
            }
            let fast_before = *fast;
            let reference_before = *reference;
            for mv in fast.legal_moves() {
                let fast_undo = fast.make_move(mv).unwrap();
                let reference_undo = reference.make_move(mv).unwrap();
                compare_with_keys(fast, reference, depth - 1, keys);
                fast.unmake_move(fast_undo);
                reference.unmake_move(reference_undo);
                assert_eq!(*fast, fast_before);
                assert_eq!(*reference, reference_before);
                assert_eq!(fast.key(), fast_before.key());
            }
        }

        let mut keys = HashMap::new();
        compare_with_keys(fast, reference, depth, &mut keys);
    }

    let mut fast = Position::start();
    let mut reference = ReferencePosition::start();
    compare(&mut fast, &mut reference, 6);
    assert_eq!(perft(&mut fast, 6), reference_perft(&mut reference, 6));
}