use super::{
CheckClass, MateContext, MoveDelta, attackers_with_delta, classify_check, mate_context,
solve_mate_in_one, solve_mate_in_one_in_place,
};
use crate::board::attack_tables::KING_ATTACKS;
use crate::board::movegen::generate_checks_all_move32;
use crate::board::{Move32List, Position, generate_legal_all_move32};
use crate::types::{Bitboard, Color, Move32};
fn naive_mate_in_one(position: &Position) -> Option<Move32> {
let mut candidates = Move32List::new();
let in_check = position.is_in_check();
if in_check {
generate_legal_all_move32(position, &mut candidates);
} else {
generate_checks_all_move32(position, &mut candidates);
candidates.retain_unordered(|mv| position.is_legal_move32(mv));
}
let mut next = position.clone();
next.init_stack();
for &mv in candidates.iter() {
if in_check && !position.gives_check_move32(mv) {
continue;
}
next.apply_move32_with_gives_check(mv, true);
if next.is_mated() {
return Some(mv);
}
next.undo_move32(mv).expect("oracle must undo the move it just applied");
}
None
}
fn verify_attackers_with_delta(position: &Position, ctx: &MateContext, mv: Move32) {
let delta = MoveDelta::from_move32(mv, ctx.occupied);
let mut next = position.clone();
next.init_stack();
next.apply_move32_with_gives_check(mv, position.gives_check_move32(mv));
assert_eq!(
next.bitboards().occupied(),
delta.occupied_after,
"occupied_after mismatch: sfen={} usi={}",
position.to_sfen(None),
mv.to_usi(),
);
let mut targets = KING_ATTACKS[ctx.their_king];
targets.set(ctx.their_king);
match classify_check(position, ctx, mv) {
Some(CheckClass::Direct(checker) | CheckClass::Discovered(checker)) => {
targets.set(checker);
targets |= Bitboard::between(checker, ctx.their_king);
}
Some(CheckClass::Double) | None => {}
}
let delta_without_king = MoveDelta {
occupied_after: delta.occupied_after.and_not(Bitboard::from_square(ctx.their_king)),
..delta
};
while let Some(sq) = targets.pop_lsb() {
for color in [Color::BLACK, Color::WHITE] {
for probe in [&delta, &delta_without_king] {
let expected = next.attackers_to_color(color, sq, probe.occupied_after);
let actual = attackers_with_delta(position, color, sq, probe);
assert_eq!(
actual,
expected,
"attackers_with_delta mismatch: sfen={} usi={} sq={} color={}",
position.to_sfen(None),
mv.to_usi(),
sq.to_index(),
color.to_index(),
);
}
}
}
}
fn verify_in_place_solver(position: &Position, expected: Option<Move32>, sfen: &str) {
let mut work = position.clone();
work.init_stack();
let entry_sfen = work.to_sfen(None);
let entry_board_key = work.board_key();
let entry_hands = [work.hand(Color::BLACK), work.hand(Color::WHITE)];
let entry_turn = work.turn();
let entry_depth = work.state_stack_depth();
let found = solve_mate_in_one_in_place(&mut work);
assert_eq!(found, expected, "in-place solver diverged from wrapper: sfen={sfen}");
assert_eq!(work.to_sfen(None), entry_sfen, "sfen must be restored: sfen={sfen}");
assert_eq!(work.board_key(), entry_board_key, "board key must be restored: sfen={sfen}");
assert_eq!(
[work.hand(Color::BLACK), work.hand(Color::WHITE)],
entry_hands,
"hands must be restored: sfen={sfen}",
);
assert_eq!(work.turn(), entry_turn, "turn must be restored: sfen={sfen}");
assert_eq!(
work.state_stack_depth(),
entry_depth,
"state stack depth must be restored: sfen={sfen}",
);
}
fn verify_position_oracles(sfen: &str) {
let position = Position::from_sfen(sfen).expect("oracle corpus line must be a valid sfen");
let fast = solve_mate_in_one(&position);
let naive = naive_mate_in_one(&position);
assert_eq!(
fast.is_some(),
naive.is_some(),
"solve_mate_in_one disagreed on mate existence: sfen={sfen}"
);
verify_in_place_solver(&position, naive, sfen);
if position.is_in_check() {
return;
}
let Some(ctx) = mate_context(&position) else {
return;
};
let mut candidates = Move32List::new();
generate_checks_all_move32(&position, &mut candidates);
candidates.retain_unordered(|mv| position.is_legal_move32(mv));
for &mv in candidates.iter() {
verify_attackers_with_delta(&position, &ctx, mv);
}
}
const ORACLE_SFENS: &[&str] = &[
"ln1gkg1nl/6+P2/2sppps1p/2p3p2/p8/P1P1P3P/2NPbPP2/3sK1SR1/L1+b2G1NL w R2Pgp 44",
"l3kgsnl/9/p1pS+Bp3/7pp/6PP1/9/PPPPPPn1P/1B1GG4/LNS1KG+r1L w R3Psnp 56",
"l3k2nl/4g1gb1/1+S1pspp+P1/p1p6/3n4p/2PPR1P2/P2bPP2P/2g2GS2/LN2K3L w R2Psn2p 52",
"lns+R4l/1p1pS4/p1p1ppB1p/4k1p2/1R7/6P1P/P1PPnPS2/2+b1G1g2/L3K1sNL b 2G3Pnp 53",
"1+P1gkg2l/2s3s+P1/3ppp2p/P1p2npp1/l1gN1+b3/4P1P2/N2PKPS1P/2+p1G2R1/L1+r3sNL w Pbp 60",
"lnsG5/4g4/prpp1p1pp/1p4p1k/4+B4/2P1P3S/P+b1PSP1LP/4K2SL/2G2G1r1 b P3nl3p 73",
"lnsg2g1l/3k3p1/p1ppp+P+S2/8p/9/PPP2Sp1P/3PP4/1BGSKP+r2/LN3r2L w BG2NP2p 76",
"lnk1p1+R1l/1rsg3+P1/p1ppG2p1/4N3p/3S5/P7P/2+lPP4/2G1KP3/L1S4+b1 b N2Pbgsn4p 85",
"l7l/3+R5/k1+B3n2/4p1p2/P1p2p2p/2SbP1P2/1G1G1PN1P/9/LNK5L b R2GSN6P2s2p 107",
"ln1gkg1nl/6+P2/2sppps1p/2p3p2/p8/P1P1P3P/2NP1PP2/3s1KSR1/L1+b2G1NL w R2Pbgp 42",
"l3kgsnl/9/p1pS+Bp3/7pp/6PP1/9/PPPPPPn1P/1B1GG2+r1/LNS1K3L w RG3Psnp 54",
"l3k2nl/4g1gb1/1+S1pspp+P1/p1p6/3n4p/2PPR1P2/P2bPP2P/5GS2/LN1K4L w R2Pgsn2p 50",
"lns+R4l/1p1p5/p1pkppB1p/6p2/1R7/6P1P/P1PPnPS2/2+b1G1g2/L3K1sNL b 2GS3Pnp 51",
"1+P1gkg2l/2s3s+P1/3ppp2p/P1p2npp1/l2N1+b3/3KP1P2/N2P1PS1P/2+p1G2R1/L1+r3sNL w Pbgp 58",
"lnsG5/4g4/prpp1p1pp/1p4p2/4+B3k/2P1P4/P+b1PSP1LP/4K2SL/2G2G1r1 b SP3nl3p 71",
"1n5nl/lS5k1/1r2pg3/p1K3ppp/2P6/P2nSp3/1P+r1P3P/1s3s3/L7L w P2b3gn6p 136",
"l5knl/4+Rg2g/pp1p+Ng2p/7P1/1SP1B1p2/PKp1s3P/1P2P4/3B5/LN2G3+p b SNLPrs4p 145",
];
const IN_CHECK_SFENS: &[&str] = &[
"l1r3bn1/3k1g1sl/2G1pp3/p2p1Pp1p/4P4/PP1P2PRP/1g1S5/4+b4/LNK3sNL w Pgsn4p 104",
"ln2k3l/1r2g+B3/2p2g+b2/ppl2P2p/1N1pS2p1/1K2pS2P/PP1+r2+p2/L1g2+n3/9 w g2sn6p 126",
"2lk3n1/1+P3Gs2/p1Kppp1p1/4n4/2pP2p2/7P1/P3PPPR1/3+n1G3/7S1 b R2B2G2SN3L2P2p 117",
"l8/2+NG1P+N2/pg1pp4/5p3/Psp1bl3/s1PP5/1p2k3P/K3P2P1/LN2+r3L w Prb2g2sn4p 150",
"l6nl/4s2b1/p3krp2/2GPpS1Np/3pP1Pp1/4sK2P/PPp2P3/1p+b1G1r+n1/L6NL w S2P2gp 108",
"l6n1/2p1gsg2/p1n1pkpP1/5N2l/1+r6P/6P2/PB+b1gPN2/6S2/+p1PK1G2L w LPr2s6p 84",
];
#[test]
fn mate_search_preserves_input_and_handles_checked_positions() {
let position = Position::from_sfen(
"lnsG5/4g4/prpp1p1pp/1p4p1k/4+B4/2P1P3P/P+b1PSP1L1/4K2SL/2G2G1r1 b SP3nl3p 73",
)
.expect("valid mating position");
let original = position.to_sfen(None);
assert!(solve_mate_in_one(&position).is_some());
assert_eq!(position.to_sfen(None), original);
let checked =
Position::from_sfen("4k4/9/4R4/9/9/9/9/9/4K4 w - 1").expect("valid checked position");
assert!(checked.is_in_check());
assert_eq!(solve_mate_in_one(&checked), None);
}
#[test]
fn refutation_oracles_hold_on_builtin_positions() {
for sfen in ORACLE_SFENS {
verify_position_oracles(sfen);
}
}
#[test]
fn in_check_positions_stay_on_the_legacy_path() {
for sfen in IN_CHECK_SFENS {
let position = Position::from_sfen(sfen).expect("valid in-check position");
assert!(position.is_in_check(), "sfen must be an in-check position: {sfen}");
assert_eq!(
solve_mate_in_one(&position),
naive_mate_in_one(&position),
"in-check immutable path must retain the board-first candidate order: {sfen}"
);
verify_position_oracles(sfen);
}
}
fn assert_production_immutable_result(position: &Position) {
let entry = position.to_sfen(None);
let fast = solve_mate_in_one(position);
let naive = naive_mate_in_one(position);
assert_eq!(fast.is_some(), naive.is_some(), "mate existence mismatch: {entry}");
assert_eq!(position.to_sfen(None), entry, "immutable solver mutated input: {entry}");
if let Some(mv) = fast {
assert!(position.is_legal_move32(mv), "illegal candidate: {entry}");
assert!(position.gives_check_move32(mv), "non-check candidate: {entry}");
let mut next = position.clone_for_search();
next.apply_move32_with_gives_check(mv, true);
assert!(next.is_mated(), "non-mating candidate: {entry}");
next.undo_move32(mv).expect("candidate validation must undo the move it just applied");
}
}
#[test]
fn production_immutable_streaming_matches_naive_existence_and_preserves_input() {
let drop = Position::from_sfen("3pkp3/9/3G5/9/9/9/9/9/4K4 b R 1").expect("valid drop fixture");
let board =
Position::from_sfen("3pkp3/9/3G1S3/9/9/9/9/9/4K4 b - 1").expect("valid board fixture");
let nonmate =
Position::from_sfen("4k4/9/9/9/9/9/9/9/4K4 b P 1").expect("valid nonmate fixture");
let capture = Position::from_sfen(
"6+S2/ln5gP/n1sg+R1n1+N/1S2ppp2/P2gkP3/3l3P1/1+bPpKB3/s3G1Pp+l/L4r3 w 2P5p 178",
)
.expect("valid capture fixture");
let promotion = Position::from_sfen(
"6+S2/ln5gP/n1sg+R1n1+N/1S2ppp2/P3kP3/3lB2P1/1+bPpK4/s3G1Pp+l/L4r3 w G2P5p 180",
)
.expect("valid promotion fixture");
let in_check = Position::from_sfen(IN_CHECK_SFENS[0]).expect("valid in-check fixture");
for position in [&drop, &board, &nonmate, &capture, &promotion, &in_check] {
assert_production_immutable_result(position);
}
assert_production_immutable_result(
&Position::from_sfen(ORACLE_SFENS[0]).expect("valid white fixture"),
);
let mut history = crate::board::hirate_position();
for usi in ["7g7f", "3c3d", "2g2f", "8c8d"] {
let mv = crate::board::move_from_usi(&history, usi).expect("valid history move");
history.apply_move32(mv);
}
assert!(history.state_stack_depth() > 1, "fixture must carry history");
assert_production_immutable_result(&history);
}
#[test]
fn refutation_oracles_hold_on_corpus_if_configured() {
let Ok(path) = std::env::var("RSSHOGI_MATE_ORACLE_CORPUS") else {
return;
};
let content = std::fs::read_to_string(&path).expect("corpus file must be readable");
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
verify_position_oracles(line);
}
}