Skip to content

Commit

Permalink
Merge branch 'master' into Support_impossible_checks_when_validate_po…
Browse files Browse the repository at this point in the history
…sition
  • Loading branch information
lenguyenthanh authored Jul 17, 2023
2 parents 32993c2 + 77f5f85 commit 6e7aa86
Show file tree
Hide file tree
Showing 31 changed files with 554 additions and 744 deletions.
2 changes: 1 addition & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = "3.7.9"
version = "3.7.10"
runner.dialect = scala3

align.preset = more
Expand Down
3 changes: 1 addition & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ lazy val scalachess = Project("scalachess", file(".")).settings(
"org.scalameta" %% "munit" % "1.0.0-M8" % Test,
"org.scalacheck" %% "scalacheck" % "1.17.0" % Test,
"org.scalameta" %% "munit-scalacheck" % "1.0.0-M8" % Test,
"com.github.lenguyenthanh" % "compression" % "aacf55bea2" % Test, // a fork of lichess compression which public everything so we can use it for testing.
"com.disneystreaming" %% "weaver-cats" % "0.8.3" % Test,
"com.disneystreaming" %% "weaver-scalacheck" % "0.8.3" % Test,
"co.fs2" %% "fs2-core" % "3.7.0" % Test,
Expand Down Expand Up @@ -37,7 +36,7 @@ lazy val scalachess = Project("scalachess", file(".")).settings(
)

ThisBuild / organization := "org.lichess"
ThisBuild / version := "15.4.3"
ThisBuild / version := "15.4.4"
ThisBuild / scalaVersion := "3.3.0"
ThisBuild / licenses += "MIT" -> url("https://opensource.org/licenses/MIT")

Expand Down
47 changes: 35 additions & 12 deletions src/main/scala/Castles.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,21 @@ package chess

import cats.syntax.all.*

import bitboard.OpaqueBitboard
import bitboard.Bitboard
import Square.*

import scala.annotation.targetName

opaque type Castles = Long
object Castles extends OpaqueBitboard[Castles]:
object Castles:

extension (c: Castles)

inline def can(inline color: Color): Boolean = c.intersects(Bitboard.rank(color.backRank))
inline def can(inline color: Color): Boolean = Bitboard.rank(color.backRank).intersects(c)
inline def can(inline color: Color, inline side: Side) = c.contains(color.at(side))

def isEmpty = c == 0L

def whiteKingSide: Boolean = c.contains(H1)
def whiteQueenSide: Boolean = c.contains(A1)
def blackKingSide: Boolean = c.contains(H8)
Expand All @@ -23,7 +26,7 @@ object Castles extends OpaqueBitboard[Castles]:
c & Bitboard.rank(color.lastRank)

def without(color: Color, side: Side): Castles =
c & ~color.at(side).bb
c & ~color.at(side).bl

def add(color: Color, side: Side): Castles =
c.addSquare(color.at(side))
Expand All @@ -41,7 +44,25 @@ object Castles extends OpaqueBitboard[Castles]:

def toSeq: Array[Boolean] = Array(whiteKingSide, whiteQueenSide, blackKingSide, blackQueenSide)

extension (b: Boolean) inline def at(square: Square) = if b then square.bb else empty
inline def unary_~ : Castles = ~c
inline infix def &(inline o: Long): Castles = c & o
inline infix def ^(inline o: Long): Castles = c ^ o
inline infix def |(inline o: Long): Castles = c | o

@targetName("andB")
inline infix def &(o: Bitboard): Castles = c & o.value
@targetName("xorB")
inline infix def ^(o: Bitboard): Castles = c ^ o.value
@targetName("orB")
inline infix def |(o: Bitboard): Castles = c | o.value

def value: Long = c
def contains(square: Square): Boolean =
(c & (1L << square.value)) != 0L

def addSquare(square: Square): Castles = c | square.bl

extension (b: Boolean) inline def at(square: Square) = if b then square.bl else none

extension (color: Color)
inline def at(side: Side): Square =
Expand All @@ -65,13 +86,16 @@ object Castles extends OpaqueBitboard[Castles]:
blackKingSide.at(Black.kingSide) |
blackQueenSide.at(Black.queenSide)

def apply(bb: Bitboard): Castles = bb.value
inline def apply(inline xs: Iterable[Square]): Castles = xs.foldLeft(none)((b, s) => b | s.bl)

def apply(str: String): Castles = str match
case "-" => empty
case "-" => none
case _ =>
str.toList
.traverse(charToSquare)
.map(Bitboard(_).value)
.getOrElse(empty)
.foldM(none): (acc, c) =>
charToSquare(c).map(acc.addSquare)
.getOrElse(none)

val charToSquare: (c: Char) => Option[Square] =
case 'k' => Some(H8)
Expand All @@ -80,6 +104,5 @@ object Castles extends OpaqueBitboard[Castles]:
case 'Q' => Some(A1)
case _ => None

val all: Castles = CORNERS
val init: Castles = all
val none: Castles = empty
val init: Castles = 0x8100000000000081L
val none: Castles = 0L
4 changes: 2 additions & 2 deletions src/main/scala/History.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ case class CheckCount(white: Int = 0, black: Int = 0):
case class History(
lastMove: Option[Uci] = None,
positionHashes: PositionHash = Monoid[PositionHash].empty,
castles: Castles = Castles.all,
castles: Castles = Castles.init,
checkCount: CheckCount = CheckCount(0, 0),
unmovedRooks: UnmovedRooks,
halfMoveClock: HalfMoveClock = HalfMoveClock.initial
Expand Down Expand Up @@ -80,6 +80,6 @@ object History:
)

def castle(color: Color, kingSide: Boolean, queenSide: Boolean) =
History(castles = Castles.all.update(color, kingSide, queenSide), unmovedRooks = UnmovedRooks.corners)
History(castles = Castles.init.update(color, kingSide, queenSide), unmovedRooks = UnmovedRooks.corners)

def noCastle = History(castles = Castles.none, unmovedRooks = UnmovedRooks.none)
8 changes: 3 additions & 5 deletions src/main/scala/Move.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package chess

import chess.format.Uci
import bitboard.Bitboard.*
import cats.syntax.option.*
import cats.kernel.Monoid
import chess.bitboard.Bitboard
import cats.Monoid

case class Move(
piece: Piece,
Expand Down Expand Up @@ -48,7 +46,7 @@ case class Move(
if captures then
unmovedRooks.side(dest) match
case Some(result) =>
unmovedRooks = unmovedRooks & ~dest.bb
unmovedRooks = unmovedRooks & ~dest.bl
result match
case Some(side) =>
castleRights = castleRights.without(!piece.color, side)
Expand All @@ -63,7 +61,7 @@ case class Move(
if piece is Rook then
unmovedRooks.side(orig) match
case Some(result) =>
unmovedRooks = unmovedRooks & ~orig.bb
unmovedRooks = unmovedRooks & ~orig.bl
result match
case Some(side) =>
castleRights = castleRights.without(piece.color, side)
Expand Down
25 changes: 13 additions & 12 deletions src/main/scala/Situation.scala
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,20 @@ case class Situation(board: Board, color: Color):
(king.kingAttacks & mask).flatMap(to => normalMove(king, to, King, isOccupied(to)))

def genSafeKing(mask: Bitboard): List[Move] =
ourKing.fold(Nil)(king =>
for
to <- king.kingAttacks & mask
if board.attackers(to, !color).isEmpty
move <- normalMove(king, to, King, isOccupied(to))
yield move
)
ourKing.fold(Nil)(genSafeKing(_, mask))

def genSafeKing(king: Square, mask: Bitboard): List[Move] =
for
to <- king.kingAttacks & mask
if board.attackers(to, !color).isEmpty
move <- normalMove(king, to, King, isOccupied(to))
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 = history.unmovedRooks & Bitboard.rank(color.backRank) & board.rooks
val rooks = Bitboard.rank(color.backRank) & board.rooks & history.unmovedRooks.value
for
rook <- rooks
toKingFile = if rook < king then File.C else File.G
Expand All @@ -254,14 +255,14 @@ case class Situation(board: Board, color: Color):
if variant.chess960 || variant.fromPosition
then Bitboard.between(king, rook) | Bitboard.between(king, kingTo)
else Bitboard.between(king, rook)
if (path & board.occupied & ~rook.bb).isEmpty
kingPath = Bitboard.between(king, kingTo) | king.bb
if kingPath.forall(variant.castleCheckSafeSquare(board, _, color, board.occupied ^ king.bb))
if (path & board.occupied & ~rook.bl).isEmpty
kingPath = Bitboard.between(king, kingTo) | king.bl
if kingPath.forall(variant.castleCheckSafeSquare(board, _, color, board.occupied ^ king.bl))
if variant.castleCheckSafeSquare(
board,
kingTo,
color,
board.occupied ^ king.bb ^ rook.bb ^ rookTo.bb
board.occupied ^ king.bl ^ rook.bl ^ rookTo.bl
)
moves <- castle(king, kingTo, rook, rookTo)
yield moves
Expand Down
3 changes: 3 additions & 0 deletions src/main/scala/Square.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ object Square extends OpaqueInt[Square]:
inline def withRankOf(inline o: Square): Square = withRank(o.rank)
inline def withFileOf(inline o: Square): Square = withFile(o.file)

inline def bb: Bitboard = Bitboard(1L << s.value)
inline def bl: Long = 1L << s.value

end extension

inline def apply(inline file: File, inline rank: Rank): Square = file.value + 8 * rank.value
Expand Down
39 changes: 30 additions & 9 deletions src/main/scala/UnmovedRooks.scala
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package chess

import bitboard.OpaqueBitboard
import bitboard.Bitboard
import scala.annotation.targetName

opaque type UnmovedRooks = Long
object UnmovedRooks extends OpaqueBitboard[UnmovedRooks]:
object UnmovedRooks:
// for lila testing only
val default: UnmovedRooks = UnmovedRooks(Bitboard.rank(Rank.First) | Bitboard.rank(Rank.Eighth))
val corners: UnmovedRooks = CORNERS
val none: UnmovedRooks = empty
val corners: UnmovedRooks = 0x8100000000000081L
val none: UnmovedRooks = 0L

def apply(b: Bitboard): UnmovedRooks = b.value
@targetName("applyUnmovedRooks")
def apply(b: Bitboard): UnmovedRooks = b.value
def apply(l: Long): UnmovedRooks = l
inline def apply(inline xs: Iterable[Square]): UnmovedRooks = xs.foldLeft(none)((b, s) => b | s.bl)

// guess unmovedRooks from board
// we assume rooks are on their initial position
Expand All @@ -20,10 +23,13 @@ object UnmovedRooks extends OpaqueBitboard[UnmovedRooks]:
UnmovedRooks(wr | br)

extension (ur: UnmovedRooks)
def toList: List[Square] = ur.squares
inline def bb: Bitboard = Bitboard(ur)
def isEmpty = ur == 0L
def value: Long = ur
def toList: List[Square] = ur.bb.squares

def without(color: Color): UnmovedRooks =
ur & Bitboard.rank(color.lastRank)
ur & Bitboard.rank(color.lastRank).value

// Try to guess the side of the rook at postion `square`
// If the position is not a ummovedRook return None
Expand All @@ -32,10 +38,25 @@ object UnmovedRooks extends OpaqueBitboard[UnmovedRooks]:
// If there are two rooks on the same rank, return the side of the rook
def side(square: Square): Option[Option[Side]] =
val rook = square.bb
if ur.isDisjoint(rook) then None
if rook.isDisjoint(ur) then None
else
(ur & ~rook & Bitboard.rank(square.rank)).first match
(Bitboard.rank(square.rank) & ~rook & ur.value).first match
case Some(otherRook) =>
if otherRook.file > square.file then Some(Some(QueenSide))
else Some(Some(KingSide))
case None => Some(None)

def contains(square: Square): Boolean =
(ur & (1L << square.value)) != 0L

inline def unary_~ : UnmovedRooks = ~ur
inline infix def &(inline o: Long): UnmovedRooks = ur & o
inline infix def ^(inline o: Long): UnmovedRooks = ur ^ o
inline infix def |(inline o: Long): UnmovedRooks = ur | o

@targetName("and")
inline infix def &(o: Bitboard): UnmovedRooks = ur & o.value
@targetName("xor")
inline infix def ^(o: Bitboard): UnmovedRooks = ur ^ o.value
@targetName("or")
inline infix def |(o: Bitboard): UnmovedRooks = ur | o.value
95 changes: 95 additions & 0 deletions src/main/scala/bitboard/Attacks.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package chess
package bitboard

object Attacks:
private val all = -1L

private[bitboard] val RANKS = Array.fill(8)(0L)
private[bitboard] val FILES = Array.fill(8)(0L)
private[bitboard] val BETWEEN = Array.ofDim[Long](64, 64)
private[bitboard] val RAYS = Array.ofDim[Long](64, 64)

// Large overlapping attack table indexed using magic multiplication.
private[bitboard] val ATTACKS = Array.fill(88772)(0L)
private[bitboard] val KNIGHT_ATTACKS = Array.fill(64)(0L)
private[bitboard] val KING_ATTACKS = Array.fill(64)(0L)
private[bitboard] val WHITE_PAWN_ATTACKS = Array.fill(64)(0L)
private[bitboard] val BLACK_PAWN_ATTACKS = Array.fill(64)(0L)

private val KNIGHT_DELTAS = Array[Int](17, 15, 10, 6, -17, -15, -10, -6)
private val BISHOP_DELTAS = Array[Int](7, -7, 9, -9)
private val ROOK_DELTAS = Array[Int](1, -1, 8, -8)
private val KING_DELTAS = Array[Int](1, 7, 8, 9, -1, -7, -8, -9)
private val WHITE_PAWN_DELTAS = Array[Int](7, 9)
private val BLACK_PAWN_DELTAS = Array[Int](-7, -9)

/** Slow attack set generation. Used only to bootstrap the attack tables.
*/
private def slidingAttacks(square: Int, occupied: Long, deltas: Array[Int]): Long =
var attacks = 0L
deltas.foreach: delta =>
var sq = square
var i = 0
while
i += 1
sq += delta
val con = (sq < 0 || 64 <= sq || distance(sq, sq - delta) > 2)
if !con then attacks |= 1L << sq

!(occupied.contains(sq) || con)
do ()
attacks

private def initMagics(square: Int, magic: Magic, shift: Int, deltas: Array[Int]) =
var subset = 0L
while
val attack = slidingAttacks(square, subset, deltas)
val idx = ((magic.factor * subset) >>> (64 - shift)).toInt + magic.offset
ATTACKS(idx) = attack

// Carry-rippler trick for enumerating subsets.
subset = (subset - magic.mask) & magic.mask

subset != 0
do ()

private def initialize() =
(0 until 8).foreach: i =>
RANKS(i) = 0xffL << (i * 8)
FILES(i) = 0x0101010101010101L << i

val squareRange = 0 until 64
squareRange.foreach: sq =>
KNIGHT_ATTACKS(sq) = slidingAttacks(sq, all, KNIGHT_DELTAS)
KING_ATTACKS(sq) = slidingAttacks(sq, all, KING_DELTAS)
WHITE_PAWN_ATTACKS(sq) = slidingAttacks(sq, all, WHITE_PAWN_DELTAS)
BLACK_PAWN_ATTACKS(sq) = slidingAttacks(sq, all, BLACK_PAWN_DELTAS)

initMagics(sq, Magic.ROOK(sq), 12, ROOK_DELTAS)
initMagics(sq, Magic.BISHOP(sq), 9, BISHOP_DELTAS)

for
a <- squareRange
b <- squareRange
_ =
if slidingAttacks(a, 0, ROOK_DELTAS).contains(b) then
BETWEEN(a)(b) = slidingAttacks(a, 1L << b, ROOK_DELTAS) & slidingAttacks(b, 1L << a, ROOK_DELTAS)
RAYS(a)(b) =
(1L << a) | (1L << b) | slidingAttacks(a, 0, ROOK_DELTAS) & slidingAttacks(b, 0, ROOK_DELTAS)
else if slidingAttacks(a, 0, BISHOP_DELTAS).contains(b) then
BETWEEN(a)(b) =
slidingAttacks(a, 1L << b, BISHOP_DELTAS) & slidingAttacks(b, 1L << a, BISHOP_DELTAS)
RAYS(a)(b) =
(1L << a) | (1L << b) | slidingAttacks(a, 0, BISHOP_DELTAS) & slidingAttacks(b, 0, BISHOP_DELTAS)
yield ()

initialize()

extension (l: Long)
private def contains(s: Int): Boolean =
(l & (1L << s)) != 0L

private def distance(a: Int, b: Int): Int =
inline def file(s: Int) = s & 7
inline def rank(s: Int) = s >>> 3
Math.max(Math.abs(file(a) - file(b)), Math.abs(rank(a) - rank(b)))
Loading

0 comments on commit 6e7aa86

Please sign in to comment.