shogi_legality_lite/
lib.rs

1#![cfg_attr(not(test), no_std)] // Forbids using std::*.
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc = include_str!("../README.md")]
4
5#[cfg(feature = "alloc")]
6extern crate alloc;
7
8use prelegality::will_king_be_captured;
9use shogi_core::{
10    Bitboard, Color, IllegalMoveKind, LegalityChecker, Move, PartialPosition, Piece, PieceKind,
11    PositionStatus, Square,
12};
13
14mod normal;
15/// Legality checking without confirming king's safety.
16pub mod prelegality;
17
18#[doc(hidden)]
19#[cfg(feature = "alloc")]
20pub mod mate_solver;
21#[doc(hidden)]
22#[cfg(feature = "alloc")]
23pub mod perft;
24
25/// A type for legality checking.
26///
27/// Methods of this type do not use constant tables.
28/// They do not allocate unless it is necessary.
29pub struct LiteLegalityChecker;
30
31impl LegalityChecker for LiteLegalityChecker {
32    #[cfg(feature = "alloc")]
33    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
34    #[inline(always)]
35    fn status(&self, position: &shogi_core::Position) -> PositionStatus {
36        crate::status(position)
37    }
38
39    #[inline(always)]
40    fn status_partial(&self, position: &PartialPosition) -> PositionStatus {
41        crate::status_partial(position)
42    }
43
44    #[inline(always)]
45    fn is_legal_partial(
46        &self,
47        position: &PartialPosition,
48        mv: Move,
49    ) -> Result<(), IllegalMoveKind> {
50        crate::is_legal_partial(position, mv)
51    }
52
53    #[inline(always)]
54    fn is_legal_partial_lite(&self, position: &PartialPosition, mv: Move) -> bool {
55        crate::is_legal_partial_lite(position, mv)
56    }
57
58    #[cfg(feature = "alloc")]
59    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
60    #[inline(always)]
61    fn all_legal_moves_partial(&self, position: &PartialPosition) -> alloc::vec::Vec<Move> {
62        crate::all_legal_moves_partial(position)
63    }
64
65    #[inline(always)]
66    fn normal_from_candidates(&self, position: &PartialPosition, from: Square) -> Bitboard {
67        crate::normal_from_candidates(position, from)
68    }
69
70    #[inline(always)]
71    fn normal_to_candidates(
72        &self,
73        position: &PartialPosition,
74        to: Square,
75        piece: Piece,
76    ) -> Bitboard {
77        crate::normal_to_candidates(position, to, piece)
78    }
79
80    #[inline(always)]
81    fn drop_candidates(&self, position: &PartialPosition, piece: Piece) -> Bitboard {
82        crate::drop_candidates(position, piece)
83    }
84}
85
86/// Returns the status of this position.
87///
88/// Since: 0.1.1
89#[cfg(feature = "alloc")]
90#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
91pub fn status(position: &shogi_core::Position) -> PositionStatus {
92    let result = status_partial(position.inner());
93    if result != PositionStatus::InProgress {
94        return result;
95    }
96    // repetition check, takes O(length^2)-time
97    let moves = position.moves();
98    let length = moves.len();
99    let mut history = alloc::vec::Vec::with_capacity(length + 1);
100    let mut current = position.initial_position().clone();
101    let _ = current.ply_set(1);
102    history.push(current.to_sfen_owned());
103    let mut repeated = false;
104    for i in 0..length {
105        let result = current.make_move(moves[i]);
106        if result.is_none() {
107            return PositionStatus::Invalid;
108        }
109        let _ = current.ply_set(1);
110        history.push(current.to_sfen_owned());
111        debug_assert_eq!(history.len(), i + 2);
112        let mut eq = 0;
113        for j in 0..i + 1 {
114            if history[i + 1] == history[j] {
115                eq += 1;
116            }
117        }
118        if eq >= 3 {
119            // The same position appeared 4 times.
120            repeated = true;
121        }
122    }
123    if repeated {
124        return PositionStatus::Draw;
125    }
126    PositionStatus::InProgress
127}
128
129/// Returns the status of this position.
130///
131/// Because [`PartialPosition`] does not have a move sequence in it,
132/// it cannot return [`PositionStatus::Draw`] (which needs repetition check).
133///
134/// Since: 0.1.1
135pub fn status_partial(position: &PartialPosition) -> PositionStatus {
136    let side = position.side_to_move();
137    if crate::prelegality::is_mate(position) == Some(true) {
138        return [PositionStatus::WhiteWins, PositionStatus::BlackWins][side.array_index()];
139    }
140    let hand_b = position.hand_of_a_player(Color::Black);
141    let hand_w = position.hand_of_a_player(Color::White);
142    let max = [
143        (PieceKind::Pawn, 18),
144        (PieceKind::Lance, 4),
145        (PieceKind::Knight, 4),
146        (PieceKind::Silver, 4),
147        (PieceKind::Gold, 4),
148        (PieceKind::Bishop, 2),
149        (PieceKind::Rook, 2),
150    ];
151    for (piece_kind, limit) in max {
152        let mut bb = position.piece_kind_bitboard(piece_kind);
153        if let Some(promoted) = piece_kind.promote() {
154            bb |= position.piece_kind_bitboard(promoted);
155        }
156        // Safety: `piece_kind` is valid in hand
157        let count = bb.count()
158            + unsafe { hand_b.count(piece_kind).unwrap_unchecked() }
159            + unsafe { hand_w.count(piece_kind).unwrap_unchecked() };
160        if count > limit {
161            return PositionStatus::Invalid;
162        }
163    }
164    PositionStatus::InProgress
165}
166
167/// Finds if a move is legal in the given position.
168///
169/// If `position` is not in progress, no moves are considered to be legal.
170/// If `is_legal_partial` returns `Ok(())`, `position.make_move(mv)` must succeed.
171///
172/// Since: 0.1.1
173pub fn is_legal_partial(position: &PartialPosition, mv: Move) -> Result<(), IllegalMoveKind> {
174    prelegality::is_valid_with_error(position, mv)?;
175    let mut next = position.clone();
176    if next.make_move(mv).is_none() {
177        return Err(IllegalMoveKind::IncorrectMove);
178    }
179    if prelegality::will_king_be_captured(&next) == Some(true) {
180        return Err(IllegalMoveKind::IgnoredCheck);
181    }
182    Ok(())
183}
184
185/// Finds if a move is legal in the given position.
186///
187/// If `position` is not in progress, no moves are considered to be legal.
188/// If `is_legal_partial_lite` returns `true`, `position.make_move(mv)` must succeed.
189///
190/// Since: 0.1.1
191pub fn is_legal_partial_lite(position: &PartialPosition, mv: Move) -> bool {
192    if !prelegality::is_valid(position, mv) {
193        return false;
194    }
195    let mut next = position.clone();
196    if next.make_move(mv).is_none() {
197        return false;
198    }
199    if prelegality::will_king_be_captured(&next) == Some(true) {
200        return false;
201    }
202    true
203}
204
205/// Finds all legal moves in the given position.
206///
207/// If `position` is not in progress, no moves are considered to be legal.
208///
209/// Since: 0.1.1
210#[cfg(feature = "alloc")]
211#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
212pub fn all_legal_moves_partial(position: &PartialPosition) -> alloc::vec::Vec<Move> {
213    use shogi_core::Hand;
214
215    let side = position.side_to_move();
216    let my_bb = position.player_bitboard(side);
217    let mut result = alloc::vec::Vec::new();
218    for from in my_bb {
219        let to_candidates = prelegality::normal_from_candidates(position, from);
220        for (index, to_candidates) in to_candidates.into_iter().enumerate() {
221            let promote = index == 1;
222            for to in to_candidates {
223                let mv = Move::Normal { from, to, promote };
224                let mut next = position.clone();
225                if next.make_move(mv).is_none() {
226                    continue;
227                }
228                if prelegality::will_king_be_captured(&next) == Some(true) {
229                    continue;
230                }
231                result.push(mv);
232            }
233        }
234    }
235
236    let my_hand = position.hand_of_a_player(side);
237    if my_hand == Hand::new() {
238        return result;
239    }
240    for piece_kind in shogi_core::Hand::all_hand_pieces() {
241        let count = unsafe { my_hand.count(piece_kind).unwrap_unchecked() };
242        if count == 0 {
243            continue;
244        }
245        let piece = Piece::new(piece_kind, side);
246        for to in position.vacant_bitboard() {
247            let mv = Move::Drop { piece, to };
248            if is_legal_partial_lite(position, mv) {
249                result.push(mv);
250            }
251        }
252    }
253    result
254}
255
256/// Finds all legal normal moves in the given position that move a piece from `from` to some square.
257///
258/// If `position` is not in progress, no moves are considered to be legal.
259/// This function returns a [`Bitboard`] consisting of all destination squares.
260///
261/// Since: 0.1.1
262pub fn normal_from_candidates(position: &PartialPosition, from: Square) -> Bitboard {
263    let mut result = Bitboard::empty();
264    let side = position.side_to_move();
265    let my_bb = position.player_bitboard(side);
266    if !my_bb.contains(from) {
267        return Bitboard::empty();
268    }
269    let to_candidates = prelegality::normal_from_candidates(position, from);
270    for (index, to_candidates) in to_candidates.into_iter().enumerate() {
271        let promote = index == 1;
272        for to in to_candidates {
273            let mv = Move::Normal { from, to, promote };
274            let mut next = position.clone();
275            if next.make_move(mv).is_none() {
276                continue;
277            }
278            if prelegality::will_king_be_captured(&next) == Some(true) {
279                continue;
280            }
281            result |= to;
282        }
283    }
284    result
285}
286
287/// Finds all legal normal moves in the given position that move `piece` from some square to `to`.
288///
289/// If `position` is not in progress, no moves are considered to be legal.
290/// This function returns a [`Bitboard`] consisting of all source squares.
291///
292/// Since: 0.1.1
293pub fn normal_to_candidates(position: &PartialPosition, to: Square, piece: Piece) -> Bitboard {
294    let mut result = Bitboard::empty();
295    for from in Square::all() {
296        for promote in [true, false] {
297            let mv = Move::Normal { from, to, promote };
298            if is_legal_partial_lite(position, mv) && position.piece_at(from) == Some(piece) {
299                result |= from;
300            }
301        }
302    }
303    result
304}
305
306/// Finds all legal drop moves in the given position that drop `piece`.
307///
308/// If `position` is not in progress, no moves are considered to be legal.
309/// This function returns a [`Bitboard`] consisting of all destination squares.
310///
311/// Since: 0.1.1
312pub fn drop_candidates(position: &PartialPosition, piece: Piece) -> Bitboard {
313    let mut result = Bitboard::empty();
314    for to in Square::all() {
315        let mv = Move::Drop { piece, to };
316        if is_legal_partial_lite(position, mv) {
317            result |= to;
318        }
319    }
320    result
321}
322
323/// Finds all checks.
324///
325/// Since: 0.1.1
326#[cfg(feature = "alloc")]
327#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
328pub fn all_checks_partial(position: &PartialPosition) -> alloc::vec::Vec<Move> {
329    use shogi_core::Hand;
330
331    let side = position.side_to_move();
332    let king = match position.king_position(side.flip()) {
333        Some(x) => x,
334        None => return alloc::vec::Vec::new(),
335    };
336    let king_file = king.file();
337    let king_rank = king.rank();
338    let my_bb = position.player_bitboard(side);
339    let mut result = alloc::vec::Vec::new();
340    for from in my_bb {
341        let to_candidates = prelegality::normal_from_candidates(position, from);
342        for (index, to_candidates) in to_candidates.into_iter().enumerate() {
343            let promote = index == 1;
344            for to in to_candidates {
345                let mv = Move::Normal { from, to, promote };
346                let mut next = position.clone();
347                if next.make_move(mv).is_none() {
348                    continue;
349                }
350                if prelegality::will_king_be_captured(&next) == Some(true) {
351                    continue;
352                }
353                if is_in_check_partial_lite(&next) {
354                    result.push(mv);
355                }
356            }
357        }
358    }
359
360    let my_hand = position.hand_of_a_player(side);
361    if my_hand == Hand::new() {
362        return result;
363    }
364    for piece_kind in shogi_core::Hand::all_hand_pieces() {
365        let count = unsafe { my_hand.count(piece_kind).unwrap_unchecked() };
366        if count == 0 {
367            continue;
368        }
369        let bb = all_drop_checks_partial_sub(position, piece_kind, king_file, king_rank);
370        for to in bb {
371            let mv = Move::Drop {
372                piece: Piece::new(piece_kind, side),
373                to,
374            };
375            result.push(mv);
376        }
377    }
378    result
379}
380
381/// Finds all checks that are drop moves.
382#[no_mangle]
383pub extern "C" fn all_drop_checks_partial(
384    position: &PartialPosition,
385    piece_kind: PieceKind,
386) -> Bitboard {
387    let side = position.side_to_move();
388    let my_hand = position.hand_of_a_player(side);
389    let count = unsafe { my_hand.count(piece_kind).unwrap_unchecked() };
390    if count == 0 {
391        return Bitboard::empty();
392    }
393    let king = match position.king_position(side.flip()) {
394        Some(x) => x,
395        None => return Bitboard::empty(),
396    };
397    all_drop_checks_partial_sub(position, piece_kind, king.file(), king.rank())
398}
399
400// Does not check if:
401// - at least one piece of `piece_kind` is in hand
402fn all_drop_checks_partial_sub(
403    position: &PartialPosition,
404    piece_kind: PieceKind,
405    king_file: u8,
406    king_rank: u8,
407) -> Bitboard {
408    let side = position.side_to_move();
409    // Special handling for drop pawn mate
410    if piece_kind == PieceKind::Pawn {
411        let new_rank = match (side, king_rank) {
412            (Color::Black, 9) | (Color::White, 1) => {
413                return Bitboard::empty();
414            }
415            (Color::Black, x) => x + 1,
416            (Color::White, x) => x - 1,
417        };
418        // There is at most one candidate square
419        let candidate = unsafe { Square::new(king_file, new_rank).unwrap_unchecked() };
420        // Is dropping a pawn there legal?
421        let mv = Move::Drop {
422            piece: Piece::new(PieceKind::Pawn, side),
423            to: candidate,
424        };
425        if prelegality::is_valid(position, mv) {
426            return Bitboard::single(candidate);
427        } else {
428            return Bitboard::empty();
429        }
430    }
431    let piece = Piece::new(piece_kind, side.flip());
432    let in_range = crate::normal::from_candidates_without_assertion(
433        position.occupied_bitboard(),
434        position,
435        piece,
436        king_file,
437        king_rank,
438    );
439    // If a drop move is stuck, it cannot be a check.
440    position.vacant_bitboard() & in_range
441}
442
443/// Determines if king is in check.
444///
445/// Since: 0.1.1
446#[no_mangle]
447pub extern "C" fn is_in_check_partial_lite(position: &PartialPosition) -> bool {
448    let mut next = position.clone();
449    next.side_to_move_set(next.side_to_move().flip());
450    matches!(will_king_be_captured(&next), Some(true))
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use shogi_usi_parser::FromUsi;
457
458    #[test]
459    fn all_legal_moves_partial_works_0() {
460        let position = PartialPosition::startpos();
461        let first_moves = all_legal_moves_partial(&position);
462        assert_eq!(first_moves.len(), 30);
463    }
464
465    #[test]
466    fn all_legal_moves_partial_works_1() {
467        let position =
468            PartialPosition::from_usi("sfen 7l1/7pk/7n1/8R/7N1/9/9/9/9 w r2b4g4s2n3l17p 1")
469                .unwrap();
470        let result = all_legal_moves_partial(&position);
471        assert_eq!(result.len(), 7);
472    }
473    #[test]
474    fn status_works() {
475        let position = shogi_core::Position::startpos();
476        let result = status(&position);
477        assert_eq!(result, PositionStatus::InProgress);
478
479        let moves_a = [
480            Move::Normal {
481                from: Square::SQ_5I,
482                to: Square::SQ_5H,
483                promote: false,
484            },
485            Move::Normal {
486                from: Square::SQ_5A,
487                to: Square::SQ_5B,
488                promote: false,
489            },
490            Move::Normal {
491                from: Square::SQ_5H,
492                to: Square::SQ_5I,
493                promote: false,
494            },
495            Move::Normal {
496                from: Square::SQ_5B,
497                to: Square::SQ_5A,
498                promote: false,
499            },
500        ];
501
502        // Slightly different from moves_a
503        let moves_b = [
504            Move::Normal {
505                from: Square::SQ_5I,
506                to: Square::SQ_4H,
507                promote: false,
508            },
509            Move::Normal {
510                from: Square::SQ_5A,
511                to: Square::SQ_6B,
512                promote: false,
513            },
514            Move::Normal {
515                from: Square::SQ_4H,
516                to: Square::SQ_5I,
517                promote: false,
518            },
519            Move::Normal {
520                from: Square::SQ_6B,
521                to: Square::SQ_5A,
522                promote: false,
523            },
524        ];
525        let mut moves = vec![];
526        for _ in 0..3 {
527            moves.extend_from_slice(&moves_a);
528        }
529        let mut position = shogi_core::Position::startpos();
530        for mv in moves {
531            position.make_move(mv).unwrap();
532        }
533        let result = status(&position);
534        assert_eq!(result, PositionStatus::Draw);
535
536        // Even if exactly the same sequence of moves are not made three times, it is considered repetition.
537        let mut moves = vec![];
538        moves.extend_from_slice(&moves_a);
539        moves.extend_from_slice(&moves_b);
540        moves.extend_from_slice(&moves_a);
541        let mut position = shogi_core::Position::startpos();
542        for mv in moves {
543            position.make_move(mv).unwrap();
544        }
545        let result = status(&position);
546        assert_eq!(result, PositionStatus::Draw);
547    }
548
549    #[test]
550    fn status_partial_works() {
551        let position = PartialPosition::startpos();
552        let result = status_partial(&position);
553        assert_eq!(result, PositionStatus::InProgress);
554        // One of the shortest mate sequences
555        let moves = [
556            Move::Normal {
557                from: Square::SQ_2G,
558                to: Square::SQ_2F,
559                promote: false,
560            },
561            Move::Normal {
562                from: Square::SQ_5A,
563                to: Square::SQ_4B,
564                promote: false,
565            },
566            Move::Normal {
567                from: Square::SQ_2F,
568                to: Square::SQ_2E,
569                promote: false,
570            },
571            Move::Normal {
572                from: Square::SQ_4B,
573                to: Square::SQ_3B,
574                promote: false,
575            },
576            Move::Normal {
577                from: Square::SQ_2E,
578                to: Square::SQ_2D,
579                promote: false,
580            },
581            Move::Normal {
582                from: Square::SQ_8B,
583                to: Square::SQ_4B,
584                promote: false,
585            },
586            Move::Normal {
587                from: Square::SQ_2D,
588                to: Square::SQ_2C,
589                promote: true,
590            },
591        ];
592        let mut position = PartialPosition::startpos();
593        for mv in moves {
594            position.make_move(mv).unwrap();
595        }
596        let result = status_partial(&position);
597        assert_eq!(result, PositionStatus::BlackWins);
598    }
599
600    #[test]
601    fn all_checks_partial_works_0() {
602        // From https://github.com/koba-e964/shogi-mate-problems/blob/d58d61336dd82096856bc3ac0ba372e5cd722bc8/2022-05-18/mate5.psn#L3
603        let position =
604            PartialPosition::from_usi("sfen 3g1ks2/6g2/4S4/7B1/9/9/9/9/9 b G2rbg2s4n4l18p 1")
605                .unwrap();
606        let checks = all_checks_partial(&position);
607        assert_eq!(checks.len(), 9);
608        assert_eq!(checks.iter().filter(|mv| mv.is_drop()).count(), 3);
609    }
610
611    #[test]
612    fn all_checks_partial_works_1() {
613        // From https://github.com/koba-e964/shogi-mate-problems/blob/d58d61336dd82096856bc3ac0ba372e5cd722bc8/2022-05-18/mate9.psn#L3
614        let position =
615            PartialPosition::from_usi("sfen 5kgnl/9/4+B1pp1/8p/9/9/9/9/9 b 2S2rb3g2s3n3l15p 1")
616                .unwrap();
617        let checks = all_checks_partial(&position);
618        assert_eq!(checks.len(), 7);
619        assert_eq!(checks.iter().filter(|mv| mv.is_drop()).count(), 3);
620    }
621
622    #[test]
623    fn all_checks_partial_works_2() {
624        // From https://github.com/koba-e964/shogi-mate-problems/blob/d58d61336dd82096856bc3ac0ba372e5cd722bc8/2022-05-19/dpm.psn#L3
625        let position =
626            PartialPosition::from_usi("sfen 7nk/9/6PB1/6NP1/9/9/9/9/9 b P2rb4g4s2n4l15p 1")
627                .unwrap();
628        let checks = all_checks_partial(&position);
629        assert_eq!(checks.len(), 2);
630        assert_eq!(checks.iter().filter(|mv| mv.is_drop()).count(), 0);
631    }
632}