rshogi-core 0.2.4

A high-performance shogi engine core library with NNUE evaluation
Documentation
// 1手詰め探索モジュール
// YaneuraOuのmate1ply_without_effect.cppの移植

pub mod drop_mate;
pub mod helpers;
pub mod move_mate;
pub mod tables;

use crate::bitboard::{
    Bitboard, RANK_BB, bishop_effect, king_effect, lance_effect, line_bb, rook_effect,
};
use crate::position::Position;
use crate::types::{Color, Move, Square};

/// 成りが選択肢に入るか
#[inline]
pub fn can_promote(c: Color, from: Square, to: Square) -> bool {
    let enemy = enemy_field(c);
    enemy.contains(from) || enemy.contains(to)
}

/// 移動先が敵陣かどうか
#[inline]
pub fn can_promote_to(c: Color, to: Square) -> bool {
    enemy_field(c).contains(to)
}

/// 3点が一直線上に並ぶか
#[inline]
pub fn aligned(s1: Square, s2: Square, s3: Square) -> bool {
    let line = line_bb(s1, s2);
    line.contains(s3)
}

/// 盤上の駒を考慮しない飛車の利き
#[inline]
pub fn rook_step_effect(sq: Square) -> Bitboard {
    rook_effect(sq, Bitboard::EMPTY)
}

/// 盤上の駒を考慮しない角の利き
#[inline]
pub fn bishop_step_effect(sq: Square) -> Bitboard {
    bishop_effect(sq, Bitboard::EMPTY)
}

/// 盤上の駒を考慮しない香の利き
#[inline]
pub fn lance_step_effect(us: Color, sq: Square) -> Bitboard {
    lance_effect(us, sq, Bitboard::EMPTY)
}

/// 斜め1ステップの利き
#[inline]
pub fn cross45_step_effect(sq: Square) -> Bitboard {
    bishop_step_effect(sq) & king_effect(sq)
}

/// 盤上の駒を考慮しない女王の利き
#[inline]
pub fn queen_step_effect(sq: Square) -> Bitboard {
    rook_step_effect(sq) | bishop_step_effect(sq)
}

/// 敵陣(成りが可能な段)
#[inline]
fn enemy_field(us: Color) -> Bitboard {
    match us {
        Color::Black => RANK_BB[0] | RANK_BB[1] | RANK_BB[2],
        Color::White => RANK_BB[6] | RANK_BB[7] | RANK_BB[8],
    }
}

/// 1手詰め判定(簡易版)
///
/// 王手がかかっていない局面で1手詰めかどうかを判定する。
/// 高速化のためのテーブルを利用し、やねうら王の簡易版ロジックに準拠する。
pub fn mate_1ply(pos: &mut Position) -> Option<Move> {
    // 王手がかかっている局面では判定しない
    if pos.in_check() {
        return None;
    }

    let us = pos.side_to_move();
    if let Some(mv) = drop_mate::check_drop_mate(pos, us) {
        return Some(mv);
    }

    if let Some(mv) = move_mate::check_move_mate(pos, us) {
        return Some(mv);
    }

    None
}

/// 1手詰め判定の初期化
///
/// CHECK_CAND_BB、CHECK_AROUND_BB、NEXT_SQUAREテーブルを初期化する。
/// この関数は起動時に一度だけ呼ばれる。
pub fn init() {
    // LazyLockを使用するため、最初のアクセス時に自動的に初期化される
    let _ = &*tables::CHECK_CAND_BB;
    let _ = &*tables::CHECK_AROUND_BB;
    let _ = &*tables::NEXT_SQUARE;
}

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

    #[test]
    fn test_module_structure() {
        // モジュールが正しく構成されているかの基本テスト
        init();
    }

    #[test]
    fn test_aligned() {
        let s1 = Square::SQ_55;
        let s2 = Square::new(crate::types::File::File5, crate::types::Rank::Rank1);
        let s3 = Square::new(crate::types::File::File5, crate::types::Rank::Rank9);
        assert!(aligned(s1, s2, s3));
        let other = Square::new(crate::types::File::File4, crate::types::Rank::Rank4);
        assert!(!aligned(s1, s2, other));
    }

    #[test]
    fn test_can_promote() {
        let from = Square::new(crate::types::File::File5, crate::types::Rank::Rank3);
        let to = Square::new(crate::types::File::File5, crate::types::Rank::Rank4);
        assert!(can_promote(Color::Black, from, to));
        assert!(can_promote_to(Color::White, from.inverse()));
    }

    fn mate_by_new(sfen: &str) -> Option<Move> {
        let mut pos = Position::new();
        pos.set_sfen(sfen).unwrap();
        super::mate_1ply(&mut pos)
    }

    #[test]
    fn test_hirate_no_mate() {
        let sfen = crate::position::SFEN_HIRATE;
        assert_eq!(mate_by_new(sfen), None);
    }

    #[test]
    fn test_drop_mate_gold_corner() {
        // 白玉1一、先手玉5九、先手: 飛3二・歩2一、持ち駒: 金
        // 1二に金打ちで詰み
        let sfen = "7Pk/6R2/9/9/9/9/9/9/4K4 b G 1";
        let new_mv = mate_by_new(sfen);
        assert!(new_mv.is_some());
    }

    #[test]
    fn test_move_mate_gold_like_2hop() {
        // 後手番、7gのと金(ProPawn)が6hの金を取って王手→詰み
        // check_cand_bb の Gold 2-hop 計算がないと検出できない
        let sfen = "ln1gk2nl/1rs6/2pppp+R+B1/p7p/9/2P5P/P1+pP+bPP2/3G2S2/LN2KG1NL w GSs5p 38";
        let mv = mate_by_new(sfen);
        assert!(mv.is_some(), "mate_1ply should find 7g6h mate");
        assert_eq!(mv.unwrap().to_usi(), "7g6h");
    }

    #[test]
    fn test_dragon_move_not_false_mate_when_avoid_is_pinner_square() {
        // 回帰テスト:
        // DRAGON近接王手判定で new_pin = pinned_pieces_excluding(them, from) を使う局面。
        // avoid=from が pinner 候補から除外されないと、受け側の駒が偽pinとなり
        // can_piece_capture が誤って失敗して偽の1手詰みを返しうる。
        let sfen = "4k4/4g4/4+R1S2/9/9/9/9/9/K8 b - 1";
        let mv = mate_by_new(sfen);
        assert!(mv.is_none(), "この局面は受けがあるため mate_1ply は None であるべき");
    }

    #[test]
    fn test_knight_promotion_not_false_mate_when_unprotected() {
        // 回帰テスト: 桂成りで金の動きの王手をかけるとき、
        // 成った駒が他の味方駒に守られていなければ玉が取れるため詰みではない。
        // has_other_attacker チェック欠落のバグ修正確認。
        //
        // 例: 先手桂が2七にいて、4一の白玉に3一成りで王手 → 他に利きなし → 玉が取れる
        let sfen = "4k4/9/9/9/9/9/1N7/9/4K4 b - 1";
        let mv = mate_by_new(sfen);
        assert!(
            mv.is_none(),
            "桂成りが保護されていない場合、玉が取れるので詰みではない: {:?}",
            mv
        );
    }

    #[test]
    fn test_knight_promotion_mate_when_protected() {
        // 桂成りで金の動きの王手をかけるとき、
        // 成った駒が他の味方駒に守られていれば玉は取れないので詰みの可能性がある。
        // 具体的な詰み局面:先手桂3三→2一成り(金利きで1一の玉に王手)、
        // 先手飛2二で2一を守る
        //   - 2一: 成桂(飛で守られている → 玉は取れない)
        //   - 1二: 飛2二の横利き
        //   - 2二: 飛がいる
        let sfen = "8k/7R1/6N2/9/9/9/9/9/4K4 b - 1";
        let mv = mate_by_new(sfen);
        assert!(
            mv.is_some(),
            "飛2二が2一を守っており退路も塞がっているため桂成りで詰み: {:?}",
            mv
        );
    }

    #[test]
    fn test_lance_nopro_skewer_fallback_after_promote_escape() {
        // 成り王手(成香)では玉に逃げられるが、不成り串刺しで詰む局面。
        // 香7五→7七不成で7八玉に串刺し王手。
        let sfen =
            "l2+R3nl/3s1kg2/3pppsp1/p1p3p1p/2lS3P1/P4PP1P/1PNPP1N2/2K1g1SR1/+b4G2L w BGN2p 46";
        let mv = mate_by_new(sfen);
        assert!(mv.is_some(), "成香では玉に逃げられるが不成り串刺しで1手詰み: {:?}", mv);
    }
}