rsshogi 1.0.1

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
use crate::board::test_support::move_from_usi_expect;
use crate::board::zobrist::ZobristKey;
use crate::board::{Bitboard, Position};
use crate::types::{Color, Piece, PieceType, Square};

const ZOBRIST_SEED: u64 = 20_151_225;
const PRNG_MULTIPLIER: u64 = 2_685_821_657_736_338_717;

struct Prng {
    state: u64,
}

impl Prng {
    const fn new(seed: u64) -> Self {
        Self { state: seed }
    }

    fn next_u64(&mut self) -> u64 {
        let mut s = self.state;
        s ^= s >> 12;
        s ^= s << 25;
        s ^= s >> 27;
        self.state = s;
        s.wrapping_mul(PRNG_MULTIPLIER)
    }
}

fn next_key(rng: &mut Prng) -> ZobristKey {
    let low = rng.next_u64();
    #[cfg(feature = "hash-128")]
    {
        let high = rng.next_u64();
        let _ = rng.next_u64();
        let _ = rng.next_u64();
        ZobristKey::new(low, high)
    }
    #[cfg(not(feature = "hash-128"))]
    {
        let _ = rng.next_u64();
        let _ = rng.next_u64();
        let _ = rng.next_u64();
        ZobristKey::new(low, 0)
    }
}

fn between_expected(from: Square, to: Square) -> Bitboard {
    let from_file = from.raw() / 9;
    let from_rank = from.raw() % 9;
    let to_file = to.raw() / 9;
    let to_rank = to.raw() % 9;

    let df = to_file - from_file;
    let dr = to_rank - from_rank;
    if df == 0 && dr == 0 {
        return Bitboard::EMPTY;
    }
    if df != 0 && dr != 0 && df.abs() != dr.abs() {
        return Bitboard::EMPTY;
    }

    let steps = df.abs().max(dr.abs());
    if steps <= 1 {
        return Bitboard::EMPTY;
    }

    let file_step = df.signum();
    let rank_step = dr.signum();
    let mut result = Bitboard::EMPTY;
    for i in 1..steps {
        let file = from_file + file_step * i;
        let rank = from_rank + rank_step * i;
        let idx = file * 9 + rank;
        result.set(Square::new(idx));
    }
    result
}

fn line_direction_index(from: Square, to: Square) -> Option<usize> {
    if from == to {
        return Some(0);
    }
    let from_file = from.raw() / 9;
    let from_rank = from.raw() % 9;
    let to_file = to.raw() / 9;
    let to_rank = to.raw() % 9;

    let df = to_file - from_file;
    let dr = to_rank - from_rank;

    if df == 0 {
        return Some(3);
    }
    if dr == 0 {
        return Some(1);
    }
    if df == dr || df == -dr {
        let same_sign = (df > 0 && dr > 0) || (df < 0 && dr < 0);
        return Some(if same_sign { 0 } else { 2 });
    }
    None
}

fn line_from_delta(sq: Square, file_step: i8, rank_step: i8) -> Bitboard {
    let mut line = Bitboard::EMPTY;
    line.set(sq);

    for sign in [-1, 1] {
        let mut file = sq.raw() / 9;
        let mut rank = sq.raw() % 9;
        loop {
            file += file_step * sign;
            rank += rank_step * sign;
            if !(0..9).contains(&file) || !(0..9).contains(&rank) {
                break;
            }
            let idx = file * 9 + rank;
            line.set(Square::new(idx));
        }
    }

    line
}

fn line_expected(from: Square, to: Square) -> Bitboard {
    if from == to {
        return Bitboard::from_square(from);
    }
    let Some(dir) = line_direction_index(from, to) else {
        return Bitboard::EMPTY;
    };
    match dir {
        0 => line_from_delta(from, -1, -1),
        1 => line_from_delta(from, -1, 0),
        2 => line_from_delta(from, -1, 1),
        3 => line_from_delta(from, 0, -1),
        _ => Bitboard::EMPTY,
    }
}

fn assert_state_matches_position(pos: &Position) {
    let stack = pos.state_stack();
    let state = stack.current();
    let keys = pos.compute_keys();
    assert_eq!(state.board_key, keys.board_key);
    assert_eq!(state.hand_key, keys.hand_key);
    assert_eq!(state.hand, pos.hand(pos.turn()));
}

#[test]
fn test_between_line_tables_match_yaneuraou_logic() {
    for from_idx in 0..Square::COUNT {
        let from = Square::from_index(from_idx);
        for to_idx in 0..Square::COUNT {
            let to = Square::from_index(to_idx);
            assert_eq!(Bitboard::between(from, to), between_expected(from, to));
            assert_eq!(Bitboard::line(from, to), line_expected(from, to));
        }
    }
}

#[test]
#[allow(clippy::large_stack_arrays)]
fn test_zobrist_table_matches_yaneuraou_logic() {
    let mut rng = Prng::new(ZOBRIST_SEED);
    let expected_side = next_key(&mut rng);
    let expected_no_pawns = next_key(&mut rng);

    let mut expected_board = [[ZobristKey::default(); Square::COUNT_WITH_NONE]; Piece::COUNT];
    let mut expected_hand = [[ZobristKey::default(); PieceType::HAND_TABLE_SIZE]; Color::COUNT];

    for row in expected_board.iter_mut().take(Piece::COUNT).skip(1) {
        for cell in row.iter_mut().take(Square::COUNT) {
            *cell = next_key(&mut rng);
        }
    }

    for row in expected_hand.iter_mut().take(Color::COUNT) {
        for cell in row.iter_mut().take(PieceType::HAND_TABLE_SIZE).skip(1) {
            *cell = next_key(&mut rng);
        }
    }

    let zobrist = crate::board::zobrist::Zobrist::instance();
    assert_eq!(zobrist.side(), expected_side);
    assert_eq!(zobrist.no_pawns(), expected_no_pawns);

    for (piece, row) in expected_board.iter().enumerate().take(Piece::COUNT) {
        for (sq, cell) in row.iter().enumerate().take(Square::COUNT_WITH_NONE) {
            assert_eq!(zobrist.board_at_index(piece, sq), *cell);
        }
    }

    for (color, row) in expected_hand.iter().enumerate().take(Color::COUNT) {
        for (pt, cell) in row.iter().enumerate().take(PieceType::HAND_TABLE_SIZE) {
            assert_eq!(zobrist.hand_at_index(color, pt), *cell);
        }
    }
}

#[test]
fn test_state_info_matches_position_after_capture() {
    let sfen = "ln1g3nl/1r3kg2/p2pppsp1/3s2p1p/1pp4P1/P1P1SP2P/1PSPP1P2/2GK3R1/LN3G1NL b Bb 1";
    let mut pos = crate::board::position_from_sfen(sfen).expect("valid sfen");
    pos.init_stack();
    assert_state_matches_position(&pos);

    let mv = move_from_usi_expect(&pos, "7f7e");
    pos.apply_move32(mv);
    assert_state_matches_position(&pos);
}

#[test]
fn test_state_info_matches_position_after_drop() {
    let sfen = "4K4/9/9/9/9/9/9/9/4k4 b P 1";
    let mut pos = crate::board::position_from_sfen(sfen).expect("valid sfen");
    pos.init_stack();
    assert_state_matches_position(&pos);

    let mv = move_from_usi_expect(&pos, "P*5e");
    pos.apply_move32(mv);
    assert_state_matches_position(&pos);
}

#[test]
fn test_state_info_matches_position_after_promotion() {
    let sfen = "lnsgkgsnl/1r5b1/2R6/ppppppppp/9/9/PPPPPPPPP/1B7/LNSGKGSNL b - 1";
    let mut pos = crate::board::position_from_sfen(sfen).expect("valid sfen");
    pos.init_stack();
    assert_state_matches_position(&pos);

    let mv = move_from_usi_expect(&pos, "7c7b+");
    pos.apply_move32(mv);
    assert_state_matches_position(&pos);
}

#[test]
fn test_state_info_matches_position_after_null_move() {
    let mut pos = crate::board::hirate_position();
    pos.init_stack();
    assert_state_matches_position(&pos);

    pos.apply_null_move().expect("null move");
    assert_state_matches_position(&pos);

    pos.undo_null_move().expect("undo null move");
    assert_state_matches_position(&pos);
}