Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions core/src/main/scala/Bitboard.scala
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ object Bitboard:
do
builder += b.lsb
b &= (b - 1L)
builder.result
builder.result()

def toSet: Set[Square] =
squares.toSet
Expand Down Expand Up @@ -187,7 +187,7 @@ object Bitboard:
do
if f(b.lsb) then builder += b.lsb
b &= (b - 1L)
builder.result
builder.result()

def withFilter(f: Square => Boolean): List[Square] =
filter(f)
Expand Down Expand Up @@ -224,7 +224,7 @@ object Bitboard:
do
builder ++= f(b.lsb)
b &= (b - 1L)
builder.result
builder.result()

def map[B](f: Square => B): List[B] =
var b = a
Expand All @@ -233,7 +233,7 @@ object Bitboard:
do
builder += f(b.lsb)
b &= (b - 1L)
builder.result
builder.result()

def iterator: Iterator[Square] = new:
private var b = a
Expand All @@ -251,4 +251,4 @@ object Bitboard:
builder ++= (if contains(s) then "1" else ".")
if f != File.H then builder ++= " "
else if s != Square.H1 then builder ++= "\n"
builder.result
builder.result()
2 changes: 1 addition & 1 deletion core/src/main/scala/Board.scala
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ case class Board(occupied: Bitboard, byColor: ByColor[Bitboard], byRole: ByRole[
val piece = color - role
(c & r).foreach: s =>
m += s -> piece
m.result
m.result()

def piecesOf(c: Color): Map[Square, Piece] =
pieceMap.filter((_, p) => p.color == c)
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/scala/Position.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ case class Position(board: Board, history: History, variant: Variant, color: Col

export color.white as isWhiteTurn

def isTurn(c: Color): Boolean = c == color

def withCastles(c: Castles) = updateHistory(_.withCastles(c))

def unary_! : Position = withColor(color = !color)
Expand Down
32 changes: 16 additions & 16 deletions core/src/main/scala/format/BinaryFen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ case class BinaryFen(value: Array[Byte]) extends AnyVal:
val reader = new Iterator[Byte]:
val inner = value.iterator
override inline def hasNext: Boolean = inner.hasNext
override inline def next: Byte = if hasNext then inner.next else 0.toByte
override inline def next: Byte = if hasNext then inner.next() else 0.toByte

val occupied = Bitboard(readLong(reader))

Expand Down Expand Up @@ -92,12 +92,12 @@ case class BinaryFen(value: Array[Byte]) extends AnyVal:
while it.hasNext
do
val (lo, hi) = readNibbles(reader)
unpackPiece(it.next, lo)
if it.hasNext then unpackPiece(it.next, hi)
unpackPiece(it.next(), lo)
if it.hasNext then unpackPiece(it.next(), hi)

val halfMoveClock = HalfMoveClock(readLeb128(reader))
val ply = Ply(readLeb128(reader))
val variant = reader.next match
val variant = reader.next() match
case 0 => Standard
case 1 => Crazyhouse
case 2 => Chess960
Expand Down Expand Up @@ -218,7 +218,7 @@ object BinaryFen:

val it = occupied.iterator
while it.hasNext
do writeNibbles(builder, packPiece(it.next), if it.hasNext then packPiece(it.next) else 0)
do writeNibbles(builder, packPiece(it.next()), if it.hasNext then packPiece(it.next()) else 0)

val halfMoveClock = position.history.halfMoveClock.value
val ply = input.fullMoveNumber.ply(position.color).value
Expand Down Expand Up @@ -256,7 +256,7 @@ object BinaryFen:
writeNibbles(builder, pockets.white.queen, pockets.black.queen)
if crazyData.promoted.nonEmpty then writeLong(builder, crazyData.promoted.value)

builder.result
builder.result()

object implementation:

Expand All @@ -271,14 +271,14 @@ object BinaryFen:
builder.addOne(v.toByte)

def readLong(reader: Iterator[Byte]): Long =
((reader.next & 0xffL) << 56) |
((reader.next & 0xffL) << 48) |
((reader.next & 0xffL) << 40) |
((reader.next & 0xffL) << 32) |
((reader.next & 0xffL) << 24) |
((reader.next & 0xffL) << 16) |
((reader.next & 0xffL) << 8) |
(reader.next & 0xffL)
((reader.next() & 0xffL) << 56) |
((reader.next() & 0xffL) << 48) |
((reader.next() & 0xffL) << 40) |
((reader.next() & 0xffL) << 32) |
((reader.next() & 0xffL) << 24) |
((reader.next() & 0xffL) << 16) |
((reader.next() & 0xffL) << 8) |
(reader.next() & 0xffL)

def writeLeb128(builder: ArrayBuilder[Byte], v: Int): Unit =
var n = v
Expand All @@ -292,7 +292,7 @@ object BinaryFen:
var n = 0
var shift = 0
while
val b = reader.next
val b = reader.next()
n |= (b & 127) << shift
shift += 7
(b & 128) != 0
Expand All @@ -303,7 +303,7 @@ object BinaryFen:
builder.addOne((lo | (hi << 4)).toByte)

def readNibbles(reader: Iterator[Byte]): (Int, Int) =
val b = reader.next
val b = reader.next()
((b & 0xf), (b >>> 4) & 0xf)

def minimumUnmovedRooks(position: Position): UnmovedRooks =
Expand Down
4 changes: 2 additions & 2 deletions core/src/main/scala/format/FenReader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ trait FenReader:
rank -= 1
if rank < 0 then error = Some("too many ranks")
else
iter.next match
iter.next() match
case '/' => // ignored, optional. Rank switch is automatic
case ch if numberSet.contains(ch) =>
file += (ch - '0')
Expand All @@ -198,7 +198,7 @@ trait FenReader:
addPieceAt(p, square)
if iter.headOption == Some('~') then
promoted |= square
val _ = iter.next
val _ = iter.next()
case None => error = Some(s"invalid piece $ch")
file += 1
val board = Board(
Expand Down
41 changes: 36 additions & 5 deletions core/src/main/scala/variant/Antichess.scala
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,44 @@ case object Antichess
// player can win (depending on whose turn it is).

override def opponentHasInsufficientMaterial(position: Position): Boolean =
justOneKnightEach(position) && allOnSameColourSquares(position)
hasInsufficientMaterial(position, !position.color)

override def playerHasInsufficientMaterial(position: Position): Boolean =
justOneKnightEach(position) && !allOnSameColourSquares(position)
hasInsufficientMaterial(position, position.color)

private def hasInsufficientMaterial(position: Position, color: Color): Boolean =
(
position.onlyKnights &&
position.white.count == 1 &&
position.black.count == 1 &&
position.isTurn(color) != allOnSameColourSquares(position)
) ||
{
val subject = if position.isTurn(color) then position.us else position.them
val opposing = if position.isTurn(color) then position.them else position.us
val subjectBishops = subject & position.bishops
val subjectPawns = subject & position.pawns
val opposingBishops = opposing & position.bishops
subjectBishops.nonEmpty &&
opposingBishops.nonEmpty &&
opposingBishops == opposing &&
List(Bitboard.lightSquares, Bitboard.darkSquares).exists(colorComplex =>
opposingBishops.isDisjoint(colorComplex) && subjectBishops.intersects(colorComplex)
) &&
(
subjectPawns.isEmpty || (
// TODO: handle cases with > 1 subject pawn/opposing bishop that are still
// impossible for the subject to win.
subjectPawns.count == 1 &&
opposingBishops.count == 1 &&
List(
(File.B, if color == Color.White then Bitboard.darkSquares else Bitboard.lightSquares),
(File.G, if color == Color.White then Bitboard.lightSquares else Bitboard.darkSquares)
).forall: (file, colorComplex) =>
subjectPawns.isDisjoint(Bitboard.file(file)) || opposingBishops.isDisjoint(colorComplex)
)
)
}

// No player can win if the only remaining pieces are opposing bishops on different coloured
// diagonals. There may be pawns that are incapable of moving and do not attack the right color
Expand Down Expand Up @@ -102,6 +136,3 @@ case object Antichess
private def allOnSameColourSquares(position: Position): Boolean =
Bitboard.lightSquares.isDisjoint(position.occupied) ||
Bitboard.darkSquares.isDisjoint(position.occupied)

private def justOneKnightEach(position: Position): Boolean =
position.onlyKnights && position.white.count == 1 && position.black.count == 1
41 changes: 39 additions & 2 deletions test-kit/src/test/scala/AntichessVariantTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,51 @@ g4 {[%emt 0.200]} 34. Rxg4 {[%emt 0.172]} 0-1"""
):
val position = FullFen("1n6/8/8/8/8/4N3/8/8 w - - 0 1")
val game = fenToGame(position, Antichess).playMoves(Square.E3 -> Square.D1).get
assertEquals(game.position.playerHasInsufficientMaterial, true)
assert(game.position.playerHasInsufficientMaterial)

test(
"Player has sufficient material when there are only two remaining knights on same color squares"
):
val position = FullFen("1n6/8/8/8/8/8/8/7N w - - 0 1")
val game = fenToGame(position, Antichess).playMoves(Square.H1 -> Square.G3).get
assertEquals(game.position.playerHasInsufficientMaterial, false)
assertNot(game.position.playerHasInsufficientMaterial)

test("Side has sufficient material - bishops"):
List(
"3b4/1B6/6b1/6N1/3K1R2/8/3QK3/8 w - - 0 1" -> (Square.G5 -> Square.H7),
"8/8/8/8/8/3p1b1B/8/8 b - - 0 1" -> (Square.D3 -> Square.D2),
"8/8/1PB5/4b3/8/8/8/8 w - - 0 1" -> (Square.C6 -> Square.A8),
"7B/8/6P1/5b2/8/8/8/8 w - - 0 1" -> (Square.G6 -> Square.G7),
"7B/8/8/5b2/8/6p1/8/8 b - - 0 1" -> (Square.F5 -> Square.E4),
"8/7B/8/4b3/8/1p6/8/8 b - - 0 1" -> (Square.E5 -> Square.D4),
"3b4/8/2B5/2P1b3/8/8/8/8 w - - 0 1" -> (Square.C6 -> Square.A8),
"8/8/BP6/B5b1/8/8/8/8 w - - 0 1" -> (Square.A6 -> Square.B5),
// the following is actually insufficient material, but doesn't qualify under curr behaviour
"8/8/2B5/P1P1b3/8/8/8/8 w - - 0 1" -> (Square.C6 -> Square.A8)
).foreach: (fen, move) =>
val game = fenToGame(FullFen(fen), Antichess)
assertNot(game.position.playerHasInsufficientMaterial)
assertNot(game.position.opponentHasInsufficientMaterial)
val updated = game.playMoves(move).get
assertNot(updated.position.playerHasInsufficientMaterial)
assertNot(updated.position.opponentHasInsufficientMaterial)

test("Side has insufficient material - bishops"):
List(
"6b1/BB6/6b1/6N1/B2K1R2/8/3QK3/1b6 w - - 0 1" -> (Square.G5 -> Square.H7),
"8/8/1P6/2B2b2/8/8/8/8 w - - 0 1" -> (Square.C5 -> Square.A3),
"6B1/8/6P1/8/5b2/8/8/8 w - - 0 1" -> (Square.G6 -> Square.G7),
"8/7B/8/8/5b2/6p1/8/8 b - - 0 1" -> (Square.F4 -> Square.E3),
"8/8/7B/8/6b1/1p6/8/8 b - - 0 1" -> (Square.B3 -> Square.B2),
"8/8/2B5/2P1b3/8/8/8/8 w - - 0 1" -> (Square.C6 -> Square.A8),
"8/8/BP6/B6b/8/8/8/8 w - - 0 1" -> (Square.A6 -> Square.B5)
).foreach: (fen, move) =>
val game = fenToGame(FullFen(fen), Antichess)
assert(game.position.playerHasInsufficientMaterial)
assertNot(game.position.opponentHasInsufficientMaterial)
val updated = game.playMoves(move).get
assertNot(updated.position.playerHasInsufficientMaterial)
assert(updated.position.opponentHasInsufficientMaterial)

test("Not be drawn on insufficient mating material"):
val position = FullFen("4K3/8/1b6/8/8/8/5B2/3k4 b - -")
Expand Down
6 changes: 3 additions & 3 deletions test-kit/src/test/scala/format/BinaryFenTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ class BinaryFenTest extends ScalaCheckSuite:
forAll: (v: Long) =>
val builder = ArrayBuilder.ofByte()
BinaryFen.implementation.writeLong(builder, v)
assertEquals(BinaryFen.implementation.readLong(builder.result.iterator), v)
assertEquals(BinaryFen.implementation.readLong(builder.result().iterator), v)

test("leb128 roundtrip"):
forAll(Gen.posNum[Int]): (v: Int) =>
val builder = ArrayBuilder.ofByte()
BinaryFen.implementation.writeLeb128(builder, v)
assertEquals(BinaryFen.implementation.readLeb128(builder.result.iterator), v)
assertEquals(BinaryFen.implementation.readLeb128(builder.result().iterator), v)

private val genNibble = Gen.chooseNum[Int](0, 15)

test("nibbles roundtrip"):
forAll(genNibble, genNibble): (lo: Int, hi: Int) =>
val builder = ArrayBuilder.ofByte()
BinaryFen.implementation.writeNibbles(builder, lo, hi)
assertEquals(BinaryFen.implementation.readNibbles(builder.result.iterator), (lo, hi))
assertEquals(BinaryFen.implementation.readNibbles(builder.result().iterator), (lo, hi))

test("rewrite fixpoint"):
forAll: (bytes: Array[Byte]) =>
Expand Down