Fix atomic960 castling (#904)
authorFabian Fichter <ianfab@users.noreply.github.com>
Mon, 18 Aug 2025 21:00:02 +0000 (23:00 +0200)
committerGitHub <noreply@github.com>
Mon, 18 Aug 2025 21:00:02 +0000 (23:00 +0200)
Closes #887.

src/position.cpp
test.py

index 3615275..5476122 100644 (file)
@@ -1127,10 +1127,12 @@ bool Position::legal(Move m) const {
           // Chess960 as they would be in standard chess.
           kto = make_square(to > from ? castling_kingside_file() : castling_queenside_file(), castling_rank(us));
           Direction step = kto > from ? EAST : WEST;
-          Square rto = kto - step;
+          Square rto = kto - (to > from ? EAST : WEST);
           // Pseudo-royal king
           if (st->pseudoRoyals & from)
-              for (Square s = from; s != kto; s += step)
+              // Loop over squares between the king and its final position
+              // Ensure to include the initial square if from == kto
+              for (Square s = from; from != kto ? s != kto : s == from; s += step)
                   if (  !(blast_on_capture() && (attacks_bb<KING>(s) & st->pseudoRoyals & pieces(~sideToMove)))
                       && attackers_to(s, occupied, ~us))
                       return false;
diff --git a/test.py b/test.py
index 0ef43f1..8ac27ab 100644 (file)
--- a/test.py
+++ b/test.py
@@ -124,6 +124,9 @@ extinctionPieceTypes = k
 [coregaldrop:coregal]
 pieceDrops = true
 startFen = rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[Qq] w KQkq - 0 1
+
+[cannonatomic:atomic]
+cannon = c
 """
 
 sf.load_variant_config(ini_text)
@@ -406,7 +409,7 @@ class TestPyffish(unittest.TestCase):
         self.assertEqual(['d4c2', 'd4f3', 'd4b5', 'd4e6'], result)
 
 
-    def test_short_castling(self):
+    def test_castling(self):
         legals = ['f5f4', 'a7a6', 'b7b6', 'c7c6', 'd7d6', 'e7e6', 'i7i6', 'j7j6', 'a7a5', 'b7b5', 'c7c5', 'e7e5', 'i7i5', 'j7j5', 'b8a6', 'b8c6', 'h6g4', 'h6i4', 'h6j5', 'h6f7', 'h6g8', 'h6i8', 'd5a2', 'd5b3', 'd5f3', 'd5c4', 'd5e4', 'd5c6', 'd5e6', 'd5f7', 'd5g8', 'j8g8', 'j8h8', 'j8i8', 'e8f7', 'c8b6', 'c8d6', 'g6g2', 'g6g3', 'g6f4', 'g6g4', 'g6h4', 'g6e5', 'g6g5', 'g6i5', 'g6a6', 'g6b6', 'g6c6', 'g6d6', 'g6e6', 'g6f6', 'g6h8', 'f8f7', 'f8g8', 'f8i8']
         moves = ['b2b4', 'f7f5', 'c2c3', 'g8d5', 'a2a4', 'h8g6', 'f2f3', 'i8h6', 'h2h3']
         result = sf.legal_moves("capablanca", CAPA, moves)
@@ -426,6 +429,20 @@ class TestPyffish(unittest.TestCase):
         result = sf.legal_moves("diana", "rbnk1r/pppbpp/3p2/5P/PPPPPB/RBNK1R w KQkq - 2 3", [])
         self.assertIn("d1f1", result)
 
+        # Atomic960 castling
+        fen = "7k/8/8/8/8/8/2PP4/1RK4q w Q - 0 1"
+        moves = sf.legal_moves("atomic", fen, [], True)
+        # 'c1b1' is the castling move (king to rook square in 960 encoding) and must be illegal
+        self.assertNotIn("c1b1", moves)
+        # A normal king/commoner move like c1b2 should remain legal
+        self.assertIn("c1b2", moves)
+
+        # Atomic960 anti-discovered check with cannon
+        fen = "8/8/8/8/8/6k1/8/c5KR w K - 0 1"
+        moves = sf.legal_moves("cannonatomic", fen, [], True)
+        self.assertNotIn("g1h1", moves)
+        self.assertIn("g1f1", moves)
+
         # Check that in variants where castling rooks are not in the corner
         # the castling rook is nevertheless assigned correctly
         result = sf.legal_moves("shako", "c8c/ernbqkbnre/pppppppppp/10/10/10/10/PPPPPPPPPP/5K2RR/10 w Kkq - 0 1", [])