rsshogi 1.2.2

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
use rsshogi::board::{
    Move32List, Position, generate_legal_all_move32, hirate_position, move_from_usi,
};
use rsshogi::mate::solve_mate_in_one;
use rsshogi::types::Move32;

const MATE_POSITIONS: &[&str] = &[
    "lnsG5/4g4/prpp1p1pp/1p4p1k/4+B4/2P1P3P/P+b1PSP1L1/4K2SL/2G2G1r1 b SP3nl3p 73",
    "ln2+P2nl/2R1+S1g2/p2p1p1p+B/8p/5+R3/2p3PkP/PP1PPP3/2+bS1KS2/5G1NL b GL4Pgsn 83",
    "l2+R3g1/2ln5/2k1ps+Bp1/2p3P2/p3Sp1P1/7b1/PLPKPP3/1S1G2G2/LN1s+n+r1N1 b G4P3p 103",
    "l2+S3kl/9/3p1pG+S1/ppp3Pp1/4+RP1np/P1n1S1p2/1P1PS1gN1/2G1G4/L2K3RL b 3P2bn2p 115",
    "l1ggk3l/3plsG2/4sp+P2/p2+Bp2pp/5n1+b1/2+RSPN3/Pp1P1P+n1P/3K5/L7+n b RS3Pg2p 117",
    "4K4/2sP1Ps2/9/4r4/9/9/9/9/4k4 w - 1",
];

fn position(sfen: &str) -> Position {
    Position::from_sfen(sfen).unwrap_or_else(|error| panic!("invalid fixture {sfen}: {error}"))
}

fn assert_checkmate_move(pos: &Position, mv: Move32) {
    assert!(pos.is_legal_move32(mv), "illegal candidate: {}", mv.to_usi());
    assert!(pos.gives_check_move32(mv), "candidate is not check: {}", mv.to_usi());

    let mut next = pos.clone();
    next.init_stack();
    next.apply_move32(mv);
    let mut replies = Move32List::new();
    generate_legal_all_move32(&next, &mut replies);
    assert!(replies.is_empty(), "candidate permits a reply: {}", mv.to_usi());
}

fn assert_solver_finds_mate(pos: &Position) -> Move32 {
    let mv = solve_mate_in_one(pos).expect("mate in one must be found");
    assert_checkmate_move(pos, mv);
    mv
}

#[test]
fn finds_rule_valid_mates_for_both_colors_and_drops() {
    for sfen in MATE_POSITIONS {
        assert_solver_finds_mate(&position(sfen));
    }
}

#[test]
fn capture_and_promotion_candidates_are_mates() {
    let capture =
        position("6+S2/ln5gP/n1sg+R1n1+N/1S2ppp2/P2gkP3/3l3P1/1+bPpKB3/s3G1Pp+l/L4r3 w 2P5p 178");
    let capture_move = move_from_usi(&capture, "3c4e").expect("valid capture fixture");
    assert!(!capture.piece_on(capture_move.to_sq()).is_empty());
    assert_checkmate_move(&capture, capture_move);
    assert_solver_finds_mate(&capture);

    let promotion =
        position("6+S2/ln5gP/n1sg+R1n1+N/1S2ppp2/P3kP3/3lB2P1/1+bPpK4/s3G1Pp+l/L4r3 w G2P5p 180");
    let promotion_move = move_from_usi(&promotion, "4i4f+").expect("valid promotion fixture");
    assert!(promotion_move.is_promotion());
    assert_checkmate_move(&promotion, promotion_move);
    assert_solver_finds_mate(&promotion);
}

#[test]
fn returns_none_without_a_mate() {
    assert!(solve_mate_in_one(&hirate_position()).is_none());
    assert!(solve_mate_in_one(&position("4k4/9/4G4/9/9/9/9/9/4K4 b - 1")).is_none());
}

#[test]
fn checking_moves_with_escape_capture_or_interposition_are_not_mates() {
    let cases = [
        ("4k4/9/4G4/9/9/9/9/9/4K4 b - 1", "5c4b", "5a6a"),
        ("4k4/9/4G4/9/9/9/9/9/4K4 b - 1", "5c5b", "5a5b"),
        ("4k4/9/4G4/4R4/9/9/9/9/4K4 b g 1", "5c4c", "G*5b"),
    ];

    for (sfen, checking_usi, reply_usi) in cases {
        let pos = position(sfen);
        let checking = move_from_usi(&pos, checking_usi).expect("valid checking fixture");
        assert!(pos.is_legal_move32(checking));
        assert!(pos.gives_check_move32(checking));

        let mut next = pos.clone();
        next.init_stack();
        next.apply_move32(checking);
        let reply = move_from_usi(&next, reply_usi).expect("valid reply fixture");
        let mut replies = Move32List::new();
        generate_legal_all_move32(&next, &mut replies);
        assert!(replies.iter().any(|&mv| mv == reply));
    }
}