1#![cfg_attr(not(test), no_std)] #![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;
15pub 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
25pub 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#[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 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 repeated = true;
121 }
122 }
123 if repeated {
124 return PositionStatus::Draw;
125 }
126 PositionStatus::InProgress
127}
128
129pub 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 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
167pub 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
185pub 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#[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
256pub 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
287pub 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
306pub 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#[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#[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
400fn 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 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 let candidate = unsafe { Square::new(king_file, new_rank).unwrap_unchecked() };
420 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 position.vacant_bitboard() & in_range
441}
442
443#[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 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 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 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 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 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 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}