eschac 0.2.0

computing chess moves
Documentation
use eschac::board::*;
use eschac::position::*;
use eschac::san::*;
use eschac::setup::*;

static P1: &'static str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -";
static P2: &'static str = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -";
static P3: &'static str = "8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -";
static P4: &'static str = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq -";
static P5: &'static str = "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ -";
static P6: &'static str = "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - -";

fn recursive_check_aux(position: Position, depth: usize) {
    assert_eq!(
        position,
        Setup::from_text_record(&position.as_setup().to_text_record())
            .unwrap()
            .into_position()
            .unwrap(),
    );

    if let Some(passed) = position.pass() {
        let mut position = position.clone();
        position.remove_en_passant_target_square();
        assert_eq!(position, passed.pass().unwrap());
    }

    let computed_mirror = {
        let mut setup = Setup::new();
        for square in Square::all() {
            setup.set(
                square.mirror(),
                position.as_setup().get(square).map(|piece| Piece {
                    role: piece.role,
                    color: !piece.color,
                }),
            );
        }
        setup.set_turn(!position.as_setup().turn());
        for color in Color::all() {
            for side in CastlingSide::all() {
                setup.set_castling_rights(
                    !color,
                    side,
                    position.as_setup().castling_rights(color, side),
                );
            }
        }
        setup.set_en_passant_target_square(
            position
                .as_setup()
                .en_passant_target_square()
                .map(|square| square.mirror()),
        );
        setup.into_position().unwrap()
    };
    let expected_mirror = position.mirror();
    assert_eq!(computed_mirror, expected_mirror);
    assert_eq!(expected_mirror.mirror(), position);

    match depth.checked_sub(1) {
        None => (),
        Some(depth) => {
            let moves = position.legal_moves();
            assert_eq!(moves.len(), position.count_legal_moves());
            assert_eq!(moves.is_check(), position.is_check());
            assert_eq!(moves.is_mate(), position.is_mate());
            assert_eq!(moves.en_passant_is_legal(), position.en_passant_is_legal());
            moves.into_iter().for_each(|m| {
                let uci = m.to_uci();
                assert_eq!(uci, uci.to_move(&position).unwrap().to_uci());
                let san: San = m.to_san();
                match san.to_move(&position) {
                    Ok(m) => assert_eq!(san, m.to_san()),
                    Err(err) => {
                        panic!("{san} is {err} on {position:?}")
                    }
                };
                recursive_check_aux(m.make(), depth)
            });
        }
    }
}
fn recursive_check(record: &str) {
    recursive_check_aux(
        Setup::from_text_record(record)
            .unwrap()
            .into_position()
            .unwrap(),
        4,
    );
}
#[test]
fn recursive_check_1() {
    recursive_check(P1);
}
#[test]
fn recursive_check_2() {
    recursive_check(P2);
}
#[test]
fn recursive_check_3() {
    recursive_check(P3);
}
#[test]
fn recursive_check_4() {
    recursive_check(P4);
}
#[test]
fn recursive_check_5() {
    recursive_check(P5);
}
#[test]
fn recursive_check_6() {
    recursive_check(P6);
}

#[test]
fn setup() {
    assert_eq!(Position::new().as_setup().to_text_record(), P1);
    assert_eq!(Setup::new().to_text_record(), "8/8/8/8/8/8/8/8 w - -");
    assert_eq!(
        Setup::from_text_record("8/8/8/8/1Pp5/8/R1k5/K7 w - b3")
            .unwrap()
            .to_text_record(),
        "8/8/8/8/1Pp5/8/R1k5/K7 w - b3",
    );

    for (record, byte) in [
        ("", 0),
        (" w - -", 1),
        ("8/8/8/8/8/8/8 w - -", 14),
        ("1/1/1/1/1/1/1/1 w - -", 2),
        ("44/44/44/44/44/44/44/44 w - -", 2),
        ("8/8/8/8/8/8/8/8/8 w - -", 16),
        ("p8/8/8/8/8/8/8/8 w - -", 2),
        ("8/8/8/8/8/8/8/8 - - - ", 17),
        ("8/8/8/8/8/8/8/8 w QQQQ -", 20),
    ] {
        let res = Setup::from_text_record(record);
        assert!(res.is_err());
        assert_eq!(res.unwrap_err().byte, byte, "{record}");
    }
    for (record, reason) in [
        (
            "8/8/8/8/8/8/8/8 w KQkq -",
            IllegalPositionReason::MissingKing,
        ),
        (
            "3kk3/8/8/8/8/8/8/3KK3 w KQkq -",
            IllegalPositionReason::TooManyKings,
        ),
        (
            "4k3/8/8/1Q6/8/8/8/4K3 w - -",
            IllegalPositionReason::HangingKing,
        ),
        (
            "4k3/8/3N4/8/8/8/8/4K3 w - -",
            IllegalPositionReason::HangingKing,
        ),
        (
            "4k3/8/8/8/8/8/8/4K2R w KQ -",
            IllegalPositionReason::InvalidCastlingRights,
        ),
        (
            "4k3/8/8/8/8/8/8/P3K3 w KQ -",
            IllegalPositionReason::PawnOnBackRank,
        ),
        (
            "p3k3/8/8/8/8/8/8/4K3 w KQ -",
            IllegalPositionReason::PawnOnBackRank,
        ),
        (
            "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR b KQkq e3",
            IllegalPositionReason::InvalidEnPassant,
        ),
        (
            "rnbqkbnr/pppppppp/8/8/8/4P3/PPPP1PPP/RNBQKBNR b KQkq e3",
            IllegalPositionReason::InvalidEnPassant,
        ),
        (
            "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e3",
            IllegalPositionReason::InvalidEnPassant,
        ),
        (
            "8/8/8/5B2/4P3/3k4/8/4K3 b - e3",
            IllegalPositionReason::ImpossibleEnPassantPin,
        ),
        (
            "8/8/8/3k4/4P3/5B2/8/4K3 b - e3",
            IllegalPositionReason::ImpossibleEnPassantPin,
        ),
        (
            "rnbqkbnr/pppppppp/8/8/4B3/8/PPPPPPPP/RNBQKBNR b KQkq -",
            IllegalPositionReason::TooMuchMaterial,
        ),
        (
            "rnbqkbnr/pppppppp/8/8/8/QBNP4/PPPPPPP1/RNBQKBNR b KQkq -",
            IllegalPositionReason::TooMuchMaterial,
        ),
    ] {
        assert!(
            Setup::from_text_record(record).map(|setup| setup.to_text_record())
                == Ok(record.to_string()),
            "{record}",
        );
        assert!(
            Setup::from_text_record(record)
                .unwrap()
                .into_position()
                .is_err_and(|e| e.reasons.contains(reason)),
            "{record} should be invalid because of {reason:?}",
        );
    }

    for record in [
        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPB/RNBQKBNR b KQkq -",
        "rnbqkbnr/pppppppp/8/8/8/8/NNNNNNNN/RNBQKBNR b KQkq -",
        "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3",
        "3kr3/8/8/8/4P3/8/8/4K3 b - e3",
        "8/8/8/3k4/3P4/8/8/3RK3 b - d3",
    ] {
        assert!(Setup::from_text_record(record).is_ok(), "{record}");
    }

    assert_eq!(
        Setup::from_text_record(
            "p1p1p1p1/p1p1p1p1/p1p1p1p1/p1p1p1p1/p1p1p1p1/p1p1p1p1/p1p1p1p1/p1p1p1p1 w KQkq a1"
        )
        .unwrap()
        .to_text_record()
        .len(),
        81
    );
}

#[test]
fn mirror() {
    assert_eq!(Position::new().pass(), Some(Position::new().mirror()));
    let position = Setup::from_text_record("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b Kq e3")
        .unwrap()
        .into_position()
        .unwrap();
    let mirror = Setup::from_text_record("rnbqkbnr/pppp1ppp/8/4p3/8/8/PPPPPPPP/RNBQKBNR w Qk e6")
        .unwrap()
        .into_position()
        .unwrap();
    assert_eq!(mirror, position.mirror());
}

fn perft_aux(record: &str, tests: &[u128]) {
    let position = Setup::from_text_record(record)
        .unwrap()
        .into_position()
        .unwrap();
    for (depth, value) in tests.iter().copied().enumerate() {
        assert_eq!(
            position.perft(depth),
            value,
            "\"{record}\" at depth {depth}",
        );
    }
}
#[test]
fn perft_1() {
    perft_aux(P1, &[1, 20, 400, 8_902, 197_281, 4_865_609, 119_060_324]);
}
#[test]
fn perft_2() {
    perft_aux(P2, &[1, 48, 2039, 97_862, 4_085_603, 193_690_690]);
}
#[test]
fn perft_3() {
    perft_aux(
        P3,
        &[1, 14, 191, 2_812, 43_238, 674_624, 11_030_083, 178_633_661],
    );
}
#[test]
fn perft_4() {
    perft_aux(P4, &[1, 6, 264, 9_467, 422_333, 15_833_292]);
}
#[test]
fn perft_5() {
    perft_aux(P5, &[1, 44, 1_486, 62_379, 2_103_487, 89_941_194]);
}
#[test]
fn perft_6() {
    perft_aux(P6, &[1, 46, 2_079, 89_890, 3_894_594, 164_075_551]);
}

#[test]
fn san() {
    let position = Setup::from_text_record("8/2KN1p2/5p2/3N1B1k/5PNp/7P/7P/8 w - -")
        .unwrap()
        .into_position()
        .unwrap();
    let san1 = "N7xf6#".parse::<San>().unwrap();
    let m1 = san1.to_move(&position).unwrap();
    let san2 = "N5xf6#".parse::<San>().unwrap();
    let m2 = san2.to_move(&position).unwrap();
    let san3 = "Ngxf6+".parse::<San>().unwrap();
    let m3 = san3.to_move(&position).unwrap();
    assert_eq!(m1.to_san(), san1);
    assert_eq!(m2.to_san(), san2);
    assert_eq!(m3.to_san(), san3);
    assert_eq!(
        "Nd7f6"
            .parse::<San>()
            .unwrap()
            .to_move(&position)
            .unwrap()
            .to_san(),
        san1,
    );
    assert_eq!(
        "Nf6".parse::<San>().unwrap().to_move(&position).map(|_| ()),
        Err(InvalidSan::Ambiguous),
    );
}

#[test]
fn max_legal_moves() {
    let position = Setup::from_text_record("R6R/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q2/pp1Q4/kBNN1KB1 w - -")
        .unwrap()
        .into_position()
        .unwrap();
    assert_eq!(position.count_legal_moves(), 218);
    assert_eq!(position.legal_moves().len(), 218);
    assert_eq!(position.perft(2), 99);
}