Skip to content

Commit

Permalink
Merge pull request #499 from lenguyenthanh/fix-generating-castlings-w…
Browse files Browse the repository at this point in the history
…hen-unmovedRooks-and-castles-are-inconsistent

Fix generating castlings when unmoved rooks and castles are inconsistent
  • Loading branch information
ornicar authored Nov 10, 2023
2 parents df5532e + 2b3c35a commit b2b4cee
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 2 deletions.
3 changes: 2 additions & 1 deletion src/main/scala/Situation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/main/scala/bitboard/Bitboard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions test-kit/src/test/scala/CastlingTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
34 changes: 34 additions & 0 deletions test-kit/src/test/scala/bitboard/BitboardTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit b2b4cee

Please sign in to comment.