diff --git a/src/main/scala/Situation.scala b/src/main/scala/Situation.scala index 4c8219da9..2eb38af13 100644 --- a/src/main/scala/Situation.scala +++ b/src/main/scala/Situation.scala @@ -237,12 +237,13 @@ case class Situation(board: Board, color: Color): yield move def genCastling(king: Square): List[Move] = - // can castle but which side? if !history.castles.can(color) || king.rank != color.backRank then Nil else val rooks = Bitboard.rank(color.backRank) & board.rooks & history.unmovedRooks.value for rook <- rooks + if (rook.value < king.value && history.castles.can(color, QueenSide)) + || (rook.value > king.value && history.castles.can(color, KingSide)) toKingFile = if rook.value < king.value then File.C else File.G toRookFile = if rook.value < king.value then File.D else File.F kingTo = Square(toKingFile, king.rank) diff --git a/src/main/scala/bitboard/Bitboard.scala b/src/main/scala/bitboard/Bitboard.scala index c5df9a184..85b1bb595 100644 --- a/src/main/scala/bitboard/Bitboard.scala +++ b/src/main/scala/bitboard/Bitboard.scala @@ -94,9 +94,12 @@ object Bitboard: // the last non empty square (the most significant bit / the leftmost bit) def last: Option[Square] = Square(63 - java.lang.Long.numberOfLeadingZeros(a)) - // remove the first non empty position + // remove the first/smallest non empty square def removeFirst: Bitboard = a & (a - 1L) + // remove the last/largest non empty square + def removeLast: Bitboard = a & ~a.msb.bl + inline def intersects(inline o: Long): Boolean = (a & o) != 0L diff --git a/test-kit/src/test/scala/CastlingTest.scala b/test-kit/src/test/scala/CastlingTest.scala index 07ff1ba15..3a0ef85aa 100644 --- a/test-kit/src/test/scala/CastlingTest.scala +++ b/test-kit/src/test/scala/CastlingTest.scala @@ -2,34 +2,49 @@ package chess import scala.language.implicitConversions import Square.* +import chess.format.Fen +import chess.variant.Standard +import monocle.syntax.all.* class CastlingTest extends ChessTest: import compare.dests val board: Board = """R K R""" + test("threat on king prevents castling: by a rook"): assertEquals( board.place(Black.rook, E3).flatMap(_ destsFrom E1), Set(D1, D2, F2, F1) ) + test("threat on king prevents castling: by a knight"): assertEquals(board.place(Black.knight, D3).flatMap(_ destsFrom E1), Set(D1, D2, E2, F1)) + test("threat on castle trip prevents castling: king side"): val board: Board = """R QK R""" assertEquals(board.place(Black.rook, F3).flatMap(_ destsFrom E1), Set(D2, E2)) assertEquals(board.place(Black.rook, G3).flatMap(_ destsFrom E1), Set(D2, E2, F2, F1)) + test("threat on castle trip prevents castling: queen side"): val board: Board = """R KB R""" assertEquals(board.place(Black.rook, D3).flatMap(_ destsFrom E1), Set(E2, F2)) assertEquals(board.place(Black.rook, C3).flatMap(_ destsFrom E1), Set(D1, D2, E2, F2)) + test("threat on castle trip prevents castling: chess 960"): val board: Board = """BK R""" assertEquals(board.place(Black.rook, F3).flatMap(_ destsFrom B1), Set(A2, B2, C2, C1)) assertEquals(board.place(Black.king, E2).flatMap(_ destsFrom B1), Set(A2, B2, C2, C1)) + test("threat on rook does not prevent castling king side"): val board: Board = """R QK R""" assertEquals(board.place(Black.rook, H3).flatMap(_ destsFrom E1), Set(D2, E2, F1, F2, G1, H1)) + test("threat on rook does not prevent castling king side"): val board: Board = """R KB R""" assertEquals(board.place(Black.rook, A3).flatMap(_ destsFrom E1), Set(A1, C1, D1, D2, E2, F2)) + + test("unmovedRooks and castles are consistent"): + val s1 = Fen.read(Standard, Fen.Epd("rnbqk2r/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w Qq - 0 1")).get + val s2 = s1.focus(_.board.history.unmovedRooks).replace(UnmovedRooks.corners) + assertEquals(s2.legalMoves.filter(_.castles), Nil) diff --git a/test-kit/src/test/scala/bitboard/BitboardTest.scala b/test-kit/src/test/scala/bitboard/BitboardTest.scala index a3d609c75..e61597f41 100644 --- a/test-kit/src/test/scala/bitboard/BitboardTest.scala +++ b/test-kit/src/test/scala/bitboard/BitboardTest.scala @@ -37,10 +37,44 @@ class BitboardTest extends ScalaCheckSuite: forAll: (bb: Bitboard) => assertEquals(bb.singleSquare.isDefined, bb.first == bb.last) + property("fold removeFirst should return empty"): + forAll: (bb: Bitboard) => + bb.fold(bb)((b, _) => b.removeFirst).isEmpty + + property("fold removeFirst.add(first) == identity"): + forAll: (bb: Bitboard) => + bb.nonEmpty ==> { + bb.removeFirst.add(bb.first.get) == bb + } + + property("bb.removeFirst == bb <=> bb.isEmpty"): + forAll: (bb: Bitboard) => + if bb.isEmpty then bb.removeFirst == bb + else bb.removeFirst != bb + test("fold removeFirst should return empty"): forAll: (bb: Bitboard) => assertEquals(bb.fold(bb)((b, _) => b.removeFirst), Bitboard.empty) + property("fold removeLast should return empty"): + forAll: (bb: Bitboard) => + bb.fold(bb)((b, _) => b.removeLast).isEmpty + + property("fold removeLast.add(last) == identity"): + forAll: (bb: Bitboard) => + bb.nonEmpty ==> { + bb.removeLast.add(bb.last.get) == bb + } + + property("bb.removeLast == bb <=> bb.isEmpty"): + forAll: (bb: Bitboard) => + if bb.isEmpty then bb.removeLast == bb + else bb.removeLast != bb + + property("removeFirst == removeLast <=> bb.count <= 1"): + forAll: (bb: Bitboard) => + bb.removeFirst == bb.removeLast == (bb.count <= 1) + test("first with a function that always return None should return None"): forAll: (bb: Bitboard) => assertEquals(bb.first(_ => None), None)