scalafmt align.preset = none

This commit is contained in:
Thibault Duplessis
2025-07-25 09:31:57 +02:00
parent d1110d0c0b
commit 4765295c1c
128 changed files with 1284 additions and 1284 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
version = "3.9.8"
runner.dialect = scala3
align.preset = more
align.preset = none
maxColumn = 110
spaces.inImportCurlyBraces = true
rewrite.rules = [SortModifiers, AvoidInfix]
@@ -23,9 +23,9 @@ class BinaryFenBench:
private val Work: Long = 10
@Param(Array("10", "100", "1000"))
var games: Int = scala.compiletime.uninitialized
var games: Int = scala.compiletime.uninitialized
var sits: List[Position.AndFullMoveNumber] = scala.compiletime.uninitialized
var fens: List[BinaryFen] = scala.compiletime.uninitialized
var fens: List[BinaryFen] = scala.compiletime.uninitialized
@Setup
def setup(): Unit =
@@ -41,7 +41,7 @@ class BinaryFenBench:
@Benchmark
def write(bh: Blackhole) =
val games = this.sits
var i = 0
var i = 0
while i < games.size do
val game = games(i)
Blackhole.consumeCPU(Work)
@@ -51,7 +51,7 @@ class BinaryFenBench:
@Benchmark
def read(bh: Blackhole) =
val games = this.fens
var i = 0
var i = 0
while i < games.size do
val fen = games(i)
Blackhole.consumeCPU(Work)
@@ -23,14 +23,14 @@ class DestinationsBench:
@Param(Array("100"))
var games: Int = scala.compiletime.uninitialized
var threecheckInput: List[Position] = scala.compiletime.uninitialized
var antichessInput: List[Position] = scala.compiletime.uninitialized
var atomicInput: List[Position] = scala.compiletime.uninitialized
var crazyhouseInput: List[Position] = scala.compiletime.uninitialized
var threecheckInput: List[Position] = scala.compiletime.uninitialized
var antichessInput: List[Position] = scala.compiletime.uninitialized
var atomicInput: List[Position] = scala.compiletime.uninitialized
var crazyhouseInput: List[Position] = scala.compiletime.uninitialized
var racingkingsInput: List[Position] = scala.compiletime.uninitialized
var hordeInput: List[Position] = scala.compiletime.uninitialized
var randomInput: List[Position] = scala.compiletime.uninitialized
var trickyInput: List[Position] = scala.compiletime.uninitialized
var hordeInput: List[Position] = scala.compiletime.uninitialized
var randomInput: List[Position] = scala.compiletime.uninitialized
var trickyInput: List[Position] = scala.compiletime.uninitialized
@Setup
def setup(): Unit =
@@ -23,14 +23,14 @@ class FenReaderBench:
@Param(Array("100"))
var games: Int = scala.compiletime.uninitialized
var threecheckInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var antichessInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var atomicInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var crazyhouseInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var threecheckInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var antichessInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var atomicInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var crazyhouseInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var racingkingsInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var hordeInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var randomInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var trickyInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var hordeInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var randomInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
var trickyInput: List[(Variant, FullFen)] = scala.compiletime.uninitialized
@Setup
def setup(): Unit =
@@ -23,14 +23,14 @@ class FenWriterBench:
@Param(Array("100"))
var games: Int = scala.compiletime.uninitialized
var threecheckInput: List[Game] = scala.compiletime.uninitialized
var antichessInput: List[Game] = scala.compiletime.uninitialized
var atomicInput: List[Game] = scala.compiletime.uninitialized
var crazyhouseInput: List[Game] = scala.compiletime.uninitialized
var threecheckInput: List[Game] = scala.compiletime.uninitialized
var antichessInput: List[Game] = scala.compiletime.uninitialized
var atomicInput: List[Game] = scala.compiletime.uninitialized
var crazyhouseInput: List[Game] = scala.compiletime.uninitialized
var racingkingsInput: List[Game] = scala.compiletime.uninitialized
var hordeInput: List[Game] = scala.compiletime.uninitialized
var randomInput: List[Game] = scala.compiletime.uninitialized
var trickyInput: List[Game] = scala.compiletime.uninitialized
var hordeInput: List[Game] = scala.compiletime.uninitialized
var randomInput: List[Game] = scala.compiletime.uninitialized
var trickyInput: List[Game] = scala.compiletime.uninitialized
@Setup
def setup(): Unit =
@@ -25,14 +25,14 @@ class PerftBench:
@Param(Array("10000", "100000", "1000000", "10000000"))
var nodes: Long = scala.compiletime.uninitialized
var threecheckPerfts: List[Perft] = scala.compiletime.uninitialized
var antichessPerfts: List[Perft] = scala.compiletime.uninitialized
var atomicPerfts: List[Perft] = scala.compiletime.uninitialized
var crazyhousePerfts: List[Perft] = scala.compiletime.uninitialized
var threecheckPerfts: List[Perft] = scala.compiletime.uninitialized
var antichessPerfts: List[Perft] = scala.compiletime.uninitialized
var atomicPerfts: List[Perft] = scala.compiletime.uninitialized
var crazyhousePerfts: List[Perft] = scala.compiletime.uninitialized
var racingkingsPerfts: List[Perft] = scala.compiletime.uninitialized
var hordePerfts: List[Perft] = scala.compiletime.uninitialized
var randomPerfts: List[Perft] = scala.compiletime.uninitialized
var trickyPerfts: List[Perft] = scala.compiletime.uninitialized
var hordePerfts: List[Perft] = scala.compiletime.uninitialized
var randomPerfts: List[Perft] = scala.compiletime.uninitialized
var trickyPerfts: List[Perft] = scala.compiletime.uninitialized
@Setup
def setup(): Unit =
@@ -19,8 +19,8 @@ class PgnBench:
// the unit of CPU work per iteration
private val Work: Long = 10
var pgnStrs: List[PgnStr] = scala.compiletime.uninitialized
var pgns: List[Pgn] = scala.compiletime.uninitialized
var pgnStrs: List[PgnStr] = scala.compiletime.uninitialized
var pgns: List[Pgn] = scala.compiletime.uninitialized
var parsedPgn: List[ParsedPgn] = scala.compiletime.uninitialized
@Setup
@@ -32,7 +32,7 @@ class PgnBench:
@Benchmark
def pgnFullParser(bh: Blackhole) =
var games = this.pgnStrs
var i = 0
var i = 0
while i < games.size do
val game = games(i)
Blackhole.consumeCPU(Work)
@@ -42,7 +42,7 @@ class PgnBench:
@Benchmark
def pgnMainlineWithMetasParser(bh: Blackhole) =
var games = this.pgnStrs
var i = 0
var i = 0
while i < games.size do
val game = games(i)
Blackhole.consumeCPU(Work)
@@ -52,7 +52,7 @@ class PgnBench:
@Benchmark
def pgnMainlineParser(bh: Blackhole) =
var games = this.pgnStrs
var i = 0
var i = 0
while i < games.size do
val game = games(i)
Blackhole.consumeCPU(Work)
@@ -23,7 +23,7 @@ class PlayBench:
private val Work: Long = 10
var dividerGames: List[List[Board]] = scala.compiletime.uninitialized
var gameMoves: List[List[SanStr]] = scala.compiletime.uninitialized
var gameMoves: List[List[SanStr]] = scala.compiletime.uninitialized
def gameReplay(sans: String) =
Standard.initialPosition.playBoards(SanStr.from(sans.split(' ')).toList).toOption.get
@@ -32,7 +32,7 @@ class PlayBench:
def setup() =
dividerGames = Fixtures.prod500standard.map(gameReplay)
var nb = 50
var nb = 50
var games = Fixtures.prod500standard
gameMoves = games.take(nb).map(g => SanStr.from(g.split(' ').toList))
@@ -47,7 +47,7 @@ class PlayBench:
@Benchmark
def playMoveOrDropWithPly(bh: Blackhole) =
val games = this.gameMoves
var i = 0
var i = 0
while i < games.size do
val moves = games(i)
Blackhole.consumeCPU(Work)
+28 -28
View File
@@ -7,41 +7,41 @@ import bitboard.Attacks.*
opaque type Bitboard = Long
object Bitboard:
inline def apply(inline l: Long): Bitboard = l
inline def apply(inline l: Long): Bitboard = l
inline def apply(inline xs: Iterable[Square]): Bitboard = xs.foldLeft(empty)((b, s) => b | s.bl)
inline def apply(inline xs: Square*): Bitboard = apply(xs.toList)
inline def apply(inline xs: Square*): Bitboard = apply(xs.toList)
val empty: Bitboard = 0L
val all: Bitboard = -1L
val all: Bitboard = -1L
// E4, D4, E5, D5
val center = 0x1818000000L
val firstRank: Bitboard = 0xffL
val lastRank: Bitboard = 0xffL << 56
val lastRank: Bitboard = 0xffL << 56
// all light squares
val lightSquares: Bitboard = 0x55aa55aa55aa55aaL
// all dark squares
val darkSquares: Bitboard = 0xaa55aa55aa55aa55L
inline def file(inline f: File): Bitboard = FILES(f.value)
inline def rank(inline r: Rank): Bitboard = RANKS(r.value)
inline def file(inline f: File): Bitboard = FILES(f.value)
inline def rank(inline r: Rank): Bitboard = RANKS(r.value)
inline def ray(inline from: Square, inline to: Square): Bitboard = RAYS(from.value)(to.value)
def aligned(a: Square, b: Square, c: Square): Boolean = ray(a, b).contains(c)
def between(a: Square, b: Square): Bitboard = BETWEEN(a.value)(b.value)
def between(a: Square, b: Square): Bitboard = BETWEEN(a.value)(b.value)
extension (l: Long)
private def lsb: Square = Square.unsafe(java.lang.Long.numberOfTrailingZeros(l))
private def msb: Square = Square.unsafe(63 - java.lang.Long.numberOfLeadingZeros(l))
extension (a: Bitboard)
inline def value: Long = a
inline def unary_~ : Bitboard = (~a)
inline infix def &(inline o: Long): Bitboard = (a & o)
inline infix def ^(inline o: Long): Bitboard = (a ^ o)
inline infix def |(inline o: Long): Bitboard = (a | o)
inline infix def <<(inline o: Int): Bitboard = (a << o)
inline def value: Long = a
inline def unary_~ : Bitboard = (~a)
inline infix def &(inline o: Long): Bitboard = (a & o)
inline infix def ^(inline o: Long): Bitboard = (a ^ o)
inline infix def |(inline o: Long): Bitboard = (a | o)
inline infix def <<(inline o: Int): Bitboard = (a << o)
inline infix def >>>(inline o: Int): Bitboard = (a >>> o)
@targetName("and")
inline infix def &(o: Bitboard): Bitboard = (a & o)
@@ -50,7 +50,7 @@ object Bitboard:
@targetName("or")
inline infix def |(o: Bitboard): Bitboard = (a | o)
inline def isEmpty: Boolean = a == 0L
inline def isEmpty: Boolean = a == 0L
inline def nonEmpty: Boolean = !isEmpty
inline def supersetOf(l: Long): Boolean =
@@ -73,7 +73,7 @@ object Bitboard:
inline def contains(file: File, rank: Rank): Boolean =
(a & file.bb & rank.bb) != 0L
def add(square: Square): Bitboard = a | square.bl
def add(square: Square): Bitboard = a | square.bl
def remove(square: Square): Bitboard = a & ~square.bl
def move(from: Square, to: Square): Bitboard =
@@ -122,7 +122,7 @@ object Bitboard:
// return list of square that sorted ascendingly
def squares: List[Square] =
var b = a
var b = a
val builder = List.newBuilder[Square]
while b != 0L
do
@@ -134,7 +134,7 @@ object Bitboard:
squares.toSet
def first[B](f: Square => Option[B]): Option[B] =
var b = a
var b = a
var result: Option[B] = None
while b != 0L && result.isEmpty
do
@@ -143,7 +143,7 @@ object Bitboard:
result
def last[B](f: Square => Option[B]): Option[B] =
var b = a
var b = a
var result: Option[B] = None
while b != 0L && result.isEmpty
do
@@ -153,7 +153,7 @@ object Bitboard:
// the smallest square that satisfies the predicate
def find(f: Square => Boolean): Option[Square] =
var b = a
var b = a
var result: Option[Square] = None
while b != 0L && result.isEmpty
do
@@ -163,7 +163,7 @@ object Bitboard:
// the larget square that satisfies the predicate
def findLast(f: Square => Boolean): Option[Square] =
var b = a
var b = a
var result: Option[Square] = None
while b != 0L && result.isEmpty
do
@@ -172,7 +172,7 @@ object Bitboard:
result
def fold[B](init: B)(f: (B, Square) => B): B =
var b = a
var b = a
var result = init
while b != 0L
do
@@ -182,7 +182,7 @@ object Bitboard:
def filter(f: Square => Boolean): List[Square] =
val builder = List.newBuilder[Square]
var b = a
var b = a
while b != 0L
do
if f(b.lsb) then builder += b.lsb
@@ -200,7 +200,7 @@ object Bitboard:
b &= (b - 1L)
def forall(f: Square => Boolean): Boolean =
var b = a
var b = a
var result = true
while b != 0L && result
do
@@ -209,7 +209,7 @@ object Bitboard:
result
def exists(f: Square => Boolean): Boolean =
var b = a
var b = a
var result = false
while b != 0L && !result
do
@@ -218,7 +218,7 @@ object Bitboard:
result
def flatMap[B](f: Square => IterableOnce[B]): List[B] =
var b = a
var b = a
val builder = List.newBuilder[B]
while b != 0L
do
@@ -227,7 +227,7 @@ object Bitboard:
builder.result
def map[B](f: Square => B): List[B] =
var b = a
var b = a
val builder = List.newBuilder[B]
while b != 0L
do
@@ -236,9 +236,9 @@ object Bitboard:
builder.result
def iterator: Iterator[Square] = new:
private var b = a
private var b = a
override inline def hasNext: Boolean = b != 0L
override inline def next: Square =
override inline def next: Square =
val result = b.lsb
b &= (b - 1L)
result
+12 -12
View File
@@ -310,26 +310,26 @@ object Board:
)
)
def fromMap(pieces: PieceMap): Board =
var pawns = Bitboard.empty
var knights = Bitboard.empty
var bishops = Bitboard.empty
var rooks = Bitboard.empty
var queens = Bitboard.empty
var kings = Bitboard.empty
var white = Bitboard.empty
var black = Bitboard.empty
var pawns = Bitboard.empty
var knights = Bitboard.empty
var bishops = Bitboard.empty
var rooks = Bitboard.empty
var queens = Bitboard.empty
var kings = Bitboard.empty
var white = Bitboard.empty
var black = Bitboard.empty
var occupied = Bitboard.empty
pieces.foreach: (s, p) =>
val position = s.bb
occupied |= position
p.role match
case Pawn => pawns |= position
case Pawn => pawns |= position
case Knight => knights |= position
case Bishop => bishops |= position
case Rook => rooks |= position
case Queen => queens |= position
case King => kings |= position
case Rook => rooks |= position
case Queen => queens |= position
case King => kings |= position
p.color match
case Color.White => white |= position
+7 -7
View File
@@ -23,8 +23,8 @@ case class ByColor[A](white: A, black: A):
def map[B](fw: A => B, fb: A => B): ByColor[B] = ByColor(fw(white), fb(black))
def map[B](f: A => B): ByColor[B] = map(f, f)
def mapList[B](f: A => B): List[B] = List(f(white), f(black))
def map[B](f: A => B): ByColor[B] = map(f, f)
def mapList[B](f: A => B): List[B] = List(f(white), f(black))
def mapReduce[B, C](f: A => B)(r: (B, B) => C): C = r(f(white), f(black))
def mapWithColor[B](f: (Color, A) => B): ByColor[B] = ByColor(f(White, white), f(Black, black))
@@ -33,13 +33,13 @@ case class ByColor[A](white: A, black: A):
def zip[B, C](other: ByColor[B], f: (A, B) => C): ByColor[C] =
ByColor(f(white, other.white), f(black, other.black))
def zipColor: ByColor[(Color, A)] = ByColor((White, white), (Black, black))
def toPair: (A, A) = (white, black)
def toPair: (A, A) = (white, black)
lazy val all: List[A] = List(white, black)
def reduce[B](f: (A, A) => B): B = f(white, black)
def fold[B](init: B)(f: (B, A) => B): B = f(f(init, white), black)
def fold[B](init: B)(f: (B, A) => B): B = f(f(init, white), black)
def fold[B](init: B)(f: (B, Color, A) => B): B = f(f(init, White, white), Black, black)
def foreach[U](f: A => U): Unit =
@@ -95,7 +95,7 @@ case class ByColor[A](white: A, black: A):
(f(white), f(black)).mapN(r)
object ByColor:
inline def fill[A](a: A): ByColor[A] = ByColor(a, a)
inline def fill[A](a: A): ByColor[A] = ByColor(a, a)
inline def fromPair[A](p: (A, A)): ByColor[A] = ByColor(p._1, p._2)
def apply[A](f: Color => A)(using NotGiven[f.type <:< PartialFunction[Color, A]]): ByColor[A] =
@@ -112,7 +112,7 @@ object ByColor:
def zero = ByColor.fill(Zero[A].zero)
given [A: Monoid]: Monoid[ByColor[A]] with
def empty = ByColor.fill(Monoid[A].empty)
def empty = ByColor.fill(Monoid[A].empty)
def combine(x: ByColor[A], y: ByColor[A]) =
ByColor(Monoid[A].combine(x.white, y.white), Monoid[A].combine(x.black, y.black))
@@ -129,7 +129,7 @@ object ByColor:
def traverse[G[_]: Applicative, A, B](fa: ByColor[A])(f: A => G[B]): G[ByColor[B]] =
fa.traverse(f)
def pure[A](a: A): ByColor[A] = ByColor.fill(a)
def pure[A](a: A): ByColor[A] = ByColor.fill(a)
def ap[A, B](ff: ByColor[A => B])(fa: ByColor[A]): ByColor[B] =
ByColor(ff.white(fa.white), ff.black(fa.black))
+8 -8
View File
@@ -4,20 +4,20 @@ import cats.Functor
case class ByRole[A](pawn: A, knight: A, bishop: A, rook: A, queen: A, king: A):
def apply(role: Role): A = role match
case Pawn => pawn
case Pawn => pawn
case Knight => knight
case Bishop => bishop
case Rook => rook
case Queen => queen
case King => king
case Rook => rook
case Queen => queen
case King => king
inline def update(role: Role, f: A => A): ByRole[A] = role match
case Pawn => copy(pawn = f(pawn))
case Pawn => copy(pawn = f(pawn))
case Knight => copy(knight = f(knight))
case Bishop => copy(bishop = f(bishop))
case Rook => copy(rook = f(rook))
case Queen => copy(queen = f(queen))
case King => copy(king = f(king))
case Rook => copy(rook = f(rook))
case Queen => copy(queen = f(queen))
case King => copy(king = f(king))
inline def find(f: A => Boolean): Option[A] =
if f(pawn) then Some(pawn)
+2 -2
View File
@@ -12,7 +12,7 @@ import scala.annotation.targetName
* a typeclass that can apply a Moveable and produce a new state and a MoveOrDrop.
*/
trait CanPlay[A]:
type Step = (next: A, move: MoveOrDrop, ply: Ply)
type Step = (next: A, move: MoveOrDrop, ply: Ply)
type Result[B] = (state: A, moves: List[B], error: Option[ErrorStr])
extension (a: A)
/**
@@ -286,6 +286,6 @@ trait CanPlay[A]:
object CanPlay:
inline def makeError(currentPly: Ply, move: Moveable): ErrorStr =
val moveAt = currentPly.fullMoveNumber
val moveAt = currentPly.fullMoveNumber
val moveStr = move.rawString.getOrElse(move.toString)
ErrorStr(s"Cannot play $moveStr at move $moveAt by ${currentPly.turn.name}")
+11 -11
View File
@@ -9,14 +9,14 @@ object Castles:
extension (c: Castles)
inline def can(inline color: Color): Boolean = Bitboard.rank(color.backRank).intersects(c)
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 whiteKingSide: Boolean = c.contains(H1)
def whiteQueenSide: Boolean = c.contains(A1)
def blackKingSide: Boolean = c.contains(H8)
def blackKingSide: Boolean = c.contains(H8)
def blackQueenSide: Boolean = c.contains(A8)
def without(color: Color): Castles =
@@ -33,7 +33,7 @@ object Castles:
def toSeq: Array[Boolean] = Array(whiteKingSide, whiteQueenSide, blackKingSide, blackQueenSide)
inline def unary_~ : Castles = ~c
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
@@ -45,7 +45,7 @@ object Castles:
@targetName("orB")
inline infix def |(o: Bitboard): Castles = c | o.value
inline def value: Long = c
inline def value: Long = c
inline def bb: Bitboard = Bitboard(c)
inline def contains(inline square: Square): Boolean =
@@ -58,12 +58,12 @@ object Castles:
extension (color: Color)
inline def at(side: Side): Square =
(color, side) match
case (White, KingSide) => H1
case (White, KingSide) => H1
case (White, QueenSide) => A1
case (Black, KingSide) => H8
case (Black, KingSide) => H8
case (Black, QueenSide) => A8
inline def kingSide: Square = at(KingSide)
inline def kingSide: Square = at(KingSide)
inline def queenSide: Square = at(QueenSide)
def apply(
@@ -86,8 +86,8 @@ object Castles:
case 'q' => Some(A8)
case 'K' => Some(H1)
case 'Q' => Some(A1)
case _ => None
case _ => None
val init: Castles = 0x8100000000000081L
val none: Castles = 0L
val init: Castles = 0x8100000000000081L
val none: Castles = 0L
val black: Castles = 0x8100000000000000L
+6 -6
View File
@@ -16,14 +16,14 @@ object Centis extends RichOpaqueInt[Centis]:
inline def *(inline o: Int): Centis = centis * o
def roundTenths: Int = (if centis > 0 then centis + 5 else centis - 4) / 10
def roundTenths: Int = (if centis > 0 then centis + 5 else centis - 4) / 10
def roundSeconds: Seconds = Seconds(Math.round(centis * 0.01f))
inline def toSeconds: BigDecimal = java.math.BigDecimal.valueOf(centis, 2)
inline def millis: Long = centis * 10L
def toDuration: FiniteDuration = FiniteDuration(millis, MILLISECONDS)
inline def millis: Long = centis * 10L
def toDuration: FiniteDuration = FiniteDuration(millis, MILLISECONDS)
def *~(scalar: Float): Centis = ofFloat(scalar * centis)
def *~(scalar: Float): Centis = ofFloat(scalar * centis)
def /(div: Int): Option[Centis] = (div != 0).option(centis / div)
def avg(other: Centis): Centis = (centis + other.value) >> 1
@@ -36,7 +36,7 @@ object Centis extends RichOpaqueInt[Centis]:
given Monoid[Centis] with
def combine(c1: Centis, c2: Centis) = c1 + c2
val empty = 0
val empty = 0
def ofLong(l: Long): Centis =
try Math.toIntExact(l)
@@ -50,7 +50,7 @@ object Centis extends RichOpaqueInt[Centis]:
if d.unit eq MILLISECONDS then d.length
else d.toMillis
inline def ofFloat(f: Float): Centis = Math.round(f)
inline def ofFloat(f: Float): Centis = Math.round(f)
inline def ofDouble(d: Double): Centis = ofLong(Math.round(d))
inline def ofSeconds(s: Int): Centis = 100 * s
+5 -5
View File
@@ -80,15 +80,15 @@ case class Clock(
)
case Some(t) =>
val elapsed = toNow(t)
val lag = (~metrics.reportedLag(elapsed)).nonNeg
val lag = (~metrics.reportedLag(elapsed)).nonNeg
val player = players(color)
val player = players(color)
val (lagComp, lagTrack) = player.lag.onMove(lag)
val moveTime = (elapsed - lagComp).nonNeg
val clockActive = gameActive && moveTime < player.remaining
val inc = clockActive.so(player.increment)
val inc = clockActive.so(player.increment)
val newC = updatePlayer(color):
_.takeTime(moveTime - inc)
@@ -117,7 +117,7 @@ case class Clock(
def goBerserk(c: Color): Clock = updatePlayer(c)(_.copy(berserk = true))
def berserked(c: Color): Boolean = players(c).berserk
def lag(c: Color): LagTracker = players(c).lag
def lag(c: Color): LagTracker = players(c).lag
def lagCompAvg: Centis = players.mapReduce(~_.lag.compAvg)(_.avg(_))
@@ -192,7 +192,7 @@ object Clock:
case 30 => "½"
case 45 => "¾"
case 90 => "1.5"
case _ => limitFormatter.format(limitSeconds / 60d)
case _ => limitFormatter.format(limitSeconds / 60d)
def show: String = toString
+12 -12
View File
@@ -18,24 +18,24 @@ enum Color(val name: String, val letter: Char) derives Eq:
@targetName("negate")
def unary_! = fold(Black, White)
lazy val backRank: Rank = fold(Rank.First, Rank.Eighth)
lazy val thirdRank: Rank = fold(Rank.Third, Rank.Sixth)
lazy val fourthRank: Rank = fold(Rank.Fourth, Rank.Fifth)
lazy val fifthRank: Rank = fold(Rank.Fifth, Rank.Fourth)
lazy val sixthRank: Rank = fold(Rank.Sixth, Rank.Third)
lazy val seventhRank: Rank = fold(Rank.Seventh, Rank.Second)
lazy val lastRank: Rank = fold(Rank.Eighth, Rank.First)
lazy val passablePawnRank: Rank = fifthRank
lazy val backRank: Rank = fold(Rank.First, Rank.Eighth)
lazy val thirdRank: Rank = fold(Rank.Third, Rank.Sixth)
lazy val fourthRank: Rank = fold(Rank.Fourth, Rank.Fifth)
lazy val fifthRank: Rank = fold(Rank.Fifth, Rank.Fourth)
lazy val sixthRank: Rank = fold(Rank.Sixth, Rank.Third)
lazy val seventhRank: Rank = fold(Rank.Seventh, Rank.Second)
lazy val lastRank: Rank = fold(Rank.Eighth, Rank.First)
lazy val passablePawnRank: Rank = fifthRank
lazy val promotablePawnRank: Rank = lastRank
inline def -(inline role: Role) = Piece(this, role)
inline def pawn = this - Pawn
inline def pawn = this - Pawn
inline def bishop = this - Bishop
inline def knight = this - Knight
inline def rook = this - Rook
inline def queen = this - Queen
inline def king = this - King
inline def rook = this - Rook
inline def queen = this - Queen
inline def king = this - King
override def hashCode = fold(1, 2)
@@ -33,10 +33,10 @@ case class CorrespondenceClock(
object CorrespondenceClock:
val hourSeconds = 60 * 60
val daySeconds = 24 * hourSeconds
val daySeconds = 24 * hourSeconds
def apply(daysPerTurn: Int, turnColor: Color, lastMoveAt: Instant): CorrespondenceClock =
val increment = daysPerTurn * 24 * 60 * 60
val increment = daysPerTurn * 24 * 60 * 60
val secondsLeft = (lastMoveAt.toSeconds + increment - nowSeconds).toInt.max(0)
CorrespondenceClock(
increment = increment,
+5 -5
View File
@@ -6,12 +6,12 @@ import scala.annotation.switch
case class Division(middle: Option[Ply], end: Option[Ply], plies: Ply):
def openingSize: Ply = middle | plies
def middleSize: Option[Ply] = middle.map((end | plies) - _)
def endSize: Option[Ply] = end.map(plies - _)
def openingSize: Ply = middle | plies
def middleSize: Option[Ply] = middle.map((end | plies) - _)
def endSize: Option[Ply] = end.map(plies - _)
def openingBounds: Option[(Int, Ply)] = middle.map(0 -> _)
def middleBounds: Option[(Ply, Ply)] = (middle, end).tupled
def endBounds: Option[(Ply, Ply)] = end.map(_ -> plies)
def middleBounds: Option[(Ply, Ply)] = (middle, end).tupled
def endBounds: Option[(Ply, Ply)] = end.map(_ -> plies)
object Division:
val empty = Division(None, None, Ply.initial)
+3 -3
View File
@@ -6,14 +6,14 @@ object File:
extension (a: File)
inline def value: Int = a
inline infix def >(inline o: File): Boolean = a > o
inline infix def <(inline o: File): Boolean = a < o
inline infix def >(inline o: File): Boolean = a > o
inline infix def <(inline o: File): Boolean = a < o
inline infix def >=(inline o: File): Boolean = a >= o
inline infix def <=(inline o: File): Boolean = a <= o
inline def char: Char = (97 + a).toChar
inline def upperCaseChar: Char = (65 + a).toChar
inline def upperCaseChar: Char = (65 + a).toChar
inline def toUpperCaseString: String = upperCaseChar.toString
// the bitboard of the file
+3 -3
View File
@@ -45,7 +45,7 @@ case class Game(
def applyWithCompensated(move: Move): Clock.WithCompensatedLag[Game] =
val newPosition = move.after
val newClock = applyClock(move.metrics, newPosition.status.isEmpty)
val newClock = applyClock(move.metrics, newPosition.status.isEmpty)
Clock.WithCompensatedLag(
copy(
@@ -75,8 +75,8 @@ case class Game(
val c2 = c1.step(metrics, gameActive)
if ply - startedAtPly == Ply(1) then c2.map(_.start) else c2
def apply(uci: Uci.Move): Either[ErrorStr, (Game, Move)] = apply(uci.orig, uci.dest, uci.promotion)
def apply(uci: Uci.Drop): Either[ErrorStr, (Game, Drop)] = drop(uci.role, uci.square)
def apply(uci: Uci.Move): Either[ErrorStr, (Game, Move)] = apply(uci.orig, uci.dest, uci.promotion)
def apply(uci: Uci.Drop): Either[ErrorStr, (Game, Drop)] = drop(uci.role, uci.square)
def apply(uci: Uci): Either[ErrorStr, (Game, MoveOrDrop)] =
uci match
case uci: Uci.Move => apply(uci)
+5 -5
View File
@@ -6,7 +6,7 @@ trait HasId[A, Id]:
extension (a: A)
def id: Id
inline def sameId(other: A): Boolean = a.id == other.id
inline def hasId(id: Id): Boolean = a.id == id
inline def hasId(id: Id): Boolean = a.id == id
extension (xs: List[A])
final def remove(v: A): List[A] =
@@ -21,8 +21,8 @@ trait HasId[A, Id]:
def loop(acc: List[A], xs: List[A]): List[A] =
xs match
case (v :: vs) if v.hasId(id) => acc ++ vs
case (v :: vs) => loop(acc :+ v, vs)
case Nil => acc
case (v :: vs) => loop(acc :+ v, vs)
case Nil => acc
loop(Nil, xs)
trait Mergeable[A]:
@@ -48,11 +48,11 @@ trait Mergeable[A]:
@tailrec
def loop(acc: List[A], rest: List[A]): List[A] =
rest match
case Nil => acc :+ v
case Nil => acc :+ v
case y :: ys =>
y.merge(v) match
case Some(m) => acc ++ (m +: ys)
case _ => loop(acc :+ y, ys)
case _ => loop(acc :+ y, ys)
loop(Nil, xs)
+1 -1
View File
@@ -6,5 +6,5 @@ trait HasPosition[A]:
extension (a: A)
def position: Position
inline def variant: Variant = position.variant
inline def color: Color = position.color
inline def color: Color = position.color
inline def history: History = position.history
+11 -11
View File
@@ -3,23 +3,23 @@ package chess
opaque type PositionHash = Array[Byte]
object PositionHash:
def apply(value: Array[Byte]): PositionHash = value
def apply(h: Hash): PositionHash = Array((h >>> 16).toByte, (h >>> 8).toByte, h.toByte)
val empty: PositionHash = Array.empty
def apply(h: Hash): PositionHash = Array((h >>> 16).toByte, (h >>> 8).toByte, h.toByte)
val empty: PositionHash = Array.empty
extension (p: PositionHash)
def value: Array[Byte] = p
inline def isEmpty: Boolean = p.length == 0
def value: Array[Byte] = p
inline def isEmpty: Boolean = p.length == 0
inline def combine(other: PositionHash): PositionHash = p ++ other
def isRepetition(times: Int) =
def isRepetition(times: Int) =
if times <= 1 then true
else if p.length <= (times - 1) * 4 * Hash.size then false
else
// compare only hashes for positions with the same side to move
var i = Hash.size * 2
var i = Hash.size * 2
var count = 0
val x = p(0)
val y = p(1)
val z = p(2)
val x = p(0)
val y = p(1)
val z = p(2)
while i <= p.length - Hash.size && count < times - 1
do
if x == p(i) && y == p(i + 1) && z == p(i + 2) then count += 1
@@ -28,8 +28,8 @@ object PositionHash:
opaque type Hash = Int
object Hash:
val size = 3
def apply(value: Int): Hash = value >>> 8
val size = 3
def apply(value: Int): Hash = value >>> 8
def apply(position: Position): Hash = hashPosition(position) >>> 8
private def hashPosition(position: Position): Int =
+2 -2
View File
@@ -17,9 +17,9 @@ case class History(
def setHalfMoveClock(v: HalfMoveClock): History = copy(halfMoveClock = v)
inline def threefoldRepetition: Boolean = positionHashes.isRepetition(3)
inline def fivefoldRepetition: Boolean = positionHashes.isRepetition(5)
inline def fivefoldRepetition: Boolean = positionHashes.isRepetition(5)
inline def canCastle(inline color: Color): Boolean = castles.can(color)
inline def canCastle(inline color: Color): Boolean = castles.can(color)
inline def canCastle(inline color: Color, inline side: Side): Boolean = castles.can(color, side)
inline def withoutCastles(inline color: Color): History = copy(castles = castles.without(color))
@@ -52,7 +52,7 @@ object InsufficientMatingMaterial:
def apply(board: Board, color: Color): Boolean =
import board.*
inline def onlyKing = kingsOnlyOf(color)
inline def KN =
inline def KN =
onlyOf(color, King, Knight) && count(color, Knight) == 1 && onlyOf(!color, King, Queen)
inline def KB =
onlyOf(color, King, Bishop) &&
+2 -2
View File
@@ -14,9 +14,9 @@ final case class LagTracker(
):
def onMove(lag: Centis) =
val comp = lag.atMost(quota)
val comp = lag.atMost(quota)
val uncomped = lag - comp
val ceDiff = compEstimate.getOrElse(Centis(1)) - comp
val ceDiff = compEstimate.getOrElse(Centis(1)) - comp
(
comp,
+6 -6
View File
@@ -66,7 +66,7 @@ case class Move(
inline def withMetrics(m: MoveMetrics): Move = copy(metrics = m)
override lazy val toSanStr: SanStr = format.pgn.Dumper(this)
override lazy val toUci: Uci.Move = Uci.Move(orig, dest, promotion)
override lazy val toUci: Uci.Move = Uci.Move(orig, dest, promotion)
override def toString = s"$piece ${toUci.uci}"
@@ -92,13 +92,13 @@ case class Move(
lazy val positionHashesOfBoardBefore =
if h.positionHashes.isEmpty then PositionHash(Hash(before)) else h.positionHashes
val resetsPositionHashes = after.variant.isIrreversible(this)
val basePositionHashes =
val basePositionHashes =
if resetsPositionHashes then PositionHash.empty else positionHashesOfBoardBefore
h.copy(positionHashes = PositionHash(Hash(after)).combine(basePositionHashes))
}
private def castleRights: (Castles, UnmovedRooks) =
var castleRights: Castles = afterWithoutHistory.history.castles
var castleRights: Castles = afterWithoutHistory.history.castles
var unmovedRooks: UnmovedRooks = afterWithoutHistory.history.unmovedRooks
// if the rook is captured
@@ -144,7 +144,7 @@ end Move
object Move:
case class Castle(king: Square, kingTo: Square, rook: Square, rookTo: Square):
def side: Side = if kingTo.file == File.C then QueenSide else KingSide
def side: Side = if kingTo.file == File.C then QueenSide else KingSide
def isStandard: Boolean = king.file == File.E && (rook.file == File.A || rook.file == File.H)
case class Drop(
@@ -159,9 +159,9 @@ case class Drop(
inline def withMetrics(m: MoveMetrics): Drop = copy(metrics = m)
override inline def color: Color = piece.color
override inline def color: Color = piece.color
override lazy val toSanStr: SanStr = format.pgn.Dumper(this)
override lazy val toUci: Uci.Drop = Uci.Drop(piece.role, square)
override lazy val toUci: Uci.Drop = Uci.Drop(piece.role, square)
override def toString = toUci.uci
+60 -60
View File
@@ -9,15 +9,15 @@ import scala.annotation.{ tailrec, targetName }
sealed abstract class Tree[A](val value: A, val child: Option[Node[A]]) derives Traverse:
final def withValue(value: A): TreeSelector[A, this.type] = this match
case n: Node[A] => n.copy(value = value)
case n: Node[A] => n.copy(value = value)
case v: Variation[A] => v.copy(value = value)
final def updateValue(f: A => A): TreeSelector[A, this.type] = this match
case n: Node[A] => n.copy(value = f(value))
case n: Node[A] => n.copy(value = f(value))
case v: Variation[A] => v.copy(value = f(value))
final def withChild(child: Option[Node[A]]): TreeSelector[A, this.type] = this match
case n: Node[A] => n.copy(child = child)
case n: Node[A] => n.copy(child = child)
case v: Variation[A] => v.copy(child = child)
final def updateChild(f: Option[Node[A]] => Option[Node[A]]): TreeSelector[A, this.type] =
@@ -26,7 +26,7 @@ sealed abstract class Tree[A](val value: A, val child: Option[Node[A]]) derives
final def withoutChild: TreeSelector[A, this.type] = withChild(none)
final def isNode: Boolean = this match
case _: Node[A] => true
case _: Node[A] => true
case _: Variation[A] => false
final def isVariation: Boolean = !this.isNode
@@ -45,13 +45,13 @@ sealed abstract class Tree[A](val value: A, val child: Option[Node[A]]) derives
def variations: List[Tree[A]] =
this match
case n: Node[A] => n.variations
case n: Node[A] => n.variations
case _: Variation[A] => Nil
final def mainlineValues: List[A] =
@tailrec
def loop(tree: Tree[A], acc: List[A]): List[A] = tree.child match
case None => tree.value :: acc
case None => tree.value :: acc
case Some(child) => loop(child, tree.value :: acc)
loop(this, Nil).reverse
@@ -60,23 +60,23 @@ sealed abstract class Tree[A](val value: A, val child: Option[Node[A]]) derives
def loop(tree: Tree[A], acc: List[Id]): List[Id] =
tree.child match
case Some(child) => loop(child, tree.value.id +: acc)
case None => tree.value.id +: acc
case None => tree.value.id +: acc
loop(this, Nil).reverse
final def findPathReserve[Id](path: List[Id])(using HasId[A, Id]): Option[List[Tree[A]]] =
@tailrec
def loop(tree: Tree[A], path: List[Id], acc: List[Tree[A]]): Option[List[Tree[A]]] = path match
case Nil => None
case head :: Nil if tree.hasId(head) => (tree :: acc).some
case Nil => None
case head :: Nil if tree.hasId(head) => (tree :: acc).some
case head :: rest if tree.hasId(head) =>
tree.child match
case None => None
case None => None
case Some(child) => loop(child, rest, tree :: acc)
case head :: _ =>
tree match
case node: Node[A] =>
node.findVariation(head) match
case None => None
case None => None
case Some(variation) => loop(variation, path, acc)
case _ => None
@@ -136,10 +136,10 @@ sealed abstract class Tree[A](val value: A, val child: Option[Node[A]]) derives
modifyAt(path, _.addChild(node).some)
type TreeSelector[A, X <: Tree[A]] <: Tree[A] = X match
case Node[A] => Node[A]
case Node[A] => Node[A]
case Variation[A] => Variation[A]
type TreeMapper[A] = (tree: Tree[A]) => TreeSelector[A, tree.type]
type TreeMapper[A] = (tree: Tree[A]) => TreeSelector[A, tree.type]
type TreeMapOption[A] = (tree: Tree[A]) => Option[TreeSelector[A, tree.type]]
final case class Node[A](
@@ -162,7 +162,7 @@ final case class Node[A](
def loop(tree: Node[A], acc: List[Node[A]]): List[Node[A]] =
tree.child match
case Some(child) => loop(child, tree :: acc)
case None => tree :: acc
case None => tree :: acc
loop(this, Nil)
/**
@@ -176,7 +176,7 @@ final case class Node[A](
if n <= 0 then acc
else
node.child match
case None => node :: acc
case None => node :: acc
case Some(child) => loop(n - 1, child, node.withoutChild :: acc)
Option.when(n > 0)(Tree.buildReverse(loop(n, this, Nil)).getOrElse(this))
@@ -190,7 +190,7 @@ final case class Node[A](
if !f(node.value) then acc
else
node.child match
case None => node :: acc
case None => node :: acc
case Some(child) => loop(child, node.withoutChild :: acc)
Tree.buildReverse(loop(this, Nil))
@@ -204,68 +204,68 @@ final case class Node[A](
def modifyChildAt[Id](path: List[Id], f: Node[A] => Option[Node[A]]): HasId[A, Id] ?=> Option[Node[A]] =
path match
case Nil => f(this)
case head :: Nil if this.hasId(head) => child.map(c => withChild(f(c)))
case Nil => f(this)
case head :: Nil if this.hasId(head) => child.map(c => withChild(f(c)))
case head :: rest if this.hasId(head) =>
child.flatMap(_.modifyChildAt(rest, f)) match
case None => None
case None => None
case Some(c) => copy(child = c.some).some
case _ =>
variations.mapAccumulate(false):
case (true, n) => (true, n)
case (true, n) => (true, n)
case (false, n) =>
n.modifyChildAt(path, f) match
case Some(nn) => (true, nn)
case None => (false, n)
case None => (false, n)
match
case (true, ns) => copy(variations = ns).some
case (false, _) => none
def modifyAt[Id](path: List[Id], f: TreeMapOption[A]): HasId[A, Id] ?=> Option[Node[A]] =
path match
case Nil => None
case head :: Nil if this.hasId(head) => f(this)
case Nil => None
case head :: Nil if this.hasId(head) => f(this)
case head :: rest if this.hasId(head) =>
child.flatMap(_.modifyAt(rest, f)) match
case None => None
case None => None
case Some(c) => copy(child = c.some).some
case head :: _ =>
variations.mapAccumulate(false.some):
case (Some(true), n) => (true.some, n)
case (Some(true), n) => (true.some, n)
case (Some(_), n) if n.hasId(head) =>
n.modifyAt(path, f) match
case None => (None, n)
case None => (None, n)
case Some(nn) => (true.some, nn)
case (x, n) => (x, n)
match
case (Some(true), ns) => copy(variations = ns).some
case _ => none
case _ => none
// delete the node at the end of the path
// return None if path is not found
// return Some(None) if the node is deleted
def deleteAt[Id](path: List[Id])(using HasId[A, Id]): Option[Option[Node[A]]] =
path match
case Nil => None
case head :: Nil if this.hasId(head) => Some(None)
case Nil => None
case head :: Nil if this.hasId(head) => Some(None)
case head :: rest if this.hasId(head) =>
child.flatMap(_.deleteAt(rest)) match
case None => None
case None => None
case Some(c) => copy(child = c).some.some
case head :: _ =>
variations.foldLeft((false.some, List.empty[Variation[A]])) {
case ((Some(true), acc), n) => (true.some, n :: acc)
case ((Some(true), acc), n) => (true.some, n :: acc)
case ((Some(false), acc), n) if n.hasId(head) =>
n.deleteAt(path) match
case None => (None, n :: acc)
case None => (None, n :: acc)
case Some(nn) => (true.some, nn ++: acc)
case ((x, acc), n) => (x, n :: acc)
} match
case (Some(true), ns) => copy(variations = ns.reverse).some.some
case _ => none
case _ => none
def promoteToMainline[Id](path: List[Id])(using HasId[A, Id]): Option[Node[A]] = path match
case Nil => None
case Nil => None
case head :: Nil =>
promote(head)
case head :: rest =>
@@ -276,14 +276,14 @@ final case class Node[A](
// find the lastest variation in the path
// promote it into mainline
def promote[Id](path: List[Id])(using HasId[A, Id]): Option[Node[A]] = path match
case Nil => None
case Nil => None
case head :: _ =>
findPathReserve(path).flatMap: ps =>
if ps.forall(_.isVariation) then this.promote(head)
else if ps.forall(_.isNode) then None
else
ps.dropWhile(_.isNode).dropWhile(_.isVariation) match
case Nil => this.promote(head)
case Nil => this.promote(head)
case rest =>
val swappingNodePath = rest.map(_.id).reverse
path
@@ -297,7 +297,7 @@ final case class Node[A](
if this.hasId(id) then this.some
else
variations.find(_.hasId(id)) match
case None => None
case None => None
case Some(v) =>
val vs = this.toVariations.removeById(id)
v.toNode.withVariations(vs).some
@@ -310,9 +310,9 @@ final case class Node[A](
// because variations only use accumulated value from the parent
def mapAccuml[S, B](init: S)(f: (S, A) => (S, B)): (S, Node[B]) =
val (s1, b) = f(init, value)
val v = variations.map(_.mapAccuml(init)(f)._2)
val v = variations.map(_.mapAccuml(init)(f)._2)
child.map(_.mapAccuml(s1)(f)) match
case None => (s1, Node(b, None, v))
case None => (s1, Node(b, None, v))
case Some(s) => (s._1, Node(b, s._2.some, v))
def mapAccuml_[S, B](init: S)(f: (S, A) => (S, B)): Node[B] =
@@ -325,11 +325,11 @@ final case class Node[A](
// should we promote a variation to mainline if the f(value) is None?
def mapAccumlOption[S, B](init: S)(f: (S, A) => (S, Option[B])): (S, Option[Node[B]]) =
f(init, value) match
case (s1, None) => (s1, None)
case (s1, None) => (s1, None)
case (s1, Some(b)) =>
val vs = variations.map(_.mapAccumlOption(init)(f)._2).flatten
child.map(_.mapAccumlOption(s1)(f)) match
case None => (s1, Node(b, None, vs).some)
case None => (s1, Node(b, None, vs).some)
case Some(s) => (s._1, Node(b, s._2, vs).some)
def mapAccumlOption_[S, B](init: S)(f: (S, A) => (S, Option[B])): Option[Node[B]] =
@@ -341,7 +341,7 @@ final case class Node[A](
if predicate(value) then this.some
else
child.match
case None => None
case None => None
case Some(c) => c.findInMainline(predicate)
def modifyInMainline(predicate: A => Boolean, f: Node[A] => Node[A]): Option[Node[A]] =
@@ -368,13 +368,13 @@ final case class Node[A](
else if n == 0 then this.some
else
child.match
case None => none
case None => none
case Some(c) => c.getMainlineNodeAt(n - 1)
@tailrec
def lastMainlineNode: Node[A] =
child match
case None => this
case None => this
case Some(c) => c.lastMainlineNode
def modifyLastMainlineNode(f: Node[A] => Node[A]): Node[A] =
@@ -391,7 +391,7 @@ final case class Node[A](
n.copy(value = f(n.value, i), child = n.child.map(c => loop(c, i + 1)), variations = Nil)
loop(this, 0)
def toVariation: Variation[A] = Variation(value, child)
def toVariation: Variation[A] = Variation(value, child)
def toVariations: List[Variation[A]] = Variation(value, child) +: variations
// we assume that they have the same path from the roof
@@ -404,7 +404,7 @@ final case class Node[A](
def mergeOrAddAsVariation(v: Variation[A])(using Mergeable[A]): Node[A] =
value.merge(v.value) match
case Some(newValue) => Node(newValue, Tree.merge(child, v.child), variations)
case _ => addVariation(v)
case _ => addVariation(v)
def mergeOrAddAsVariation(vs: List[Variation[A]])(using Mergeable[A]): Node[A] =
vs.foldLeft(this)(_.mergeOrAddAsVariation(_))
@@ -440,21 +440,21 @@ final case class Variation[A](override val value: A, override val child: Option[
f: Node[A] => Option[Node[A]]
): HasId[A, Id] ?=> Option[Variation[A]] =
path match
case Nil => None
case head :: Nil if this.hasId(head) => child.map(c => withChild(f(c)))
case Nil => None
case head :: Nil if this.hasId(head) => child.map(c => withChild(f(c)))
case head :: rest if this.hasId(head) =>
child.flatMap(_.modifyChildAt(rest, f)) match
case None => None
case None => None
case Some(c) => copy(child = c.some).some
case _ => None
def modifyAt[Id](path: List[Id], f: TreeMapOption[A]): HasId[A, Id] ?=> Option[Variation[A]] =
path match
case Nil => None
case head :: Nil if this.hasId(head) => f(this)
case Nil => None
case head :: Nil if this.hasId(head) => f(this)
case head :: rest if this.hasId(head) =>
child.flatMap(_.modifyAt(rest, f)) match
case None => None
case None => None
case Some(c) => copy(child = c.some).some
case _ => None
@@ -463,11 +463,11 @@ final case class Variation[A](override val value: A, override val child: Option[
// return Some(None) if the node is deleted
def deleteAt[Id](path: List[Id])(using HasId[A, Id]): Option[Option[Variation[A]]] =
path match
case Nil => None
case head :: Nil if this.hasId(head) => Some(None)
case Nil => None
case head :: Nil if this.hasId(head) => Some(None)
case head :: rest if this.hasId(head) =>
child.flatMap(_.deleteAt(rest)) match
case None => None
case None => None
case Some(c) => copy(child = c).some.some
case _ => None
@@ -477,7 +477,7 @@ final case class Variation[A](override val value: A, override val child: Option[
def mapAccuml[S, B](init: S)(f: (S, A) => (S, B)): (S, Variation[B]) =
val (s1, b) = f(init, value)
child.map(_.mapAccuml(s1)(f)) match
case None => (s1, Variation(b, None))
case None => (s1, Variation(b, None))
case Some(s) => (s._1, Variation(b, s._2.some))
// Akin to mapAccuml, return an Option[Variation[B]]
@@ -487,16 +487,16 @@ final case class Variation[A](override val value: A, override val child: Option[
// should we promote a variation to mainline if the f(value) is None?
def mapAccumlOption[S, B](init: S)(f: (S, A) => (S, Option[B])): (S, Option[Variation[B]]) =
f(init, value) match
case (s1, None) => (s1, None)
case (s1, None) => (s1, None)
case (s1, Some(b)) =>
child.map(_.mapAccumlOption(s1)(f)) match
case None => (s1, Variation(b, None).some)
case None => (s1, Variation(b, None).some)
case Some(s) => (s._1, Variation(b, s._2).some)
def toNode: Node[A] = Node(value, child)
object Tree:
def lift[A](f: A => A): TreeMapper[A] = tree => tree.withValue(f(tree.value))
def lift[A](f: A => A): TreeMapper[A] = tree => tree.withValue(f(tree.value))
def liftOption[A](f: A => A): TreeMapOption[A] = tree => tree.withValue(f(tree.value)).some
extension [A](tm: TreeMapper[A]) def toOption: TreeMapOption[A] = x => tm(x).some
@@ -530,7 +530,7 @@ object Tree:
@targetName("buildWithNodeReverse")
def buildReverse[A, B](s: Seq[A], f: A => Node[B]): Option[Node[B]] =
s match
case Nil => none
case Nil => none
case x :: xs => xs.foldLeft(f(x))((acc, a) => f(a).withChild(acc.some)).some
@targetName("buildWithNode")
+30 -30
View File
@@ -4,7 +4,7 @@ case class Outcome(winner: Option[Color]):
override def toString = winner match
case Some(White) => "1-0"
case Some(Black) => "0-1"
case None => "1/2-1/2"
case None => "1/2-1/2"
object Outcome:
@@ -16,7 +16,7 @@ object Outcome:
val white = Outcome(Some(White))
val black = Outcome(Some(Black))
val draw = Outcome(None)
val draw = Outcome(None)
lazy val knownResultStrings = "*" :: normalizationMap.keys.toList
@@ -26,21 +26,21 @@ object Outcome:
def value: Float = this match
case Zero => 0f
case Half => 0.5f
case One => 1f
case One => 1f
def show: String = this match
case Zero => "0"
case Half => "1/2"
case One => "1"
case One => "1"
import Points.*
type GamePoints = ByColor[Points]
def fromPoints(points: ByColor[Points]): Option[Outcome] = points match
case ByColor(One, Zero) => Some(white)
case ByColor(Zero, One) => Some(black)
case ByColor(One, Zero) => Some(white)
case ByColor(Zero, One) => Some(black)
case ByColor(Half, Half) => Some(draw)
case _ => None
case _ => None
def pointsFromResult(result: String): Option[GamePoints] =
normalizationMap.get(result)
@@ -48,7 +48,7 @@ object Outcome:
def outcomeToPoints(outcome: Outcome): GamePoints = outcome match
case Outcome(Some(White)) => ByColor(One, Zero)
case Outcome(Some(Black)) => ByColor(Zero, One)
case Outcome(None) => ByColor(Half, Half)
case Outcome(None) => ByColor(Half, Half)
def showPoints(points: Option[GamePoints]): String =
points.fold("*"):
@@ -57,52 +57,52 @@ object Outcome:
def guessPointsFromStatusAndPosition(status: Status, positionWinner: Option[Color]): Option[GamePoints] =
import Status.*
status match
case Created | Started => None
case Aborted | Cheat | NoStart => Some(ByColor(Zero, Zero))
case Stalemate | Draw => Some(ByColor(Half, Half))
case Created | Started => None
case Aborted | Cheat | NoStart => Some(ByColor(Zero, Zero))
case Stalemate | Draw => Some(ByColor(Half, Half))
case Mate | Resign | Timeout | Outoftime | UnknownFinish | VariantEnd =>
positionWinner match
case Some(White) => Some(ByColor(One, Zero))
case Some(Black) => Some(ByColor(Zero, One))
case None => Some(ByColor(Half, Half))
case None => Some(ByColor(Half, Half))
private val normalizationMap: Map[String, GamePoints] =
val hyphen = "-"
val enDash = ""
val emDash = "—"
val dashes = List(hyphen, enDash, emDash)
val hyphen = "-"
val enDash = ""
val emDash = "—"
val dashes = List(hyphen, enDash, emDash)
val separators = dashes ::: List("_", ":")
val draws = List("½", "1/2", "0.5")
val wins = List("1", "+")
val losses = "0" :: dashes
val draws = List("½", "1/2", "0.5")
val wins = List("1", "+")
val losses = "0" :: dashes
val allDraws = for
separator <- separators
draw <- draws
draw <- draws
yield s"$draw$separator$draw"
val allWins = for
separator <- separators
win <- wins
loss <- losses
win <- wins
loss <- losses
yield s"$win$separator$loss"
val allLosses = for
separator <- separators
win <- wins
loss <- losses
win <- wins
loss <- losses
yield s"$loss$separator$win"
val allCancels = for
separator <- separators
loss <- losses
loss <- losses
yield s"$loss$separator$loss"
val allHalfWins = for
separator <- separators
loss <- losses
draw <- draws
loss <- losses
draw <- draws
yield s"$draw$separator$loss"
val allHalfLosses = for
separator <- separators
loss <- losses
draw <- draws
loss <- losses
draw <- draws
yield s"$loss$separator$draw"
val pairs = allDraws.map(_ -> ByColor[Points](Half, Half)) :::
@@ -115,7 +115,7 @@ object Outcome:
val lccResults = Map(
"WHITEWIN" -> ByColor[Points](One, Zero),
"BLACKWIN" -> ByColor[Points](Zero, One),
"DRAW" -> ByColor[Points](Half, Half) // ? not sure
"DRAW" -> ByColor[Points](Half, Half) // ? not sure
)
pairs.toMap ++ lccResults
+7 -7
View File
@@ -5,22 +5,22 @@ import cats.derived.*
case class Piece(color: Color, role: Role) derives Eq:
def is(c: Color): Boolean = c == color
def is(r: Role): Boolean = r == role
def is(c: Color): Boolean = c == color
def is(r: Role): Boolean = r == role
def isNot(r: Role): Boolean = r != role
def unary_! : Piece = Piece(!color, role)
def unary_! : Piece = Piece(!color, role)
def forsyth: Char = if color.white then role.forsythUpper else role.forsyth
// the piece at from can attack the target to when mask are all the occupied squares
def eyes(from: Square, to: Square, mask: Bitboard): Boolean =
role match
case King => from.kingAttacks.contains(to)
case Queen => from.queenAttacks(mask).contains(to)
case Rook => from.rookAttacks(mask).contains(to)
case King => from.kingAttacks.contains(to)
case Queen => from.queenAttacks(mask).contains(to)
case Rook => from.rookAttacks(mask).contains(to)
case Bishop => from.bishopAttacks(mask).contains(to)
case Knight => from.knightAttacks.contains(to)
case Pawn => from.pawnAttacks(color).contains(to)
case Pawn => from.pawnAttacks(color).contains(to)
override def toString = s"$color-$role".toLowerCase
+16 -16
View File
@@ -11,49 +11,49 @@ object PlayerTitle:
private inline def apply(inline s: String): PlayerTitle = s.asInstanceOf[PlayerTitle]
extension (t: PlayerTitle)
inline def value: String = t
def isLichess: Boolean = t == "LM" || t == "BOT"
inline def value: String = t
def isLichess: Boolean = t == "LM" || t == "BOT"
def isFederation: Boolean = !isLichess
given Render[PlayerTitle] = _.value
val GM: PlayerTitle = "GM"
val GM: PlayerTitle = "GM"
val WGM: PlayerTitle = "WGM"
val IM: PlayerTitle = "IM"
val IM: PlayerTitle = "IM"
val WIM: PlayerTitle = "WIM"
val FM: PlayerTitle = "FM"
val FM: PlayerTitle = "FM"
val WFM: PlayerTitle = "WFM"
val NM: PlayerTitle = "NM"
val CM: PlayerTitle = "CM"
val NM: PlayerTitle = "NM"
val CM: PlayerTitle = "CM"
val WCM: PlayerTitle = "WCM"
val WNM: PlayerTitle = "WNM"
val LM: PlayerTitle = "LM"
val LM: PlayerTitle = "LM"
val BOT: PlayerTitle = "BOT"
// names are as stated on FIDE profile pages
val all = List[(PlayerTitle, String)](
GM -> "Grandmaster",
GM -> "Grandmaster",
WGM -> "Woman Grandmaster",
IM -> "International Master",
IM -> "International Master",
WIM -> "Woman Intl. Master",
FM -> "FIDE Master",
FM -> "FIDE Master",
WFM -> "Woman FIDE Master",
NM -> "National Master",
CM -> "Candidate Master",
NM -> "National Master",
CM -> "Candidate Master",
WCM -> "Woman Candidate Master",
WNM -> "Woman National Master",
LM -> "Lichess Master",
LM -> "Lichess Master",
BOT -> "Chess Robot"
)
val names: Map[PlayerTitle, String] = all.toMap
val names: Map[PlayerTitle, String] = all.toMap
lazy val fromNames: Map[String, PlayerTitle] = all.map(_.swap).toMap
val acronyms: List[PlayerTitle] = all.map(_._1)
def titleName(title: PlayerTitle): String = names.getOrElse(title, title.value)
def get(str: String): Option[PlayerTitle] = Option(PlayerTitle(str.toUpperCase)).filter(names.contains)
def get(str: String): Option[PlayerTitle] = Option(PlayerTitle(str.toUpperCase)).filter(names.contains)
def get(strs: List[String]): List[PlayerTitle] = strs.flatMap { get(_) }
// ordered by difficulty to achieve
+17 -17
View File
@@ -33,7 +33,7 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
if v == Crazyhouse then copy(variant = v).ensureCrazyData
else copy(variant = v)
def withCrazyData(data: Crazyhouse.Data): Position = updateHistory(_.copy(crazyData = data.some))
def withCrazyData(data: Crazyhouse.Data): Position = updateHistory(_.copy(crazyData = data.some))
def withCrazyData(data: Option[Crazyhouse.Data]): Position = updateHistory(_.copy(crazyData = data))
def withCrazyData(f: Crazyhouse.Data => Crazyhouse.Data): Position =
withCrazyData(f(crazyData.getOrElse(Crazyhouse.Data.init)))
@@ -60,7 +60,7 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
def drops: Option[List[Square]] =
variant match
case v: Crazyhouse.type => v.possibleDrops(this)
case _ => None
case _ => None
def checkSquare: Option[Square] = if check.yes then ourKing else None
@@ -94,7 +94,7 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
inline def variantEnd: Boolean = variant.specialEnd(this)
@threadUnsafe
lazy val check: Check = checkOf(color)
lazy val check: Check = checkOf(color)
inline def checkOf(c: Color): Check = variant.kingThreatened(board, c)
@threadUnsafe
@@ -187,7 +187,7 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
val s1: List[Move] = for
from <- capturers
to <- from.pawnAttacks(color) & them & mask
to <- from.pawnAttacks(color) & them & mask
move <- genPawnMoves(from, to, true)
yield move
@@ -204,13 +204,13 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
else Bitboard.rank(color.fourthRank))
val s2: List[Move] = for
to <- singleMoves & mask
to <- singleMoves & mask
from <- Square(to.value + (if isWhiteTurn then -8 else 8)).toList
move <- genPawnMoves(from, to, false)
yield move
val s3: List[Move] = for
to <- doubleMoves & mask
to <- doubleMoves & mask
from <- Square(to.value + (if isWhiteTurn then -16 else 16))
move <- normalMove(from, to, Pawn, false)
yield move
@@ -220,28 +220,28 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
def genKnight(knights: Bitboard, mask: Bitboard): List[Move] =
for
from <- knights
to <- from.knightAttacks & mask
to <- from.knightAttacks & mask
move <- normalMove(from, to, Knight, isOccupied(to))
yield move
def genBishop(bishops: Bitboard, mask: Bitboard): List[Move] =
for
from <- bishops
to <- from.bishopAttacks(board.occupied) & mask
to <- from.bishopAttacks(board.occupied) & mask
move <- normalMove(from, to, Bishop, isOccupied(to))
yield move
def genRook(rooks: Bitboard, mask: Bitboard): List[Move] =
for
from <- rooks
to <- from.rookAttacks(board.occupied) & mask
to <- from.rookAttacks(board.occupied) & mask
move <- normalMove(from, to, Rook, isOccupied(to))
yield move
def genQueen(queens: Bitboard, mask: Bitboard): List[Move] =
for
from <- queens
to <- from.queenAttacks(board.occupied) & mask
to <- from.queenAttacks(board.occupied) & mask
move <- normalMove(from, to, Queen, isOccupied(to))
yield move
@@ -268,8 +268,8 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
|| (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)
rookTo = Square(toRookFile, rook.rank)
kingTo = Square(toKingFile, king.rank)
rookTo = Square(toRookFile, rook.rank)
// calulate different path for standard vs chess960
path =
if variant.chess960 || variant.fromPosition
@@ -364,9 +364,9 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
private def castle(king: Square, kingTo: Square, rook: Square, rookTo: Square): List[Move] =
val boardAfter = for
b1 <- board.take(king)
b2 <- b1.take(rook)
b3 <- b2.put(color.king, kingTo)
b1 <- board.take(king)
b2 <- b1.take(rook)
b3 <- b2.put(color.king, kingTo)
after <- b3.put(color.rook, rookTo)
yield after
@@ -378,7 +378,7 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
val destInput = if !isChess960 then List(rook, kingTo) else List(rook)
for
after <- boardAfter.map(withBoard).toList
after <- boardAfter.map(withBoard).toList
inputKingSquare <- destInput
yield Move(
piece = color.king,
@@ -395,7 +395,7 @@ case class Position(board: Board, history: History, variant: Variant, color: Col
object Position:
case class AndFullMoveNumber(position: Position, fullMoveNumber: FullMoveNumber):
def ply: Ply = fullMoveNumber.ply(position.color)
def ply: Ply = fullMoveNumber.ply(position.color)
def toGame: Game = Game(position = position, ply = ply, startedAtPly = ply)
object AndFullMoveNumber:
+10 -10
View File
@@ -5,8 +5,8 @@ object Rank:
extension (a: Rank)
inline def value: Int = a
inline infix def >(inline o: Rank): Boolean = value > o.value
inline infix def <(inline o: Rank): Boolean = value < o.value
inline infix def >(inline o: Rank): Boolean = value > o.value
inline infix def <(inline o: Rank): Boolean = value < o.value
inline infix def >=(inline o: Rank): Boolean = value >= o.value
inline infix def <=(inline o: Rank): Boolean = value <= o.value
@@ -22,14 +22,14 @@ object Rank:
inline def fromChar(inline ch: Char): Option[Rank] = Rank(ch.toInt - 49)
val First: Rank = 0
val Second: Rank = 1
val Third: Rank = 2
val Fourth: Rank = 3
val Fifth: Rank = 4
val Sixth: Rank = 5
val First: Rank = 0
val Second: Rank = 1
val Third: Rank = 2
val Fourth: Rank = 3
val Fifth: Rank = 4
val Sixth: Rank = 5
val Seventh: Rank = 6
val Eighth: Rank = 7
val Eighth: Rank = 7
val all = List(First, Second, Third, Fourth, Fifth, Sixth, Seventh, Eighth)
val all = List(First, Second, Third, Fourth, Fifth, Sixth, Seventh, Eighth)
val allReversed = all.reverse
+1 -1
View File
@@ -6,7 +6,7 @@ object Rated extends YesNo[Rated]:
extension (r: Rated)
def name: String = if r then "rated" else "casual"
def id: Int = if r then 1 else 0
def id: Int = if r then 1 else 0
val all: List[Rated] = List(No, Yes)
+5 -5
View File
@@ -34,19 +34,19 @@ object Replay:
else
// we don't want to compare the full move number, to match transpositions
def truncateFen(fen: Fen.Full) = fen.value.split(' ').take(4).mkString(" ")
val atFenTruncated = truncateFen(atFen)
def compareFen(fen: Fen.Full) = truncateFen(fen) == atFenTruncated
val atFenTruncated = truncateFen(atFen)
def compareFen(fen: Fen.Full) = truncateFen(fen) == atFenTruncated
@scala.annotation.tailrec
def recursivePlyAtFen(position: Position, sans: List[San], ply: Ply): Either[ErrorStr, Ply] =
sans match
case Nil => ErrorStr(s"Can't find $atFenTruncated, reached ply $ply").asLeft
case Nil => ErrorStr(s"Can't find $atFenTruncated, reached ply $ply").asLeft
case san :: rest =>
san(position) match
case Left(err) => err.asLeft
case Left(err) => err.asLeft
case Right(moveOrDrop) =>
val after = moveOrDrop.after
val fen = Fen.write(after.withColor(ply.turn), ply.fullMoveNumber)
val fen = Fen.write(after.withColor(ply.turn), ply.fullMoveNumber)
if compareFen(fen) then ply.asRight
else recursivePlyAtFen(after.withColor(!position.color), rest, ply.next)
+14 -14
View File
@@ -2,9 +2,9 @@ package chess
sealed trait Role:
val forsyth: Char
lazy val forsythUpper: Char = forsyth.toUpper
lazy val pgn: Char = forsythUpper
lazy val name = toString.toLowerCase
lazy val forsythUpper: Char = forsyth.toUpper
lazy val pgn: Char = forsythUpper
lazy val name = toString.toLowerCase
inline def forsythBy(color: Color): Char =
if color.white then forsythUpper else forsyth
@@ -31,14 +31,14 @@ case object Pawn extends Role:
object Role:
val all: List[Role] = List(King, Queen, Rook, Bishop, Knight, Pawn)
val allPromotable: List[PromotableRole] = List(Queen, Rook, Bishop, Knight, King)
val allByForsyth: Map[Char, Role] = all.mapBy(_.forsyth)
val allByPgn: Map[Char, Role] = all.mapBy(_.pgn)
val allByName: Map[String, Role] = all.mapBy(_.name)
val allPromotableByName: Map[String, PromotableRole] = allPromotable.mapBy(_.toString)
val all: List[Role] = List(King, Queen, Rook, Bishop, Knight, Pawn)
val allPromotable: List[PromotableRole] = List(Queen, Rook, Bishop, Knight, King)
val allByForsyth: Map[Char, Role] = all.mapBy(_.forsyth)
val allByPgn: Map[Char, Role] = all.mapBy(_.pgn)
val allByName: Map[String, Role] = all.mapBy(_.name)
val allPromotableByName: Map[String, PromotableRole] = allPromotable.mapBy(_.toString)
val allPromotableByForsyth: Map[Char, PromotableRole] = allPromotable.mapBy(_.forsyth)
val allPromotableByPgn: Map[Char, PromotableRole] = allPromotable.mapBy(_.pgn)
val allPromotableByPgn: Map[Char, PromotableRole] = allPromotable.mapBy(_.pgn)
def forsyth(c: Char): Option[Role] = allByForsyth.get(c)
@@ -53,9 +53,9 @@ object Role:
def valueOf(r: Role): Option[Int] =
r match
case Pawn => Option(1)
case Pawn => Option(1)
case Knight => Option(3)
case Bishop => Option(3)
case Rook => Option(5)
case Queen => Option(9)
case King => None
case Rook => Option(5)
case Queen => Option(9)
case King => None
+8 -8
View File
@@ -23,9 +23,9 @@ object Square:
@targetName("aboveOf")
inline def ?^(inline other: Square): Boolean = rank > other.rank
inline def onSameFile(inline other: Square): Boolean = file == other.file
inline def onSameRank(inline other: Square): Boolean = rank == other.rank
inline def onSameLine(inline other: Square): Boolean = onSameFile(other) || onSameRank(other)
inline def onSameFile(inline other: Square): Boolean = file == other.file
inline def onSameRank(inline other: Square): Boolean = rank == other.rank
inline def onSameLine(inline other: Square): Boolean = onSameFile(other) || onSameRank(other)
inline def onSameDiagonal(inline other: Square): Boolean =
file.value - rank.value == other.file.value - other.rank.value || file.value + rank.value == other.file.value + other.rank.value
@@ -38,9 +38,9 @@ object Square:
inline def rank: Rank = Rank.of(s)
def asChar: Char =
if s <= 25 then (97 + s).toChar // a ...
if s <= 25 then (97 + s).toChar // a ...
else if s <= 51 then (39 + s).toChar // A ...
else if s <= 61 then (s - 4).toChar // 0 ...
else if s <= 61 then (s - 4).toChar // 0 ...
else if s == 62 then '!'
else '?'
@@ -53,7 +53,7 @@ object Square:
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
inline def bl: Long = 1L << s.value
def bishopAttacks(occupied: Bitboard): Bitboard =
Bitboard(ATTACKS(Magic.BISHOP(s.value).bishopIndex(occupied.value)))
@@ -75,7 +75,7 @@ object Square:
inline def apply(inline file: File, inline rank: Rank): Square = file.value + 8 * rank.value
inline def apply(index: Int): Option[Square] = Option.when(0 <= index && index < 64)(index)
inline def apply(index: Int): Option[Square] = Option.when(0 <= index && index < 64)(index)
private[chess] def unsafe(index: Int): Square = index
inline def at(x: Int, y: Int): Option[Square] = Option.when(0 <= x && x < 8 && 0 <= y && y < 8)(x + 8 * y)
@@ -154,4 +154,4 @@ object Square:
val all: List[Square] = (0 to 63).toList
val allKeys: Map[String, Square] = all.mapBy(_.key)
val charMap: Map[Char, Square] = all.mapBy(_.asChar)
val charMap: Map[Char, Square] = all.mapBy(_.asChar)
+1 -1
View File
@@ -209,5 +209,5 @@ object StartingPosition:
OpeningDb.findByStandardFen(fen).map(StartingPosition(_, featurable))
object presets:
val halloween = of(StandardFen("r1bqkb1r/pppp1ppp/2n2n2/4N3/4P3/2N5/PPPP1PPP/R1BQKB1R b KQkq -"))
val halloween = of(StandardFen("r1bqkb1r/pppp1ppp/2n2n2/4N3/4P3/2N5/PPPP1PPP/R1BQKB1R b KQkq -"))
val frankenstein = of(StandardFen("rnbqkb1r/pppp1ppp/8/4p3/2B1n3/2N5/PPPP1PPP/R1BQK1NR w KQkq -"))
+5 -5
View File
@@ -5,9 +5,9 @@ final case class Stats(samples: Int, mean: Float, sn: Float):
def record(value: Float) =
val newSamples = samples + 1
val delta = value - mean
val newMean = mean + delta / newSamples
val newSN = sn + delta * (value - newMean)
val delta = value - mean
val newMean = mean + delta / newSamples
val newSN = sn + delta * (value - newMean)
Stats(
samples = newSamples,
@@ -27,6 +27,6 @@ final case class Stats(samples: Int, mean: Float, sn: Float):
def total = samples * mean
object Stats:
val empty: Stats = Stats(0, 0, 0)
def apply(value: Float): Stats = empty.record(value)
val empty: Stats = Stats(0, 0, 0)
def apply(value: Float): Stats = empty.record(value)
def apply[T: Numeric](values: Iterable[T]): Stats = empty.record(values)
+15 -15
View File
@@ -4,27 +4,27 @@ enum Status(val id: Int):
val name = s"${toString.head.toLower}${toString.tail}"
inline def is(inline s: Status): Boolean = this == s
inline def is(inline s: Status): Boolean = this == s
inline def is(inline f: Status.type => Status): Boolean = is(f(Status))
inline infix def >=(inline s: Status): Boolean = id >= s.id
inline infix def >(inline s: Status): Boolean = id > s.id
inline infix def >(inline s: Status): Boolean = id > s.id
inline infix def <=(inline s: Status): Boolean = id <= s.id
inline infix def <(inline s: Status): Boolean = id < s.id
inline infix def <(inline s: Status): Boolean = id < s.id
case Created extends Status(10)
case Started extends Status(20)
case Aborted extends Status(25) // from this point the game is finished
case Mate extends Status(30)
case Resign extends Status(31)
case Stalemate extends Status(32)
case Timeout extends Status(33) // when player leaves the game
case Draw extends Status(34)
case Outoftime extends Status(35) // clock flag
case Cheat extends Status(36)
case NoStart extends Status(37) // the player did not make the first move in time
case Created extends Status(10)
case Started extends Status(20)
case Aborted extends Status(25) // from this point the game is finished
case Mate extends Status(30)
case Resign extends Status(31)
case Stalemate extends Status(32)
case Timeout extends Status(33) // when player leaves the game
case Draw extends Status(34)
case Outoftime extends Status(35) // clock flag
case Cheat extends Status(36)
case NoStart extends Status(37) // the player did not make the first move in time
case UnknownFinish extends Status(38) // we don't know why the game ended
case VariantEnd extends Status(60) // the variant has a special ending
case VariantEnd extends Status(60) // the variant has a special ending
object Status:
+3 -3
View File
@@ -6,7 +6,7 @@ import Clock.{ LimitSeconds, LimitMinutes, IncrementSeconds }
case class TournamentClock(limitSeconds: LimitSeconds, incrementSeconds: IncrementSeconds):
def limit: Centis = Centis.ofSeconds(limitSeconds.value)
def limit: Centis = Centis.ofSeconds(limitSeconds.value)
def increment: Centis = Centis.ofSeconds(incrementSeconds.value)
def limitMinutes = LimitMinutes(limitSeconds.value / 60)
@@ -32,6 +32,6 @@ object TournamentClock:
.replaceAllIn(str.toLowerCase.replace(" ", ""), "")
.split('+')
.match
case Array(a) => a.toIntOption.map(make(strict)(_, 0))
case Array(a) => a.toIntOption.map(make(strict)(_, 0))
case Array(a, b) => (a.toIntOption, b.toIntOption).mapN(make(strict))
case _ => none
case _ => none
+7 -7
View File
@@ -7,11 +7,11 @@ object UnmovedRooks:
// for lila testing only
val default: UnmovedRooks = UnmovedRooks(Bitboard.rank(Rank.First) | Bitboard.rank(Rank.Eighth))
val corners: UnmovedRooks = 0x8100000000000081L
val none: UnmovedRooks = 0L
val none: UnmovedRooks = 0L
@targetName("applyUnmovedRooks")
def apply(b: Bitboard): UnmovedRooks = b.value
def apply(l: Long): UnmovedRooks = l
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
@@ -22,9 +22,9 @@ object UnmovedRooks:
UnmovedRooks(wr | br)
extension (ur: UnmovedRooks)
inline def bb: Bitboard = Bitboard(ur)
def isEmpty = ur == 0L
def value: Long = ur
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 =
@@ -51,7 +51,7 @@ object UnmovedRooks:
def contains(square: Square): Boolean =
(ur & (1L << square.value)) != 0L
inline def unary_~ : UnmovedRooks = ~ur
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
+1 -1
View File
@@ -61,7 +61,7 @@ object Attacks:
var subset = 0L
while
val attack = slidingAttacks(square, subset, deltas)
val idx = ((magic.factor * subset) >>> (64 - shift)).toInt + magic.offset
val idx = ((magic.factor * subset) >>> (64 - shift)).toInt + magic.offset
ATTACKS(idx) = attack
// Carry-rippler trick for enumerating subsets.
+1 -1
View File
@@ -5,7 +5,7 @@ import scala.annotation.static
class Magic(val mask: Long, val factor: Long, val offset: Int):
def bishopIndex(occupied: Long): Int = (factor * (occupied & mask) >>> (64 - 9)).toInt + offset
def rookIndex(occupied: Long): Int = (factor * (occupied & mask) >>> (64 - 12)).toInt + offset
def rookIndex(occupied: Long): Int = (factor * (occupied & mask) >>> (64 - 12)).toInt + offset
object Magic:
@static
+11 -11
View File
@@ -8,34 +8,34 @@ enum Score:
case Mate(m: Eval.Mate)
inline def fold[A](w: Eval.Cp => A, b: Eval.Mate => A): A = this match
case Cp(cp) => w(cp)
case Cp(cp) => w(cp)
case Mate(mate) => b(mate)
inline def cp: Option[Eval.Cp] = fold(Some(_), _ => None)
inline def cp: Option[Eval.Cp] = fold(Some(_), _ => None)
inline def mate: Option[Eval.Mate] = fold(_ => None, Some(_))
inline def isCheckmate = mate.exists(_.value == 0)
inline def mateFound = mate.isDefined
inline def mateFound = mate.isDefined
inline def invert: Score = fold(c => Cp(c.invert), m => Mate(m.invert))
inline def invert: Score = fold(c => Cp(c.invert), m => Mate(m.invert))
inline def invertIf(cond: Boolean): Score = if cond then invert else this
object Score:
val initial = Cp(Eval.Cp.initial)
def cp(cp: Int): Score = Cp(Eval.Cp(cp))
val initial = Cp(Eval.Cp.initial)
def cp(cp: Int): Score = Cp(Eval.Cp(cp))
def mate(mate: Int): Score = Mate(Eval.Mate(mate))
object Eval:
opaque type Cp = Int
object Cp extends OpaqueInt[Cp]:
val CEILING = Cp(1000)
val initial = Cp(15)
val CEILING = Cp(1000)
val initial = Cp(15)
inline def ceilingWithSignum(signum: Int) = CEILING.invertIf(signum < 0)
extension (cp: Cp)
inline def centipawns = cp.value
inline def pawns: Float = cp.value / 100f
inline def pawns: Float = cp.value / 100f
inline def showPawns: String = "%.2f".format(pawns)
inline def ceiled: Cp =
@@ -43,7 +43,7 @@ object Eval:
else if cp.value < -Cp.CEILING then -Cp.CEILING
else cp
inline def invert: Cp = Cp(-cp.value)
inline def invert: Cp = Cp(-cp.value)
inline def invertIf(cond: Boolean): Cp = if cond then invert else cp
def signum: Int = Math.signum(cp.value.toFloat).toInt
@@ -55,7 +55,7 @@ object Eval:
extension (mate: Mate)
inline def moves: Int = mate.value
inline def invert: Mate = Mate(-moves)
inline def invert: Mate = Mate(-moves)
inline def invertIf(cond: Boolean): Mate = if cond then invert else mate
inline def signum: Int = if positive then 1 else -1
+45 -45
View File
@@ -11,23 +11,23 @@ case class BinaryFen(value: Array[Byte]) extends AnyVal:
def read: Position.AndFullMoveNumber =
val reader = new Iterator[Byte]:
val inner = value.iterator
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))
var pawns = Bitboard.empty
var pawns = Bitboard.empty
var knights = Bitboard.empty
var bishops = Bitboard.empty
var rooks = Bitboard.empty
var queens = Bitboard.empty
var kings = Bitboard.empty
var white = Bitboard.empty
var black = Bitboard.empty
var rooks = Bitboard.empty
var queens = Bitboard.empty
var kings = Bitboard.empty
var white = Bitboard.empty
var black = Bitboard.empty
var turn = White
var unmovedRooks = UnmovedRooks(Bitboard.empty)
var turn = White
var unmovedRooks = UnmovedRooks(Bitboard.empty)
var epMove: Option[Uci.Move] = None
def unpackPiece(sq: Square, nibble: Int) =
@@ -96,8 +96,8 @@ case class BinaryFen(value: Array[Byte]) extends AnyVal:
if it.hasNext then unpackPiece(it.next, hi)
val halfMoveClock = HalfMoveClock(readLeb128(reader))
val ply = Ply(readLeb128(reader))
val variant = reader.next match
val ply = Ply(readLeb128(reader))
val variant = reader.next match
case 0 => Standard
case 1 => Crazyhouse
case 2 => Chess960
@@ -165,10 +165,10 @@ case class BinaryFen(value: Array[Byte]) extends AnyVal:
ply.fullMoveNumber
)
override def hashCode: Int = value.toSeq.hashCode
override def hashCode: Int = value.toSeq.hashCode
override def equals(that: Any): Boolean = that match
case thatFen: BinaryFen => value.toSeq.equals(thatFen.value.toSeq)
case _ => false
case _ => false
object BinaryFen:
@@ -181,7 +181,7 @@ object BinaryFen:
.updateHistory(_.setHalfMoveClock(HalfMoveClock.initial))
.withVariant(position.variant match
case Standard | Chess960 | FromPosition => Standard
case other => other),
case other => other),
FullMoveNumber.initial
)
)
@@ -202,38 +202,38 @@ object BinaryFen:
// Encoding from
// https://github.com/official-stockfish/nnue-pytorch/blob/2db3787d2e36f7142ea4d0e307b502dda4095cd9/lib/nnue_training_data_formats.h#L4607
case Some(Piece(_, Pawn)) if pawnPushedTo.contains(sq) => 12
case Some(Piece(White, Pawn)) => 0
case Some(Piece(Black, Pawn)) => 1
case Some(Piece(White, Knight)) => 2
case Some(Piece(Black, Knight)) => 3
case Some(Piece(White, Bishop)) => 4
case Some(Piece(Black, Bishop)) => 5
case Some(Piece(White, Rook)) => if unmovedRooks.contains(sq) then 13 else 6
case Some(Piece(Black, Rook)) => if unmovedRooks.contains(sq) then 14 else 7
case Some(Piece(White, Queen)) => 8
case Some(Piece(Black, Queen)) => 9
case Some(Piece(White, King)) => 10
case Some(Piece(Black, King)) => if position.color.white then 11 else 15
case None => 0 // unreachable
case Some(Piece(White, Pawn)) => 0
case Some(Piece(Black, Pawn)) => 1
case Some(Piece(White, Knight)) => 2
case Some(Piece(Black, Knight)) => 3
case Some(Piece(White, Bishop)) => 4
case Some(Piece(Black, Bishop)) => 5
case Some(Piece(White, Rook)) => if unmovedRooks.contains(sq) then 13 else 6
case Some(Piece(Black, Rook)) => if unmovedRooks.contains(sq) then 14 else 7
case Some(Piece(White, Queen)) => 8
case Some(Piece(Black, Queen)) => 9
case Some(Piece(White, King)) => 10
case Some(Piece(Black, King)) => if position.color.white then 11 else 15
case None => 0 // unreachable
val it = occupied.iterator
while it.hasNext
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
val brokenTurn = position.color.black && position.board.byPiece(Black, King).isEmpty
val ply = input.fullMoveNumber.ply(position.color).value
val brokenTurn = position.color.black && position.board.byPiece(Black, King).isEmpty
val variantHeader = position.variant match
case Standard => 0
case Crazyhouse => 1
case Chess960 => 2
case FromPosition => 3
case Standard => 0
case Crazyhouse => 1
case Chess960 => 2
case FromPosition => 3
case KingOfTheHill => 4
case ThreeCheck => 5
case Antichess => 6
case Atomic => 7
case Horde => 8
case RacingKings => 9
case ThreeCheck => 5
case Antichess => 6
case Atomic => 7
case Horde => 8
case RacingKings => 9
if halfMoveClock > 0 || ply > 1 || brokenTurn || variantHeader != 0
then writeLeb128(builder, halfMoveClock)
@@ -248,7 +248,7 @@ object BinaryFen:
writeNibbles(builder, position.history.checkCount.white, position.history.checkCount.black)
else if position.variant.crazyhouse then
val crazyData = position.crazyData.getOrElse(Crazyhouse.Data.init)
val pockets = crazyData.pockets
val pockets = crazyData.pockets
writeNibbles(builder, pockets.white.pawn, pockets.black.pawn)
writeNibbles(builder, pockets.white.knight, pockets.black.knight)
writeNibbles(builder, pockets.white.bishop, pockets.black.bishop)
@@ -289,7 +289,7 @@ object BinaryFen:
builder.addOne(n.toByte)
def readLeb128(reader: Iterator[Byte]): Int =
var n = 0
var n = 0
var shift = 0
while
val b = reader.next
@@ -307,8 +307,8 @@ object BinaryFen:
((b & 0xf), (b >>> 4) & 0xf)
def minimumUnmovedRooks(position: Position): UnmovedRooks =
val white = position.history.unmovedRooks.bb & position.white & Bitboard.firstRank
val black = position.history.unmovedRooks.bb & position.black & Bitboard.lastRank
val white = position.history.unmovedRooks.bb & position.white & Bitboard.firstRank
val black = position.history.unmovedRooks.bb & position.black & Bitboard.lastRank
val castles = position.history.castles
UnmovedRooks(
(if castles.whiteKingSide then white.isolateLast else Bitboard.empty) |
@@ -325,8 +325,8 @@ object BinaryFen:
): Castles =
val whiteRooks = unmovedRooks.bb & white & Bitboard.firstRank
val blackRooks = unmovedRooks.bb & black & Bitboard.lastRank
val whiteKing = (white & kings & Bitboard.firstRank).first.getOrElse(Square.E1)
val blackKing = (black & kings & Bitboard.lastRank).first.getOrElse(Square.E8)
val whiteKing = (white & kings & Bitboard.firstRank).first.getOrElse(Square.E1)
val blackKing = (black & kings & Bitboard.lastRank).first.getOrElse(Square.E8)
Castles(
whiteKingSide = whiteRooks.last.exists(r => r.value > whiteKing.value),
whiteQueenSide = whiteRooks.first.exists(r => r.value < whiteKing.value),
+12 -12
View File
@@ -25,11 +25,11 @@ object FullFen extends OpaqueString[FullFen]:
def isInitial: Boolean = a == initial
def simple: SimpleFen = SimpleFen.fromFull(a)
def simple: SimpleFen = SimpleFen.fromFull(a)
def opening: StandardFen = SimpleFen.opening(a)
def board: BoardFen = SimpleFen.board(a)
def board: BoardFen = SimpleFen.board(a)
val initial: FullFen = FullFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
val initial: FullFen = FullFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
def clean(source: String): FullFen = FullFen(source.replace("_", " ").trim)
def getColor(arr: Array[String]): Option[Color] =
if arr.length < 2 then None
@@ -41,23 +41,23 @@ object FullFen extends OpaqueString[FullFen]:
opaque type SimpleFen = String
object SimpleFen extends OpaqueString[SimpleFen]:
extension (a: SimpleFen)
def color: Option[Color] = a.split(' ').lift(1).flatMap(_.headOption).flatMap(Color.apply)
def colorOrWhite: Color = color | Color.White
def castling: String = a.split(' ').lift(2) | "-"
def color: Option[Color] = a.split(' ').lift(1).flatMap(_.headOption).flatMap(Color.apply)
def colorOrWhite: Color = color | Color.White
def castling: String = a.split(' ').lift(2) | "-"
def enpassant: Option[Square] = a.split(' ').lift(3).flatMap(Square.fromKey(_))
def opening: StandardFen = StandardFen.fromSimple(a)
def board: BoardFen = a.takeWhile(_ != ' ')
def opening: StandardFen = StandardFen.fromSimple(a)
def board: BoardFen = a.takeWhile(_ != ' ')
def fromFull(fen: FullFen): StandardFen =
fen.value.split(' ').take(4) match
case Array(board, turn, castle, ep) => SimpleFen(s"$board $turn $castle $ep")
case _ => fen.into(SimpleFen)
case _ => fen.into(SimpleFen)
// Like SimpleFen, but for standard chess, without ZH pockets
opaque type StandardFen = String
object StandardFen extends OpaqueString[StandardFen]:
extension (a: StandardFen) def board: BoardFen = a.value.takeWhile(_ != ' ')
def fromFull(fen: FullFen): StandardFen = fromSimple(FullFen.simple(fen))
def fromSimple(fen: SimpleFen): StandardFen =
def fromFull(fen: FullFen): StandardFen = fromSimple(FullFen.simple(fen))
def fromSimple(fen: SimpleFen): StandardFen =
fen.value.split(' ').take(4) match
case Array(board, turn, castle, ep) =>
StandardFen(s"${BoardFen(board).removePockets} $turn $castle $ep")
@@ -72,7 +72,7 @@ object BoardAndColorFen extends OpaqueString[BoardAndColorFen]
opaque type BoardFen = String
object BoardFen extends OpaqueString[BoardFen]:
extension (a: BoardFen)
def andColor(c: Color) = BoardAndColorFen(s"$a ${c.letter}")
def andColor(c: Color) = BoardAndColorFen(s"$a ${c.letter}")
def removePockets: BoardFen =
if a.contains('[') then a.takeWhile('[' !=)
else if a.count('/' == _) == 8 then a.split('/').take(8).mkString("/")
+20 -20
View File
@@ -20,7 +20,7 @@ trait FenReader:
makeBoard(variant, fBoard).map { (board, crazyData) =>
// We trust Fen's color to be correct, if there is no color we use the color of the king in check
// If there is no king in check we use white
val color = fColor.orElse(variant.checkColor(board)) | Color.White
val color = fColor.orElse(variant.checkColor(board)) | Color.White
val position = new Position(
board,
History(
@@ -36,7 +36,7 @@ trait FenReader:
else
fCastling.foldLeft(Castles.none -> UnmovedRooks.none):
case ((c, r), ch) =>
val color = Color.fromWhite(ch.isUpper)
val color = Color.fromWhite(ch.isUpper)
val backRank = Bitboard.rank(color.backRank)
// rooks that can be used for castling
val rooks = position.rooks & position.byColor(color) & backRank
@@ -44,8 +44,8 @@ trait FenReader:
for
kingSquare <- (position.kingOf(color) & backRank).first
rookSquare <- ch.toLower match
case 'k' => rooks.findLast(_ ?> kingSquare)
case 'q' => rooks.find(_ ?< kingSquare)
case 'k' => rooks.findLast(_ ?> kingSquare)
case 'q' => rooks.find(_ ?< kingSquare)
case file => rooks.find(_.file.char == file)
side <- Side.kingRookSide(kingSquare, rookSquare)
yield (c.add(color, side), r | rookSquare.bl)
@@ -149,33 +149,33 @@ trait FenReader:
def makeBoardWithCrazyPromoted(boardFen: String): (BoardWithCrazyPromoted, Option[String]) =
var promoted = Bitboard.empty
var pawns = Bitboard.empty
var knights = Bitboard.empty
var bishops = Bitboard.empty
var rooks = Bitboard.empty
var queens = Bitboard.empty
var kings = Bitboard.empty
var white = Bitboard.empty
var black = Bitboard.empty
var pawns = Bitboard.empty
var knights = Bitboard.empty
var bishops = Bitboard.empty
var rooks = Bitboard.empty
var queens = Bitboard.empty
var kings = Bitboard.empty
var white = Bitboard.empty
var black = Bitboard.empty
var occupied = Bitboard.empty
inline def addPieceAt(p: Piece, s: Long) =
occupied |= s
p.role match
case Pawn => pawns |= s
case Pawn => pawns |= s
case Knight => knights |= s
case Bishop => bishops |= s
case Rook => rooks |= s
case Queen => queens |= s
case King => kings |= s
case Rook => rooks |= s
case Queen => queens |= s
case King => kings |= s
p.color match
case Color.White => white |= s
case Color.Black => black |= s
var rank = 7
var file = 0
val iter = boardFen.iterator.buffered
var rank = 7
var file = 0
val iter = boardFen.iterator.buffered
var error = none[String]
while iter.hasNext && error.isEmpty
do
@@ -185,7 +185,7 @@ trait FenReader:
if rank < 0 then error = Some("too many ranks")
else
iter.next match
case '/' => // ignored, optional. Rank switch is automatic
case '/' => // ignored, optional. Rank switch is automatic
case ch if numberSet.contains(ch) =>
file += (ch - '0')
if file > 8 then error = Some(s"file = $file")
+6 -6
View File
@@ -12,7 +12,7 @@ import chess.variant.Crazyhouse
trait FenWriter:
private given Ordering[File] = Ordering.by[File, Int](_.value)
given Ordering[Square] = Ordering.by[Square, File](_.file)
given Ordering[Square] = Ordering.by[Square, File](_.file)
def write(position: Position): FullFen =
write(position, FullMoveNumber(1))
@@ -47,13 +47,13 @@ trait FenWriter:
.fold("-")(_.key)}"
def writeBoard(position: Position): BoardFen =
val fen = scala.collection.mutable.StringBuilder(70)
val fen = scala.collection.mutable.StringBuilder(70)
var empty = 0
for y <- Rank.allReversed do
empty = 0
for x <- File.all do
position.pieceAt(x, y) match
case None => empty = empty + 1
case None => empty = empty + 1
case Some(piece) =>
if empty == 0 then fen.append(piece.forsyth.toString)
else
@@ -81,8 +81,8 @@ trait FenWriter:
case _ => ""
private[chess] def writeCastles(position: Position): String =
val wr = position.rooks & position.white & Bitboard.rank(White.backRank)
val br = position.rooks & position.black & Bitboard.rank(Black.backRank)
val wr = position.rooks & position.white & Bitboard.rank(White.backRank)
val br = position.rooks & position.black & Bitboard.rank(Black.backRank)
val wur = position.unmovedRooks.without(Black).bb
val bur = position.unmovedRooks.without(White).bb
(if position.castles.whiteKingSide then
@@ -106,4 +106,4 @@ trait FenWriter:
.getOrElse("q")
else "") match
case "" => "-"
case s => s
case s => s
+5 -5
View File
@@ -21,10 +21,10 @@ object Uci:
) extends Uci:
def keys: String = s"${orig.key}${dest.key}"
def uci: String = s"$keys$promotionString"
def uci: String = s"$keys$promotionString"
def charKeys: String = s"${orig.asChar}${dest.asChar}"
def chars: String = s"$charKeys$promotionString"
def chars: String = s"$charKeys$promotionString"
def promotionString: String = promotion.fold("")(_.forsyth.toString)
@@ -67,12 +67,12 @@ object Uci:
object Drop:
def fromChars(move: String): Option[Drop] = for
role <- move.headOption.flatMap(Role.allByPgn.get)
role <- move.headOption.flatMap(Role.allByPgn.get)
square <- move.lift(2).flatMap(Square.fromChar)
yield Uci.Drop(role, square)
def fromStrings(roleS: String, posS: String): Option[Drop] = for
role <- Role.allByName.get(roleS)
role <- Role.allByName.get(roleS)
square <- Square.fromKey(posS)
yield Drop(role, square)
@@ -85,7 +85,7 @@ object Uci:
def apply(move: String): Option[Uci] =
if move.lift(1).contains('@') then
for
role <- move.headOption.flatMap(Role.allByPgn.get)
role <- move.headOption.flatMap(Role.allByPgn.get)
square <- Square.fromKey(move.slice(2, 4))
yield Uci.Drop(role, square)
else Uci.Move(move)
+6 -6
View File
@@ -13,10 +13,10 @@ case class UciCharPair(a: Char, b: Char):
char2squareMap.get(b) match
case Some(sq) => Uci.Move(from, sq, None)
case None =>
case None =>
char2promotionMap.get(b) match
case Some((file, prom)) => Uci.Move(from, Square(file, lastRank(from)), Some(prom))
case None => Uci.Drop(unsafeCharToDropRole(b), from)
case None => Uci.Drop(unsafeCharToDropRole(b), from)
object UciCharPair:
@@ -24,14 +24,14 @@ object UciCharPair:
def apply(uci: Uci): UciCharPair =
uci match
case Uci.Move(orig, dest, None) => UciCharPair(toChar(orig), toChar(dest))
case Uci.Move(orig, dest, None) => UciCharPair(toChar(orig), toChar(dest))
case Uci.Move(orig, dest, Some(role)) => UciCharPair(toChar(orig), toChar(dest.file, role))
case Uci.Drop(role, square) => UciCharPair(toChar(square), dropRole2charMap.getOrElse(role, voidChar))
object implementation:
val charShift = 35 // Start at Char(35) == '#'
val voidChar = 33.toChar // '!'. We skipped Char(34) == '"'.
val charShift = 35 // Start at Char(35) == '#'
val voidChar = 33.toChar // '!'. We skipped Char(34) == '"'.
val square2charMap: Map[Square, Char] = Square.all.map { square =>
square -> (square.hashCode + charShift).toChar
@@ -44,7 +44,7 @@ object UciCharPair:
val promotion2charMap: Map[(File, PromotableRole), Char] = for
(role, index) <- Role.allPromotable.zipWithIndex.toMap
file <- File.all
file <- File.all
yield (file, role) -> (charShift + square2charMap.size + index * 8 + file.value).toChar
lazy val char2promotionMap: Map[Char, (File, PromotableRole)] =
+4 -4
View File
@@ -5,13 +5,13 @@ package format
* Made from concatenated UciCharPair strings */
opaque type UciPath = String
object UciPath extends OpaqueString[UciPath]:
def fromId(id: UciCharPair): UciPath = id.toString
def fromId(id: UciCharPair): UciPath = id.toString
def fromIds(ids: Iterable[UciCharPair]): UciPath = ids.mkString
extension (e: UciPath)
def computeIds: Iterator[UciCharPair] = e.grouped(2).flatMap(strToId)
def ids: List[UciCharPair] = computeIds.toList
def ids: List[UciCharPair] = computeIds.toList
def head: Option[UciCharPair] = strToId(e)
@@ -19,13 +19,13 @@ object UciPath extends OpaqueString[UciPath]:
def split: Option[(UciCharPair, UciPath)] = head.map(_ -> e.drop(2))
inline def isEmpty: Boolean = e.isEmpty
inline def isEmpty: Boolean = e.isEmpty
inline def nonEmpty: Boolean = !isEmpty
def lastId: Option[UciCharPair] = strToId(e.takeRight(2))
def +(id: UciCharPair): UciPath = e + id.toString
def +(more: UciPath): UciPath = e + more
def +(more: UciPath): UciPath = e + more
def prepend(id: UciCharPair): UciPath = id.toString + e
+43 -43
View File
@@ -5,28 +5,28 @@ import scala.util.Try
object Binary:
def writeMove(m: SanStr): Try[List[Byte]] = Try(Writer.move(m))
def writeMove(m: SanStr): Try[List[Byte]] = Try(Writer.move(m))
def writeMoves(ms: Iterable[SanStr]): Try[Array[Byte]] = Try(Writer.moves(ms))
def readMoves(bs: List[Byte]): Try[List[SanStr]] = Try(Reader.moves(bs))
def readMoves(bs: List[Byte]): Try[List[SanStr]] = Try(Reader.moves(bs))
def readMoves(bs: List[Byte], nb: Int): Try[List[SanStr]] = Try(Reader.moves(bs, nb))
private object MoveType:
val SimplePawn = 0
val SimplePawn = 0
val SimplePiece = 1
val FullPawn = 2
val FullPiece = 3
val FullPawn = 2
val FullPiece = 3
private object Encoding:
val pieceInts: Map[String, Int] =
Map("K" -> 1, "Q" -> 2, "R" -> 3, "N" -> 4, "B" -> 5, "O-O" -> 6, "O-O-O" -> 7)
val pieceStrs: Map[Int, String] = pieceInts.map { case (k, v) => v -> k }
val pieceStrs: Map[Int, String] = pieceInts.map { case (k, v) => v -> k }
val dropPieceInts: Map[String, Int] = Map("P" -> 1, "Q" -> 2, "R" -> 3, "N" -> 4, "B" -> 5)
val dropPieceStrs: Map[Int, String] = dropPieceInts.map { case (k, v) => v -> k }
val promotionInts: Map[String, Int] = Map("" -> 0, "Q" -> 1, "R" -> 2, "N" -> 3, "B" -> 4, "K" -> 6)
val promotionStrs: Map[Int, String] = promotionInts.map { case (k, v) => v -> k }
val checkInts: Map[String, Int] = Map("" -> 0, "+" -> 1, "#" -> 2)
val checkStrs: Map[Int, String] = checkInts.map { case (k, v) => v -> k }
val checkInts: Map[String, Int] = Map("" -> 0, "+" -> 1, "#" -> 2)
val checkStrs: Map[Int, String] = checkInts.map { case (k, v) => v -> k }
private object Reader:
@@ -34,13 +34,13 @@ object Binary:
private val maxPlies = 600
def moves(bs: List[Byte]): List[SanStr] = moves(bs, maxPlies)
def moves(bs: List[Byte]): List[SanStr] = moves(bs, maxPlies)
def moves(bs: List[Byte], nb: Int): List[SanStr] = SanStr.from(intMoves(bs.map(Binary.toInt(_)), nb))
def intMoves(bs: List[Int], pliesToGo: Int): List[String] =
bs match
case _ if pliesToGo <= 0 => Nil
case Nil => Nil
case _ if pliesToGo <= 0 => Nil
case Nil => Nil
case b1 :: rest if moveType(b1) == MoveType.SimplePawn =>
simplePawn(b1) :: intMoves(rest, pliesToGo - 1)
case b1 :: b2 :: rest if moveType(b1) == MoveType.SimplePiece =>
@@ -69,47 +69,47 @@ object Binary:
val check = checkStrs(cut(b2, 5, 3))
s"$castle$check"
case piece =>
val square = squareString(right(b1, 6))
val square = squareString(right(b1, 6))
val capture = if bitAt(b2, 3) then "x" else ""
val check = checkStrs(cut(b2, 5, 3))
val check = checkStrs(cut(b2, 5, 3))
s"$piece$capture$square$check"
def drop(b1: Int, b2: Int): String =
val piece = dropPieceStrs(b2 >> 5)
val piece = dropPieceStrs(b2 >> 5)
val square = squareString(right(b1, 6))
val check = checkStrs(cut(b2, 5, 3))
val check = checkStrs(cut(b2, 5, 3))
s"$piece@$square$check"
def fullPawn(b1: Int, b2: Int): String =
val square = squareString(right(b1, 6))
val square = squareString(right(b1, 6))
val fileCapture = (b2 >> 6) match
case 1 => s"${(square(0) - 1).toChar}x"
case 2 => s"${(square(0) + 1).toChar}x"
case _ => ""
val check = checkStrs(cut(b2, 6, 4))
val prom = promotionStrs(cut(b2, 4, 1))
val check = checkStrs(cut(b2, 6, 4))
val prom = promotionStrs(cut(b2, 4, 1))
val promotion = if prom.isEmpty then "" else s"=$prom"
s"$fileCapture$square$promotion$check"
def fullPiece(b1: Int, b2: Int, b3: Int): String =
val square = squareString(right(b1, 6))
val piece = pieceStrs(b2 >> 5)
val square = squareString(right(b1, 6))
val piece = pieceStrs(b2 >> 5)
val capture = if bitAt(b2, 3) then "x" else ""
val check = checkStrs(cut(b2, 5, 3))
val disamb = (b3 >> 6) match
val check = checkStrs(cut(b2, 5, 3))
val disamb = (b3 >> 6) match
case 0 => fileChar(right(b3, 3)).toString
case 1 => rankChar(right(b3, 3)).toString
case _ => squareString(right(b3, 6))
s"$piece$disamb$capture$square$check"
private def moveType(i: Int) = i >> 6
private def moveType(i: Int) = i >> 6
private def squareString(i: Int) = fileChar(i >> 3).toString + rankChar(right(i, 3))
private def fileChar(i: Int) = (i + 97).toChar
private def rankChar(i: Int) = (i + 49).toChar
private def fileChar(i: Int) = (i + 97).toChar
private def rankChar(i: Int) = (i + 49).toChar
private def right(i: Int, x: Int): Int = i & lengthMasks(x)
private def right(i: Int, x: Int): Int = i & lengthMasks(x)
private def cut(i: Int, from: Int, to: Int): Int = right(i, from) >> to
private def bitAt(i: Int, p: Int): Boolean = cut(i, p, p - 1) != 0
private val lengthMasks =
private def bitAt(i: Int, p: Int): Boolean = cut(i, p, p - 1) != 0
private val lengthMasks =
Map(1 -> 0x01, 2 -> 0x03, 3 -> 0x07, 4 -> 0x0f, 5 -> 0x1f, 6 -> 0x3f, 7 -> 0x7f, 8 -> 0xff)
private def !!(msg: String) = throw Exception("Binary reader failed: " + msg)
@@ -119,8 +119,8 @@ object Binary:
def move(str: SanStr): List[Byte] =
(str.value match
case square if square.length == 2 => simplePawn(square)
case CastlingR(str, check) => castling(str, check)
case square if square.length == 2 => simplePawn(square)
case CastlingR(str, check) => castling(str, check)
case SimplePieceR(piece, capture, square, check) =>
simplePiece(piece, square, capture, check)
case FullPawnR(file, square, promotion, check) =>
@@ -178,7 +178,7 @@ object Binary:
else if orig.head.toInt < 97 then rankInt(orig.head)
else fileInt(orig.head)
def boolInt(s: String): Int = if s.nonEmpty then 1 else 0
def boolInt(s: String): Int = if s.nonEmpty then 1 else 0
def boolInt(b: Boolean): Int = if b then 1 else 0
def posInt(square: String): Int = posInt(fileInt(square.head), rankInt(square(1)))
@@ -192,18 +192,18 @@ object Binary:
if file.head < square.head then 1 else 2
}
val pieceR = "([KQRNB])"
val fileR = "(?:([a-h])x)?"
val posR = "([a-h][1-9])"
val captureR = "(x?)"
val checkR = "([\\+#]?)"
val promotionR = "(?:\\=?([QRNBK]))?"
val origR = "([a-h]?[1-8]?)".r
val pieceR = "([KQRNB])"
val fileR = "(?:([a-h])x)?"
val posR = "([a-h][1-9])"
val captureR = "(x?)"
val checkR = "([\\+#]?)"
val promotionR = "(?:\\=?([QRNBK]))?"
val origR = "([a-h]?[1-8]?)".r
val SimplePieceR = s"^$pieceR$captureR$posR$checkR$$".r
val FullPawnR = s"^$fileR$posR$promotionR$checkR$$".r
val CastlingR = s"^(O-O|O-O-O)$checkR$$".r
val FullPieceR = s"^$pieceR$origR$captureR$posR$checkR$$".r
val DropR = s"^([QRNBP])@$posR$checkR$$".r
val FullPawnR = s"^$fileR$posR$promotionR$checkR$$".r
val CastlingR = s"^(O-O|O-O-O)$checkR$$".r
val FullPieceR = s"^$pieceR$origR$captureR$posR$checkR$$".r
val DropR = s"^([QRNBP])@$posR$checkR$$".r
private inline def toInt(b: Byte): Int = b & 0xff
private def showByte(b: Int): String = "%08d".format(b.toBinaryString.toInt)
private def showByte(b: Int): String = "%08d".format(b.toBinaryString.toInt)
+23 -23
View File
@@ -19,9 +19,9 @@ case class Glyphs(
def toggle(glyph: Glyph): Glyphs =
glyph match
case g: Glyph.MoveAssessment => copy(move = (!move.contains(g)).option(g))
case g: Glyph.MoveAssessment => copy(move = (!move.contains(g)).option(g))
case g: Glyph.PositionAssessment => copy(position = (!position.contains(g)).option(g))
case g: Glyph.Observation =>
case g: Glyph.Observation =>
copy(observations =
if observations.contains(g) then observations.filter(g !=)
else g :: observations
@@ -62,16 +62,16 @@ object Glyph:
sealed trait MoveAssessment extends Glyph
object MoveAssessment:
val good = new Glyph(1, "!", "Good move") with MoveAssessment
val mistake = new Glyph(2, "?", "Mistake") with MoveAssessment
val brilliant = new Glyph(3, "!!", "Brilliant move") with MoveAssessment
val blunder = new Glyph(4, "??", "Blunder") with MoveAssessment
val good = new Glyph(1, "!", "Good move") with MoveAssessment
val mistake = new Glyph(2, "?", "Mistake") with MoveAssessment
val brilliant = new Glyph(3, "!!", "Brilliant move") with MoveAssessment
val blunder = new Glyph(4, "??", "Blunder") with MoveAssessment
val interesting = new Glyph(5, "!?", "Interesting move") with MoveAssessment
val dubious = new Glyph(6, "?!", "Dubious move") with MoveAssessment
val only = new Glyph(7, "□", "Only move") with MoveAssessment
val zugzwang = new Glyph(22, "⨀", "Zugzwang") with MoveAssessment
val dubious = new Glyph(6, "?!", "Dubious move") with MoveAssessment
val only = new Glyph(7, "□", "Only move") with MoveAssessment
val zugzwang = new Glyph(22, "⨀", "Zugzwang") with MoveAssessment
val all = List(good, mistake, brilliant, blunder, interesting, dubious, only, zugzwang)
val all = List(good, mistake, brilliant, blunder, interesting, dubious, only, zugzwang)
val byId: Map[Int, Glyph] = all.mapBy(_.id)
def display = all
@@ -79,14 +79,14 @@ object Glyph:
sealed trait PositionAssessment extends Glyph
object PositionAssessment:
val equal = new Glyph(10, "=", "Equal position") with PositionAssessment
val unclear = new Glyph(13, "∞", "Unclear position") with PositionAssessment
val equal = new Glyph(10, "=", "Equal position") with PositionAssessment
val unclear = new Glyph(13, "∞", "Unclear position") with PositionAssessment
val whiteSlightlyBetter = new Glyph(14, "⩲", "White is slightly better") with PositionAssessment
val blackSlightlyBetter = new Glyph(15, "⩱", "Black is slightly better") with PositionAssessment
val whiteQuiteBetter = new Glyph(16, "±", "White is better") with PositionAssessment
val blackQuiteBetter = new Glyph(17, "∓", "Black is better") with PositionAssessment
val whiteMuchBetter = new Glyph(18, "+", "White is winning") with PositionAssessment
val blackMuchBetter = new Glyph(19, "-+", "Black is winning") with PositionAssessment
val whiteQuiteBetter = new Glyph(16, "±", "White is better") with PositionAssessment
val blackQuiteBetter = new Glyph(17, "∓", "Black is better") with PositionAssessment
val whiteMuchBetter = new Glyph(18, "+", "White is winning") with PositionAssessment
val blackMuchBetter = new Glyph(19, "-+", "Black is winning") with PositionAssessment
val all = List(
equal,
@@ -105,14 +105,14 @@ object Glyph:
sealed trait Observation extends Glyph
object Observation:
val novelty = new Glyph(146, "N", "Novelty") with Observation
val development = new Glyph(32, "↑↑", "Development") with Observation
val initiative = new Glyph(36, "↑", "Initiative") with Observation
val attack = new Glyph(40, "→", "Attack") with Observation
val counterplay = new Glyph(132, "⇆", "Counterplay") with Observation
val timeTrouble = new Glyph(138, "⊕", "Time trouble") with Observation
val novelty = new Glyph(146, "N", "Novelty") with Observation
val development = new Glyph(32, "↑↑", "Development") with Observation
val initiative = new Glyph(36, "↑", "Initiative") with Observation
val attack = new Glyph(40, "→", "Attack") with Observation
val counterplay = new Glyph(132, "⇆", "Counterplay") with Observation
val timeTrouble = new Glyph(138, "⊕", "Time trouble") with Observation
val compensation = new Glyph(44, "=∞", "With compensation") with Observation
val withIdea = new Glyph(140, "∆", "With the idea") with Observation
val withIdea = new Glyph(140, "∆", "With the idea") with Observation
val all = List(novelty, development, initiative, attack, counterplay, timeTrouble, compensation, withIdea)
val byId: Map[Int, Glyph] = all.mapBy(_.id)
+23 -23
View File
@@ -10,9 +10,9 @@ import cats.syntax.all.*
object Parser:
// https://unicode-explorer.com/c/00A0
private val nbsp = P.char('\u00A0')
private val nbsp = P.char('\u00A0')
private val whitespace = R.cr | R.lf | R.wsp | nbsp | P.char('\uFEFF')
val pgnComment = P.caret.filter(_.col == 0) *> P.char('%') *> P.until(P.char('\n')).void
val pgnComment = P.caret.filter(_.col == 0) *> P.char('%') *> P.until(P.char('\n')).void
// pgnComment with % or whitespaces
private val escape = pgnComment.? *> whitespace.rep0.?
@@ -54,9 +54,9 @@ object Parser:
def san(str: SanStr): Either[ErrorStr, San] =
simpleSan.parse(str.value, "Error parsing move")
private val blockComment = P.until0(P.char('}')).with1.between(P.char('{'), P.char('}')).map(Comment(_))
private val blockComment = P.until0(P.char('}')).with1.between(P.char('{'), P.char('}')).map(Comment(_))
private val inlineComment = P.char(';') *> P.until(R.lf).map(Comment(_))
private val comment = (blockComment | inlineComment).withContext("Invalid comment") <* escape
private val comment = (blockComment | inlineComment).withContext("Invalid comment") <* escape
private def mapResult(result: String): String = Outcome.fromResult(result).fold(result)(_.toString)
@@ -84,8 +84,8 @@ object Parser:
.?
.flatMap(o => o.fold(P.unit)(_ => P.failWith("Null moves are not supported").void))
private val preMoveEscape = ((number.backtrack | comment).rep0 ~ forbidNullMove).void
private val moveAndMetas = SanParser.san ~ SanParser.metas
private val preMoveEscape = ((number.backtrack | comment).rep0 ~ forbidNullMove).void
private val moveAndMetas = SanParser.san ~ SanParser.metas
private val postMoveEscape = moveExtras.rep0.void <* escape
private val simpleSan: P[San] =
@@ -112,7 +112,7 @@ object Parser:
(P.char('(') *> comment.rep0.surroundedBy(escape) ~ recurse.rep0 <* (P.char(')') ~ escape))
.map((comments, sans) =>
sans match
case Nil => None
case Nil => None
case x :: xs =>
Variation(x.value.copy(variationComments = comments.cleanUp), Tree.build(xs)).some
)
@@ -135,7 +135,7 @@ object Parser:
val rankMap = Rank.all.mapBy(_.char)
val glyph: P[Glyph] = mapParser(Glyph.MoveAssessment.all.mapBy(_.symbol), "glyph")
val glyphs = glyph.rep0.map(Glyphs.fromList)
val glyphs = glyph.rep0.map(Glyphs.fromList)
val capture = P.char('x').?.map(_.isDefined)
@@ -194,7 +194,7 @@ object Parser:
val castleQSide = List("O-O-O", "o-o-o", "0-0-0", "OOO", "ooo", "000", "OOO", "ooo", "000")
val qCastle: P[Side] = P.stringIn(castleQSide).as(QueenSide)
val castleKSide = List("O-O", "o-o", "0-0", "OO", "oo", "00", "OO", "oo", "00")
val castleKSide = List("O-O", "o-o", "0-0", "OO", "oo", "00", "OO", "oo", "00")
val kCastle: P[Side] = P.stringIn(castleKSide).as(KingSide)
val castle: P[San] = (qCastle | kCastle).withString.map((side, raw) => Castle(side, raw.some))
@@ -222,17 +222,17 @@ object Parser:
val tagName: P[String] = R.alpha.rep.string.withContext("Tag name can only contains alphabet characters")
val escaped: P[String] = P.char('\\') *> (R.dquote | P.char('\\')).string
val valueChar: P[String] = escaped | P.charWhere(_ != '"').string
val tagValue: P[String] = valueChar.rep0.map(_.mkString).with1.surroundedBy(R.dquote)
val tagContent: P[Tag] = ((tagName <* R.wsp.rep) ~ tagValue).map(Tag(_, _))
val tag: P[Tag] = tagContent.between(P.char('['), P.char(']')) <* whitespace.rep0
val tags: P0[List[Tag]] = tag.rep0
val tagValue: P[String] = valueChar.rep0.map(_.mkString).with1.surroundedBy(R.dquote)
val tagContent: P[Tag] = ((tagName <* R.wsp.rep) ~ tagValue).map(Tag(_, _))
val tag: P[Tag] = tagContent.between(P.char('['), P.char(']')) <* whitespace.rep0
val tags: P0[List[Tag]] = tag.rep0
private val tagsAndMovesParser: P0[ParsedPgn] =
(TagParser.tags.surroundedBy(escape) ~ fullMovesParser.?)
.map: (optionalTags, optionalMoves) =>
val preTags = Tags.sanitize(optionalTags)
optionalMoves match
case None => ParsedPgn(InitialComments.empty, preTags, None)
case None => ParsedPgn(InitialComments.empty, preTags, None)
case Some((init, nodes, result)) =>
ParsedPgn(init, updateTagsWithResult(preTags, result), Tree.build(nodes))
@@ -242,7 +242,7 @@ object Parser:
(TagParser.tags.surroundedBy(escape) ~ p.?).map: (optionalTags, optionalMoves) =>
val preTags = Tags.sanitize(optionalTags)
optionalMoves match
case None => ParsedMainline(InitialComments.empty, preTags, Nil)
case None => ParsedMainline(InitialComments.empty, preTags, Nil)
case Some((init, sans, result)) =>
ParsedMainline(init, updateTagsWithResult(preTags, result), sans)
@@ -252,7 +252,7 @@ object Parser:
private inline def escapePgnTag[A](p: P0[A]): P0[A] =
escape *> P.string("[pgn]").? *> p <* P.string("[/pgn]").? <* escape
private val tagsParser = TagParser.tags.surroundedBy(escape)
private val tagsParser = TagParser.tags.surroundedBy(escape)
private val pgnParser: P0[ParsedPgn] = escapePgnTag(tagsAndMovesParser)
private val pgnMainlineParser: P0[ParsedMainline[SanWithMetas]] =
@@ -266,8 +266,8 @@ object Parser:
p.parse(str).bimap(showExpectations(context, str), _._2)
private def showExpectations(context: String, str: String)(error: P.Error): ErrorStr =
val lm = LocationMap(str)
val idx = error.failedAtOffset
val lm = LocationMap(str)
val idx = error.failedAtOffset
val caret = lm.toCaret(idx).getOrElse(throw RuntimeException("This is impossible"))
ErrorStr:
s"$context: ${expToString(error.expected.head)} at line ${caret.line + 1}, column ${caret.col + 1}"
@@ -277,16 +277,16 @@ object Parser:
case Expectation.OneOfStr(_, strs) =>
strs match
case one :: Nil => s"expected: $one"
case _ => s"expected one of: $strs"
case _ => s"expected one of: $strs"
case Expectation.InRange(_, lower, upper) =>
if lower == upper then s"expected: $lower"
else s"expected char in range: [$lower, $upper]"
case Expectation.StartOfString(_) => "expected start of the file"
case Expectation.EndOfString(_, length) => s"expected end of file but $length characters remaining"
case Expectation.StartOfString(_) => "expected start of the file"
case Expectation.EndOfString(_, length) => s"expected end of file but $length characters remaining"
case Expectation.Length(_, expected, actual) =>
s"expected $expected more characters but only $actual remaining"
case Expectation.ExpectedFailureAt(_, matched) =>
s"expected failure but the parser matched: $matched"
case Expectation.Fail(_) => "Failed"
case Expectation.FailWith(_, message) => message
case Expectation.Fail(_) => "Failed"
case Expectation.FailWith(_, message) => message
case Expectation.WithContext(contextStr, _) => contextStr
+2 -2
View File
@@ -69,7 +69,7 @@ case class Move(
builder.append(san.value)
glyphs.toList.foreach:
case glyph if glyph.id <= 6 => builder.append(glyph.symbol)
case glyph => builder.append(" $").append(glyph.id)
case glyph => builder.append(" $").append(glyph.id)
if nonEmpty then
List(clockString, opening, result).flatten
.:::(comments.map(_.map(Move.noDoubleLineBreak)))
@@ -84,7 +84,7 @@ object Move:
given PgnNodeEncoder[Move] with
extension (m: Move)
def appendSanStr(builder: StringBuilder) = m.appendSanStr(builder)
def appendSanStr(builder: StringBuilder) = m.appendSanStr(builder)
def appendVariationComment(builder: StringBuilder) =
m.variationComments.foreach(x => builder.append(" { ").append(x.value).append(" }"))
def hasComment = m.hasComment
@@ -62,7 +62,7 @@ object PgnNodeEncoder:
addTurnNumberPrefix(forceTurnNumber, builder, ply)
renderValueAndVariations(builder, ply)
tree.child.match
case None => ()
case None => ()
case Some(x) =>
builder.addOne(' ')
x.render(builder, tree.forceTurnNumber(ply), ply.next)
+43 -43
View File
@@ -16,9 +16,9 @@ case class Tag(name: TagType, value: String):
str.replace("\\", "\\\\").replace("\"", "\\\"")
trait TagType:
lazy val name = toString
lazy val name = toString
lazy val lowercase = name.toLowerCase
val isUnknown = false
val isUnknown = false
case class Tags(value: List[Tag]) extends AnyVal:
@@ -41,15 +41,15 @@ case class Tags(value: List[Tag]) extends AnyVal:
.map(_.toLowerCase)
.flatMap:
case "chess 960" | "fischerandom" | "fischerrandom" => chess.variant.Chess960.some
case "3 check" | "3-check" => chess.variant.ThreeCheck.some
case name => chess.variant.Variant.byName(name)
case "3 check" | "3-check" => chess.variant.ThreeCheck.some
case name => chess.variant.Variant.byName(name)
def anyDate: Option[String] = apply(_.UTCDate).orElse(apply(_.Date))
def year: Option[Int] =
anyDate.flatMap:
case Tags.DateRegex(y, _, _) => y.toIntOption
case _ => None
case _ => None
def fen: Option[FullFen] = FullFen.from(apply(_.FEN))
@@ -88,7 +88,7 @@ case class Tags(value: List[Tag]) extends AnyVal:
.flatMap(_.split('.').lift(1))
.flatMap(_.toIntOption)
def names: ByColor[Option[PlayerName]] = ByColor(apply(_.White), apply(_.Black)).map(PlayerName.from(_))
def names: ByColor[Option[PlayerName]] = ByColor(apply(_.White), apply(_.Black)).map(PlayerName.from(_))
def ratings: ByColor[Option[IntRating]] = ByColor(apply(_.WhiteElo), apply(_.BlackElo)).map: r =>
IntRating.from(r.flatMap(_.toIntOption))
def titles: ByColor[Option[PlayerTitle]] =
@@ -106,8 +106,8 @@ case class Tags(value: List[Tag]) extends AnyVal:
yield minutes * 60 + seconds
s.split(':').toList match
case List(h, m, s) => (h.toIntOption, readMinutesAndSeconds(m, s)).mapN(_ * 3600 + _)
case List(m, s) => readMinutesAndSeconds(m, s)
case _ => None
case List(m, s) => readMinutesAndSeconds(m, s)
case _ => None
seconds.map(Centis.ofSeconds)
override def toString = sorted.value.mkString("\n")
@@ -141,49 +141,49 @@ object Tag:
given Eq[Tag] = Eq.fromUniversalEquals
case object Event extends TagType
case object Site extends TagType
case object Date extends TagType
case object Event extends TagType
case object Site extends TagType
case object Date extends TagType
case object UTCDate extends TagType:
val format = DateTimeFormatter.ofPattern("yyyy.MM.dd")
case object UTCTime extends TagType:
val format = DateTimeFormatter.ofPattern("HH:mm:ss")
case object Round extends TagType
case object Board extends TagType
case object White extends TagType
case object Black extends TagType
case object TimeControl extends TagType
case object WhiteClock extends TagType
case object BlackClock extends TagType
case object ReferenceTime extends TagType
case object WhiteElo extends TagType
case object BlackElo extends TagType
case object WhiteRatingDiff extends TagType
case object BlackRatingDiff extends TagType
case object WhiteTitle extends TagType
case object BlackTitle extends TagType
case object WhiteTeam extends TagType
case object BlackTeam extends TagType
case object WhiteFideId extends TagType
case object BlackFideId extends TagType
case object Result extends TagType
case object FEN extends TagType
case object Variant extends TagType
case object ECO extends TagType
case object Opening extends TagType
case object Termination extends TagType
case object Annotator extends TagType
case object GameId extends TagType
case object Round extends TagType
case object Board extends TagType
case object White extends TagType
case object Black extends TagType
case object TimeControl extends TagType
case object WhiteClock extends TagType
case object BlackClock extends TagType
case object ReferenceTime extends TagType
case object WhiteElo extends TagType
case object BlackElo extends TagType
case object WhiteRatingDiff extends TagType
case object BlackRatingDiff extends TagType
case object WhiteTitle extends TagType
case object BlackTitle extends TagType
case object WhiteTeam extends TagType
case object BlackTeam extends TagType
case object WhiteFideId extends TagType
case object BlackFideId extends TagType
case object Result extends TagType
case object FEN extends TagType
case object Variant extends TagType
case object ECO extends TagType
case object Opening extends TagType
case object Termination extends TagType
case object Annotator extends TagType
case object GameId extends TagType
case class Unknown(n: String) extends TagType:
override def toString = n
override def toString = n
override val isUnknown = true
val names = ByColor(White, Black)
val elos = ByColor(WhiteElo, BlackElo)
val titles = ByColor(WhiteTitle, BlackTitle)
val names = ByColor(White, Black)
val elos = ByColor(WhiteElo, BlackElo)
val titles = ByColor(WhiteTitle, BlackTitle)
val fideIds = ByColor(WhiteFideId, BlackFideId)
val teams = ByColor(WhiteTeam, BlackTeam)
val clocks = ByColor(WhiteClock, BlackClock)
val teams = ByColor(WhiteTeam, BlackTeam)
val clocks = ByColor(WhiteClock, BlackClock)
val tagTypes = List(
Event,
+6 -6
View File
@@ -9,18 +9,18 @@ object FullMoveNumber extends RichOpaqueInt[FullMoveNumber]:
val initial: FullMoveNumber = 1
extension (e: FullMoveNumber)
def ply(turn: Color): Ply = Ply(e * 2 - turn.fold(2, 1))
def next: FullMoveNumber = e + 1
def next: FullMoveNumber = e + 1
opaque type Ply = Int
object Ply extends RelaxedOpaqueInt[Ply]:
val initial: Ply = 0
val initial: Ply = 0
val firstMove: Ply = 1
extension (e: Ply)
inline def turn: Color = Color.fromWhite(e.isEven) // whose turn it is to play now
inline def turn: Color = Color.fromWhite(e.isEven) // whose turn it is to play now
def fullMoveNumber: FullMoveNumber = FullMoveNumber(1 + e / 2)
inline def isEven: Boolean = (e & 1) == 0
inline def isOdd: Boolean = !e.isEven
inline def next: Ply = Ply(e + 1)
inline def isEven: Boolean = (e & 1) == 0
inline def isOdd: Boolean = !e.isEven
inline def next: Ply = Ply(e + 1)
/* The halfmove clock specifies a decimal number of half moves with respect to the 50 move draw rule.
* It is reset to zero after a capture or a pawn move and incremented otherwise. */
+6 -6
View File
@@ -14,12 +14,12 @@ final class Opening(
val (family: OpeningFamily, variation: Option[OpeningVariation]) = name.value.split(":", 2) match
case Array(f, v) => OpeningFamily(OpeningName(f)) -> Some(OpeningVariation(v.takeWhile(',' !=).trim))
case Array(f) => OpeningFamily(OpeningName(f)) -> None
case _ => OpeningFamily(name) -> None
case Array(f) => OpeningFamily(OpeningName(f)) -> None
case _ => OpeningFamily(name) -> None
lazy val nbMoves: Int = uci.value.count(' ' ==) + 1
lazy val nbMoves: Int = uci.value.count(' ' ==) + 1
lazy val lastUci: Option[Uci.Move] = uci.value.split(' ').lastOption.flatMap(Uci.Move.apply)
lazy val key: OpeningKey = Opening.nameToKey(name)
lazy val key: OpeningKey = Opening.nameToKey(name)
override def toString = name.value
@@ -34,8 +34,8 @@ object Opening:
object nameToKey:
private val splitAccentRegex = "[\u0300-\u036f]".r
private val multiSpaceRegex = """\s+""".r
private val badChars = """[^\w\-]+""".r
private val multiSpaceRegex = """\s+""".r
private val badChars = """[^\w\-]+""".r
def apply(name: OpeningName) = OpeningKey:
badChars.replaceAllIn(
multiSpaceRegex.replaceAllIn(
+5 -5
View File
@@ -20,7 +20,7 @@ object OpeningDb:
.foldLeft(Map.empty) { case (acc, op) =>
acc.updatedWith(op.key):
case Some(prev) if prev.uci.value.size < op.uci.value.size => prev.some
case _ => op.some
case _ => op.some
}
def isShortest(op: Opening) = shortestLines.get(op.key).contains(op)
@@ -29,7 +29,7 @@ object OpeningDb:
def findByStandardFen(fen: StandardFen): Option[Opening] = byFen.get(fen)
val SEARCH_MAX_PLIES = 40
val SEARCH_MAX_PLIES = 40
val SEARCH_MIN_PIECES = 20
// assumes standard initial Fen and variant
@@ -46,7 +46,7 @@ object OpeningDb:
.take(SEARCH_MAX_PLIES)
.takeWhile:
case move: Move => move.before.board.nbPieces >= SEARCH_MIN_PIECES
case _ => false
case _ => false
.collect { case move: Move => move }
.toVector
moves.map(_.before) ++ moves.lastOption.map(_.after).toVector
@@ -59,9 +59,9 @@ object OpeningDb:
.drop(1)
.foldRight(none[Opening.AtPly]):
case ((board, ply), None) => byFen.get(format.Fen.writeOpening(board)).map(_.atPly(Ply(ply)))
case (_, found) => found
case (_, found) => found
def searchInFens(fens: Iterable[StandardFen]): Option[Opening] =
fens.foldRight(none[Opening]):
case (fen, None) => findByStandardFen(fen)
case (_, found) => found
case (_, found) => found
+2 -2
View File
@@ -14,11 +14,11 @@ case object Antichess
standardInitialPosition = true
):
override val initialBoard: Board = Board.standard
override val initialBoard: Board = Board.standard
override def initialPieces: Map[Square, Piece] = initialBoard.pieceMap
// In antichess, it is not permitted to castle
override val castles: Castles = Castles.none
override val castles: Castles = Castles.none
override val initialFen: FullFen = FullFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1")
// In antichess, the king can't be put into check so we always return false
+7 -7
View File
@@ -12,7 +12,7 @@ case object Atomic
standardInitialPosition = true
):
override val initialBoard: Board = Board.standard
override val initialBoard: Board = Board.standard
override def initialPieces: Map[Square, Piece] = initialBoard.pieceMap
override def validMoves(position: Position): List[Move] =
@@ -31,7 +31,7 @@ case object Atomic
position.kingsOnlyOf(!position.color)
/** Atomic chess has a special end where a king has been killed by exploding with an adjacent captured piece */
override def specialEnd(position: Position): Boolean = position.kings.count < 2
override def specialEnd(position: Position): Boolean = position.kings.count < 2
override def validMovesAt(position: Position, square: Square): List[Move] =
super.validMovesAt(position, square).view.map(explodeSurroundingPieces).filter(kingSafety).toList
@@ -84,13 +84,13 @@ case object Atomic
// Pawns are immune (for some reason), but all pieces surrounding the captured piece and the capturing piece
// itself explode
val squaresToExplode = (move.dest.kingAttacks & afterBoard.occupied & ~afterBoard.pawns) | move.dest.bl
val afterExplosions = afterBoard.withBoard(afterBoard.board.discard(squaresToExplode))
val afterExplosions = afterBoard.withBoard(afterBoard.board.discard(squaresToExplode))
val rooksToExploded = squaresToExplode & afterBoard.rooks
val castles = afterBoard.castles & ~rooksToExploded
val castles = afterBoard.castles & ~rooksToExploded
val unMovedRooks = afterBoard.unmovedRooks & ~rooksToExploded
val newBoard = afterExplosions.updateHistory(_.copy(castles = castles, unmovedRooks = unMovedRooks))
val newBoard = afterExplosions.updateHistory(_.copy(castles = castles, unmovedRooks = unMovedRooks))
move.copy(afterWithoutHistory = newBoard)
else move
@@ -124,10 +124,10 @@ case object Atomic
InsufficientMatingMaterial.pawnBlockedByPawn(square, position)
|| piece.is(King) || piece.is(Bishop)
)
val randomBishop = position.pieces.find { case (_, piece) => piece.is(Bishop) }
val randomBishop = position.pieces.find { case (_, piece) => piece.is(Bishop) }
val bishopsAbsentOrPawnitized = randomBishop match
case Some((square, piece)) => bishopPawnitized(position.board, piece.color, square.isLight)
case None => true
case None => true
closedStructure && bishopsAbsentOrPawnitized
private def bishopPawnitized(board: Board, sideWithBishop: Color, bishopLight: Boolean) =
+45 -45
View File
@@ -15,7 +15,7 @@ case object Crazyhouse
standardInitialPosition = true
):
override val initialBoard: Board = Board.standard
override val initialBoard: Board = Board.standard
override def initialPieces: Map[Square, Piece] = initialBoard.pieceMap
override val initialFen: FullFen = FullFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR/ w KQkq - 0 1")
@@ -36,7 +36,7 @@ case object Crazyhouse
override def drop(position: Position, role: Role, square: Square): Either[ErrorStr, Drop] =
for
d1 <- position.crazyData.toRight(ErrorStr("Position has no crazyhouse data"))
_ <- Either.cond((role != Pawn || canDropPawnOn(square)), d1, ErrorStr(s"Can't drop $role on $square"))
_ <- Either.cond((role != Pawn || canDropPawnOn(square)), d1, ErrorStr(s"Can't drop $role on $square"))
piece = Piece(position.color, role)
d2 <- d1.drop(piece).toRight(ErrorStr(s"No $piece to drop on $square"))
b1 <- position.board
@@ -62,7 +62,7 @@ case object Crazyhouse
// there is always sufficient mating material in Crazyhouse
override def opponentHasInsufficientMaterial(position: Position): Boolean = false
override def isInsufficientMaterial(position: Position): Boolean = false
override def isInsufficientMaterial(position: Position): Boolean = false
// if the king is not in check, all drops are possible, we just return None
// king is in single check, we return the squares between the king and the checker
@@ -128,7 +128,7 @@ case object Crazyhouse
to <- if role == Pawn then targets & ~Bitboard.firstRank & ~Bitboard.lastRank else targets
piece = Piece(position.color, role)
after <- position.board.put(piece, to)
d2 <- data.drop(piece)
d2 <- data.drop(piece)
yield Drop(piece, to, position, position.withBoard(after).withCrazyData(d2))
type Pockets = ByColor[Pocket]
@@ -157,7 +157,7 @@ case object Crazyhouse
if promoted.contains(orig) then copy(promoted = promoted.move(orig, dest)) else this
def isEmpty = pockets.forall(_.isEmpty)
def size = pockets.reduce(_.size + _.size)
def size = pockets.reduce(_.size + _.size)
object Data:
val init: Data = Data(Pockets.empty, Bitboard.empty)
@@ -175,32 +175,32 @@ case object Crazyhouse
case class Pocket(pawn: Int, knight: Int, bishop: Int, rook: Int, queen: Int):
def forsythUpper: String = forsyth.toUpperCase
def forsyth: String = forsyth(pawn, 'p') + forsyth(knight, 'n') +
def forsyth: String = forsyth(pawn, 'p') + forsyth(knight, 'n') +
forsyth(bishop, 'b') + forsyth(rook, 'r') + forsyth(queen, 'q')
def forsyth(role: Int, char: Char): String = List.fill(role)(char).mkString
def size: Int = pawn + knight + bishop + rook + queen
def isEmpty: Boolean = size == 0
def nonEmpty: Boolean = size > 0
def size: Int = pawn + knight + bishop + rook + queen
def isEmpty: Boolean = size == 0
def nonEmpty: Boolean = size > 0
def hasNonPawn: Boolean = knight + bishop + rook + queen > 0
def contains(r: Role): Boolean = r match
case Pawn => pawn > 0
case Pawn => pawn > 0
case Knight => knight > 0
case Bishop => bishop > 0
case Rook => rook > 0
case Queen => queen > 0
case King => false
case Rook => rook > 0
case Queen => queen > 0
case King => false
def apply(role: Role): Option[Int] =
role match
case Pawn => Some(pawn)
case Pawn => Some(pawn)
case Knight => Some(knight)
case Bishop => Some(bishop)
case Rook => Some(rook)
case Queen => Some(queen)
case King => None
case Rook => Some(rook)
case Queen => Some(queen)
case King => None
def take(role: Role): Option[Pocket] =
update(role, (x => Option.when(x > 0)(x - 1)))
@@ -208,20 +208,20 @@ case object Crazyhouse
def store(role: Role): Pocket = update(role, _ + 1)
def update(role: Role, f: Int => Int): Pocket = role match
case Pawn => copy(pawn = f(pawn))
case Pawn => copy(pawn = f(pawn))
case Knight => copy(knight = f(knight))
case Bishop => copy(bishop = f(bishop))
case Rook => copy(rook = f(rook))
case Queen => copy(queen = f(queen))
case King => this
case Rook => copy(rook = f(rook))
case Queen => copy(queen = f(queen))
case King => this
def update(role: Role, f: Int => Option[Int]): Option[Pocket] = role match
case Pawn => f(pawn).map(x => copy(pawn = x))
case Pawn => f(pawn).map(x => copy(pawn = x))
case Knight => f(knight).map(x => copy(knight = x))
case Bishop => f(bishop).map(x => copy(bishop = x))
case Rook => f(rook).map(x => copy(rook = x))
case Queen => f(queen).map(x => copy(queen = x))
case King => None
case Rook => f(rook).map(x => copy(rook = x))
case Queen => f(queen).map(x => copy(queen = x))
case King => None
def flatMap[B](f: (Role, Int) => IterableOnce[B]): List[B] =
List(f(Pawn, pawn), f(Knight, knight), f(Bishop, bishop), f(Rook, rook), f(Queen, queen)).flatten
@@ -240,44 +240,44 @@ case object Crazyhouse
val empty: Pocket = Pocket(0, 0, 0, 0, 0)
def apply(roles: Seq[Role]): Pocket =
var pawn = 0
var pawn = 0
var knight = 0
var bishop = 0
var rook = 0
var queen = 0
var rook = 0
var queen = 0
roles.foreach:
case Pawn => pawn += 1
case Pawn => pawn += 1
case Knight => knight += 1
case Bishop => bishop += 1
case Rook => rook += 1
case Queen => queen += 1
case King =>
case Rook => rook += 1
case Queen => queen += 1
case King =>
Pocket(pawn, knight, bishop, rook, queen)
object Pockets:
inline def apply(pieces: Seq[Piece]): Pockets =
var whitePawn = 0
var whitePawn = 0
var whiteKnight = 0
var whiteBishop = 0
var whiteRook = 0
var whiteQueen = 0
var blackPawn = 0
var whiteRook = 0
var whiteQueen = 0
var blackPawn = 0
var blackKnight = 0
var blackBishop = 0
var blackRook = 0
var blackQueen = 0
var blackRook = 0
var blackQueen = 0
pieces.foreach:
case Piece(White, Pawn) => whitePawn += 1
case Piece(White, Pawn) => whitePawn += 1
case Piece(White, Knight) => whiteKnight += 1
case Piece(White, Bishop) => whiteBishop += 1
case Piece(White, Rook) => whiteRook += 1
case Piece(White, Queen) => whiteQueen += 1
case Piece(Black, Pawn) => blackPawn += 1
case Piece(White, Rook) => whiteRook += 1
case Piece(White, Queen) => whiteQueen += 1
case Piece(Black, Pawn) => blackPawn += 1
case Piece(Black, Knight) => blackKnight += 1
case Piece(Black, Bishop) => blackBishop += 1
case Piece(Black, Rook) => blackRook += 1
case Piece(Black, Queen) => blackQueen += 1
case Piece(_, King) =>
case Piece(Black, Rook) => blackRook += 1
case Piece(Black, Queen) => blackQueen += 1
case Piece(_, King) =>
ByColor(
Pocket(whitePawn, whiteKnight, whiteBishop, whiteRook, whiteQueen),
Pocket(blackPawn, blackKnight, blackBishop, blackRook, blackQueen)
@@ -12,7 +12,7 @@ case object FromPosition
standardInitialPosition = false
):
override val initialBoard: Board = Board.standard
override val initialBoard: Board = Board.standard
override def initialPieces: Map[Square, Piece] = initialBoard.pieceMap
override def validMoves(position: Position): List[Move] =
+13 -13
View File
@@ -23,8 +23,8 @@ case object Horde
x <- File.all
y <- Rank.all.take(4)
yield (Square(x, y) -> White.pawn)
val frontPawns = List(Square.B5, Square.C5, Square.F5, Square.G5).map { _ -> White.pawn }
val blackPawns = File.all.map { Square(_, Rank.Seventh) -> Black.pawn }
val frontPawns = List(Square.B5, Square.C5, Square.F5, Square.G5).map { _ -> White.pawn }
val blackPawns = File.all.map { Square(_, Rank.Seventh) -> Black.pawn }
val blackPieces = File.all.map { x => Square(x, Rank.Eighth) -> (Black - backRank(x.value)) }
(whitePawnsHorde ++ frontPawns ++ blackPawns ++ blackPieces).toMap
@@ -76,7 +76,7 @@ case object Horde
*/
private def hordeClosedPosition(position: Position): Boolean =
val hordeSquare = position.byColor(White)
val mateInOne = hordeSquare.count == 1 &&
val mateInOne = hordeSquare.count == 1 &&
hordeSquare.singleSquare.exists(pieceThreatened(position.board, Color.black, _))
!mateInOne && {
if position.isWhiteTurn then position.legalMoves.isEmpty
@@ -96,20 +96,20 @@ case object Horde
// Black can always win by capturing the horde
if color.black then false
else
val hordeRole = board.byRoleOf(color)
val horde = hordeRole.map(_.count)
val hordeRole = board.byRoleOf(color)
val horde = hordeRole.map(_.count)
val hordeBishops: SquareColor => Int = color => (hordeRole.bishop & color.bb).count
val hordeBishopColor = if hordeBishops(Light) >= 1 then Light else Dark
val hordeBishopColor = if hordeBishops(Light) >= 1 then Light else Dark
val hordeBishopNum = Math.min(hordeBishops(Light), 2) + Math.min(hordeBishops(Dark), 2)
// Two same color bishops suffice to cover all the light and dark squares
// around the enemy king.
val hordeNum = horde.pawn + horde.knight + horde.rook + horde.queen + hordeBishopNum
val hordeNum = horde.pawn + horde.knight + horde.rook + horde.queen + hordeBishopNum
val piecesRole = board.byRoleOf(Color.black)
val pieces = piecesRole.map(_.count)
val pieces = piecesRole.map(_.count)
val piecesBishops: SquareColor => Int = color => (piecesRole.bishop & color.bb).count
val piecesNum = piecesRole.map(_.count).values.sum
val piecesOfTypeNot = (pieces: Int) => piecesNum - pieces
val piecesNum = piecesRole.map(_.count).values.sum
val piecesOfTypeNot = (pieces: Int) => piecesNum - pieces
if hordeNum == 0 then true
else if hordeNum >= 4 then false // Four or more white pieces can always deliver mate.
// Pawns/queens are never insufficient material when paired with any other
@@ -142,7 +142,7 @@ case object Horde
else if horde.pawn == 1 then
// Promote the pawn to a queen or a knight and check whether white
// can mate.
val pawnSquare = (board.pawns & board.byColor(Color.white)).first.get // we know there is a pawn
val pawnSquare = (board.pawns & board.byColor(Color.white)).first.get // we know there is a pawn
val promoteToQueen = board.putOrReplace(White.queen, pawnSquare)
val promoteToKnight = board.putOrReplace(White.knight, pawnSquare)
hasInsufficientMaterial(promoteToQueen, color) && hasInsufficientMaterial(promoteToKnight, color)
@@ -249,8 +249,8 @@ enum SquareColor:
def bb: Bitboard = this match
case Light => Bitboard.lightSquares
case Dark => Bitboard.darkSquares
case Dark => Bitboard.darkSquares
def unary_! : SquareColor = this match
case Light => Dark
case Dark => Light
case Dark => Light
@@ -12,7 +12,7 @@ case object KingOfTheHill
standardInitialPosition = true
):
override val initialBoard: Board = Board.standard
override val initialBoard: Board = Board.standard
override def initialPieces: Map[Square, Piece] = initialBoard.pieceMap
override def validMoves(position: Position): List[Move] =
@@ -30,4 +30,4 @@ case object KingOfTheHill
/** You only need a king to be able to win in this variant
*/
override def opponentHasInsufficientMaterial(position: Position): Boolean = false
override def isInsufficientMaterial(position: Position): Boolean = false
override def isInsufficientMaterial(position: Position): Boolean = false
@@ -37,7 +37,7 @@ case object RacingKings
)
override val initialBoard: Board = Board.fromMap(initialPieces)
override val castles: Castles = Castles.none
override val castles: Castles = Castles.none
override val allowsCastling: Boolean = false
override val initialFen: FullFen = FullFen("8/8/8/8/8/8/krbnNBRK/qrbnNBRQ w - - 0 1")
@@ -53,7 +53,7 @@ case object RacingKings
override def valid(position: Position, strict: Boolean): Boolean =
super.valid(position, strict) && (!strict || position.check.no)
override def isInsufficientMaterial(position: Position): Boolean = false
override def isInsufficientMaterial(position: Position): Boolean = false
override def opponentHasInsufficientMaterial(position: Position): Boolean = false
// It is a win, when exactly one king made it to the goal. When white reaches
+10 -10
View File
@@ -12,9 +12,9 @@ case object Standard
standardInitialPosition = true
):
override val initialBoard: Board = Board.standard
override val initialBoard: Board = Board.standard
override def initialPieces: Map[Square, Piece] = initialBoard.pieceMap
override val initialPosition: Position = Position(initialBoard, this, White)
override val initialPosition: Position = Position(initialBoard, this, White)
override def valid(position: Position, strict: Boolean): Boolean =
super.valid(position, strict) && (!strict || hasValidCheckers(position))
@@ -23,7 +23,7 @@ case object Standard
import position.{ genNonKing, genSafeKing, genCastling, color, ourKing }
val enPassantMoves = position.genEnPassant(position.us & position.pawns)
ourKing.fold(Nil): king =>
val checkers = position.attackers(king, !position.color)
val checkers = position.attackers(king, !position.color)
val candidates =
if checkers.isEmpty then
val targets = ~position.us
@@ -51,10 +51,10 @@ case object Standard
private def genEvasions(king: Square, position: Position, checkers: Bitboard): List[Move] =
import position.{ genNonKing, genSafeKing, us }
// Checks by these sliding pieces can maybe be blocked.
val sliders = checkers & position.sliders
val attacked = sliders.fold(Bitboard.empty)((a, s) => a | (Bitboard.ray(king, s) ^ s.bl))
val sliders = checkers & position.sliders
val attacked = sliders.fold(Bitboard.empty)((a, s) => a | (Bitboard.ray(king, s) ^ s.bl))
val safeKings = genSafeKing(king, ~us & ~attacked)
val blockers = checkers.singleSquare.fold(Nil)(c => genNonKing(Bitboard.between(king, c) | checkers))
val blockers = checkers.singleSquare.fold(Nil)(c => genNonKing(Bitboard.between(king, c) | checkers))
safeKings ++ blockers
def hasValidCheckers(position: Position): Boolean =
@@ -66,8 +66,8 @@ case object Standard
private def isValidCheckersForEnPassant(position: Position, activeCheckers: Bitboard): Boolean =
(for
enPassantSquare <- position.potentialEpSquare
enPassantUp <- enPassantSquare.prevRank(position.color)
enPassantDown <- enPassantSquare.nextRank(position.color)
enPassantUp <- enPassantSquare.prevRank(position.color)
enPassantDown <- enPassantSquare.nextRank(position.color)
yield activeCheckers.count == 1 && (
activeCheckers.first.contains(enPassantSquare) || position.board
.move(enPassantUp, enPassantDown)
@@ -81,7 +81,7 @@ case object Standard
else
(for
firstChecker <- activeCheckers.first
lastChecker <- activeCheckers.last
ourKing <- position.ourKing
lastChecker <- activeCheckers.last
ourKing <- position.ourKing
yield !Bitboard.aligned(firstChecker, lastChecker, ourKing))
.getOrElse(false)
+1 -1
View File
@@ -14,7 +14,7 @@ case object ThreeCheck
standardInitialPosition = true
):
override val initialBoard: Board = Board.standard
override val initialBoard: Board = Board.standard
override def initialPieces: Map[Square, Piece] = initialBoard.pieceMap
override val initialFen: FullFen = FullFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1 +0+0")
+22 -22
View File
@@ -22,16 +22,16 @@ abstract class Variant private[variant] (
def initialBoard: Board
def initialPosition: Position = Position(initialBoard, this, White)
inline def standard: Boolean = this == Standard
inline def chess960: Boolean = this == Chess960
inline def fromPosition: Boolean = this == FromPosition
inline def standard: Boolean = this == Standard
inline def chess960: Boolean = this == Chess960
inline def fromPosition: Boolean = this == FromPosition
inline def kingOfTheHill: Boolean = this == KingOfTheHill
inline def threeCheck: Boolean = this == ThreeCheck
inline def antichess: Boolean = this == Antichess
inline def atomic: Boolean = this == Atomic
inline def horde: Boolean = this == Horde
inline def racingKings: Boolean = this == RacingKings
inline def crazyhouse: Boolean = this == Crazyhouse
inline def threeCheck: Boolean = this == ThreeCheck
inline def antichess: Boolean = this == Antichess
inline def atomic: Boolean = this == Atomic
inline def horde: Boolean = this == Horde
inline def racingKings: Boolean = this == RacingKings
inline def crazyhouse: Boolean = this == Crazyhouse
inline def exotic: Boolean = !standard
@@ -49,9 +49,9 @@ abstract class Variant private[variant] (
def isValidPromotion(promotion: Option[PromotableRole]): Boolean =
promotion match
case None => true
case None => true
case Some(Queen | Rook | Knight | Bishop) => true
case _ => false
case _ => false
def validMoves(position: Position): List[Move]
@@ -61,14 +61,14 @@ abstract class Variant private[variant] (
if piece.color != position.color then Nil
else
val targets = ~us
val bb = square.bb
val bb = square.bb
piece.role match
case Pawn => position.genEnPassant(us & bb) ++ position.genPawn(bb, targets)
case Pawn => position.genEnPassant(us & bb) ++ position.genPawn(bb, targets)
case Knight => position.genKnight(us & bb, targets)
case Bishop => position.genBishop(us & bb, targets)
case Rook => position.genRook(us & bb, targets)
case Queen => position.genQueen(us & bb, targets)
case King => position.genKingAt(targets, square)
case Rook => position.genRook(us & bb, targets)
case Queen => position.genQueen(us & bb, targets)
case King => position.genKingAt(targets, square)
}
def pieceThreatened(board: Board, by: Color, to: Square): Boolean =
@@ -212,7 +212,7 @@ object Variant:
Horde,
RacingKings
)
val byId = all.mapBy(_.id)
val byId = all.mapBy(_.id)
val byKey = all.mapBy(_.key)
val openingSensibleVariants: Set[Variant] = Set(
@@ -231,12 +231,12 @@ object Variant:
inline def default: Variant = Standard
inline def apply(inline id: Id): Option[Variant] = list.byId.get(id)
inline def apply(inline id: Id): Option[Variant] = list.byId.get(id)
inline def apply(inline key: LilaKey): Option[Variant] = list.byKey.get(key)
def orDefault(id: Id): Variant = apply(id) | default
def orDefault(key: LilaKey): Variant = apply(key) | default
def idOrDefault(id: Option[Id]): Variant = id.flatMap(apply(_)) | default
def orDefault(key: Option[LilaKey]): Variant = key.flatMap(apply(_)) | default
def orDefault(id: Id): Variant = apply(id) | default
def orDefault(key: LilaKey): Variant = apply(key) | default
def idOrDefault(id: Option[Id]): Variant = id.flatMap(apply(_)) | default
def orDefault(key: Option[LilaKey]): Variant = key.flatMap(apply(_)) | default
def byName(name: String): Option[Variant] =
list.all.find(_.name.toLowerCase == name.toLowerCase)
+8 -8
View File
@@ -13,7 +13,7 @@ object Json:
given Writes[chess.Color] = LibJson.writeAs(_.name)
given Reads[Uci] = LibJson.optRead(Uci.apply)
given Reads[Uci] = LibJson.optRead(Uci.apply)
given Writes[Uci] = LibJson.writeAs(_.uci)
given OWrites[Crazyhouse.Pocket] = OWrites: p =>
@@ -32,7 +32,7 @@ object Json:
given Writes[Opening] with
def writes(o: Opening) = PlayJson.obj("eco" -> o.eco, "name" -> o.name)
given Writes[Glyph] = PlayJson.writes[Glyph]
given Writes[Glyph] = PlayJson.writes[Glyph]
given Writes[Glyphs] = Writes[Glyphs]: gs =>
PlayJson.toJson(gs.toList)
@@ -49,20 +49,20 @@ object Json:
given OWrites[CorrespondenceClock] = OWrites: c =>
PlayJson.obj(
"daysPerTurn" -> c.daysPerTurn,
"increment" -> c.increment,
"white" -> c.whiteTime,
"black" -> c.blackTime
"increment" -> c.increment,
"white" -> c.whiteTime,
"black" -> c.blackTime
)
given OWrites[chess.opening.Opening.AtPly] = OWrites: o =>
PlayJson.obj(
"eco" -> o.opening.eco,
"eco" -> o.opening.eco,
"name" -> o.opening.name,
"ply" -> o.ply
"ply" -> o.ply
)
def destString(dests: Map[Square, Bitboard]): String =
val sb = new java.lang.StringBuilder(80)
val sb = new java.lang.StringBuilder(80)
var first = true
dests.foreach: (orig, dests) =>
if first then first = false
+116 -116
View File
@@ -24,7 +24,7 @@ object Elo extends RichOpaqueInt[Elo]:
val prd = playersRatingDiff(player.rating, game.opponentRating)
getExpectedScore(prd)
val achievedScore = games.foldMap(_.points.value)
val ratingDiff =
val ratingDiff =
Math.round(player.kFactor * (achievedScore - expectedScore))
player.rating + ratingDiff
@@ -42,7 +42,7 @@ object Elo extends RichOpaqueInt[Elo]:
def computePerformanceRating(games: Seq[Game]): Option[Elo] =
games.nonEmpty.option:
val averageOpponentRating = games.map(_.opponentRating).sum / games.size
val percentageScore = Math.round(games.map(_.points.value).sum * 100 / games.size)
val percentageScore = Math.round(games.map(_.points.value).sum * 100 / games.size)
averageOpponentRating + performanceRatingTableFIDE.getOrElse(percentageScore, 0)
final class Player(val rating: Elo, val kFactor: KFactor)
@@ -51,20 +51,20 @@ object Elo extends RichOpaqueInt[Elo]:
// 8.1.2 FIDE table
// We use the full table for perfect tournament performance rating (PTP) calculations (no +- 400 limit)
val conversionTableFIDE: Map[Int, Float] = List(
3 -> 0.50f,
10 -> 0.51f,
17 -> 0.52f,
25 -> 0.53f,
32 -> 0.54f,
39 -> 0.55f,
46 -> 0.56f,
53 -> 0.57f,
61 -> 0.58f,
68 -> 0.59f,
76 -> 0.60f,
83 -> 0.61f,
91 -> 0.62f,
98 -> 0.63f,
3 -> 0.50f,
10 -> 0.51f,
17 -> 0.52f,
25 -> 0.53f,
32 -> 0.54f,
39 -> 0.55f,
46 -> 0.56f,
53 -> 0.57f,
61 -> 0.58f,
68 -> 0.59f,
76 -> 0.60f,
83 -> 0.61f,
91 -> 0.62f,
98 -> 0.63f,
106 -> 0.64f,
113 -> 0.65f,
121 -> 0.66f,
@@ -113,105 +113,105 @@ object Elo extends RichOpaqueInt[Elo]:
// 1.4.9 FIDE table
val performanceRatingTableFIDE: Map[Int, Int] = Map(
0 -> -800,
1 -> -677,
2 -> -589,
3 -> -538,
4 -> -501,
5 -> -470,
6 -> -444,
7 -> -422,
8 -> -401,
9 -> -383,
10 -> -366,
11 -> -351,
12 -> -336,
13 -> -322,
14 -> -309,
15 -> -296,
16 -> -284,
17 -> -273,
18 -> -262,
19 -> -251,
20 -> -240,
21 -> -230,
22 -> -220,
23 -> -211,
24 -> -202,
25 -> -193,
26 -> -184,
27 -> -175,
28 -> -166,
29 -> -158,
30 -> -149,
31 -> -141,
32 -> -133,
33 -> -125,
34 -> -117,
35 -> -110,
36 -> -102,
37 -> -95,
38 -> -87,
39 -> -80,
40 -> -72,
41 -> -65,
42 -> -57,
43 -> -50,
44 -> -43,
45 -> -36,
46 -> -29,
47 -> -21,
48 -> -14,
49 -> -7,
50 -> 0,
51 -> 7,
52 -> 14,
53 -> 21,
54 -> 29,
55 -> 36,
56 -> 43,
57 -> 50,
58 -> 57,
59 -> 65,
60 -> 72,
61 -> 80,
62 -> 87,
63 -> 95,
64 -> 102,
65 -> 110,
66 -> 117,
67 -> 125,
68 -> 133,
69 -> 141,
70 -> 149,
71 -> 158,
72 -> 166,
73 -> 175,
74 -> 184,
75 -> 193,
76 -> 202,
77 -> 211,
78 -> 220,
79 -> 230,
80 -> 240,
81 -> 251,
82 -> 262,
83 -> 273,
84 -> 284,
85 -> 296,
86 -> 309,
87 -> 322,
88 -> 336,
89 -> 351,
90 -> 366,
91 -> 383,
92 -> 401,
93 -> 422,
94 -> 444,
95 -> 470,
96 -> 501,
97 -> 538,
98 -> 589,
99 -> 677,
0 -> -800,
1 -> -677,
2 -> -589,
3 -> -538,
4 -> -501,
5 -> -470,
6 -> -444,
7 -> -422,
8 -> -401,
9 -> -383,
10 -> -366,
11 -> -351,
12 -> -336,
13 -> -322,
14 -> -309,
15 -> -296,
16 -> -284,
17 -> -273,
18 -> -262,
19 -> -251,
20 -> -240,
21 -> -230,
22 -> -220,
23 -> -211,
24 -> -202,
25 -> -193,
26 -> -184,
27 -> -175,
28 -> -166,
29 -> -158,
30 -> -149,
31 -> -141,
32 -> -133,
33 -> -125,
34 -> -117,
35 -> -110,
36 -> -102,
37 -> -95,
38 -> -87,
39 -> -80,
40 -> -72,
41 -> -65,
42 -> -57,
43 -> -50,
44 -> -43,
45 -> -36,
46 -> -29,
47 -> -21,
48 -> -14,
49 -> -7,
50 -> 0,
51 -> 7,
52 -> 14,
53 -> 21,
54 -> 29,
55 -> 36,
56 -> 43,
57 -> 50,
58 -> 57,
59 -> 65,
60 -> 72,
61 -> 80,
62 -> 87,
63 -> 95,
64 -> 102,
65 -> 110,
66 -> 117,
67 -> 125,
68 -> 133,
69 -> 141,
70 -> 149,
71 -> 158,
72 -> 166,
73 -> 175,
74 -> 184,
75 -> 193,
76 -> 202,
77 -> 211,
78 -> 220,
79 -> 230,
80 -> 240,
81 -> 251,
82 -> 262,
83 -> 273,
84 -> 284,
85 -> 296,
86 -> 309,
87 -> 322,
88 -> 336,
89 -> 351,
90 -> 366,
91 -> 383,
92 -> 401,
93 -> 422,
94 -> 444,
95 -> 470,
96 -> 501,
97 -> 538,
98 -> 589,
99 -> 677,
100 -> 800
)
@@ -16,8 +16,8 @@ final class GlickoCalculator(
// Simpler use case: a single game
def computeGame(game: Game, skipDeviationIncrease: Boolean = false): Try[ByColor[Player]] =
val ratings = game.players.map(conversions.toRating)
val gameResult = conversions.toGameResult(ratings, game.outcome)
val ratings = game.players.map(conversions.toRating)
val gameResult = conversions.toGameResult(ratings, game.outcome)
val periodResults = impl.GameRatingPeriodResults(List(gameResult))
Try:
calculator.updateRatings(periodResults, skipDeviationIncrease)
@@ -39,7 +39,7 @@ final class GlickoCalculator(
def toGameResult(ratings: ByColor[Rating], outcome: Outcome): GameResult =
outcome.winner match
case None => GameResult(ratings.white, ratings.black, true)
case None => GameResult(ratings.white, ratings.black, true)
case Some(White) => GameResult(ratings.white, ratings.black, false)
case Some(Black) => GameResult(ratings.black, ratings.white, false)
@@ -12,9 +12,9 @@ final private[glicko] class Rating(
import RatingCalculator.*
// the following variables are used to hold values temporarily whilst running calculations
private[impl] var workingRating: Double = scala.compiletime.uninitialized
private[impl] var workingRating: Double = scala.compiletime.uninitialized
private[impl] var workingRatingDeviation: Double = scala.compiletime.uninitialized
private[impl] var workingVolatility: Double = scala.compiletime.uninitialized
private[impl] var workingVolatility: Double = scala.compiletime.uninitialized
/** Return the average skill value of the player scaled down to the scale used by the algorithm's internal
* workings.
@@ -8,7 +8,7 @@ import java.time.Instant
private object RatingCalculator:
private val MULTIPLIER: Double = 173.7178
val DEFAULT_RATING: Double = 1500.0
val DEFAULT_RATING: Double = 1500.0
def convertRatingToOriginalGlickoScale(rating: Double): Double =
((rating * MULTIPLIER) + DEFAULT_RATING)
@@ -30,8 +30,8 @@ final private[glicko] class RatingCalculator(
import RatingCalculator.*
private val CONVERGENCE_TOLERANCE: Double = 0.000001
private val ITERATION_MAX: Int = 1000
private val DAYS_PER_MILLI: Double = 1.0 / (1000 * 60 * 60 * 24)
private val ITERATION_MAX: Int = 1000
private val DAYS_PER_MILLI: Double = 1.0 / (1000 * 60 * 60 * 24)
private val ratingPeriodsPerMilli: Double = ratingPeriodsPerDay.value * DAYS_PER_MILLI
@@ -86,12 +86,12 @@ final private[glicko] class RatingCalculator(
* @param elapsedRatingPeriods
*/
private def calculateNewRating(player: Rating, results: List[Result], elapsedRatingPeriods: Double): Unit =
val phi = player.getGlicko2RatingDeviation
val phi = player.getGlicko2RatingDeviation
val sigma = player.volatility
val a = Math.log(Math.pow(sigma, 2))
val a = Math.log(Math.pow(sigma, 2))
val delta = deltaOf(player, results)
val v = vOf(player, results)
val tau = this.tau.value
val v = vOf(player, results)
val tau = this.tau.value
// step 5.2 - set the initial values of the iterative algorithm to come in step 5.4
var A: Double = a
@@ -114,7 +114,7 @@ final private[glicko] class RatingCalculator(
while Math.abs(B - A) > CONVERGENCE_TOLERANCE && iterations < ITERATION_MAX do
iterations = iterations + 1
// println(String.format("%f - %f (%f) > %f", B, A, Math.abs(B - A), CONVERGENCE_TOLERANCE))
val C = A + (((A - B) * fA) / (fB - fA))
val C = A + (((A - B) * fA) / (fB - fA))
val fC = f(C, delta, phi, v, a, tau)
if fC * fB <= 0 then
@@ -23,7 +23,7 @@ final private[glicko] class FloatingResult(player: Rating, opponent: Rating, sco
def players = List(player, opponent)
final private[glicko] class GameResult(winner: Rating, loser: Rating, isDraw: Boolean) extends Result:
private val POINTS_FOR_WIN = 1.0d
private val POINTS_FOR_WIN = 1.0d
private val POINTS_FOR_LOSS = 0.0d
private val POINTS_FOR_DRAW = 0.5d
@@ -54,7 +54,7 @@ final private[glicko] class GameResult(winner: Rating, loser: Rating, isDraw: Bo
private[glicko] trait RatingPeriodResults[R <: Result]():
val results: List[R]
def getResults(player: Rating): List[R] = results.filter(_.participated(player))
def getParticipants: Set[Rating] = results.flatMap(_.players).toSet
def getParticipants: Set[Rating] = results.flatMap(_.players).toSet
final private[glicko] class GameRatingPeriodResults(val results: List[GameResult])
extends RatingPeriodResults[GameResult]
+6 -6
View File
@@ -12,12 +12,12 @@ case class Glicko(
volatility: Double
):
def intRating: IntRating = IntRating(rating.toInt)
def intDeviation = deviation.toInt
def provisional = RatingProvisional(deviation >= provisionalDeviation)
def established = provisional.no
def intDeviation = deviation.toInt
def provisional = RatingProvisional(deviation >= provisionalDeviation)
def established = provisional.no
def establishedIntRating = Option.when(established)(intRating)
def clueless = deviation >= cluelessDeviation
def display = s"$intRating${if provisional.yes then "?" else ""}"
def clueless = deviation >= cluelessDeviation
def display = s"$intRating${if provisional.yes then "?" else ""}"
def average(other: Glicko, weight: Float = 0.5f): Glicko =
if weight >= 1 then other
else if weight <= 0 then this
@@ -30,7 +30,7 @@ case class Glicko(
override def toString = f"$intRating/$intDeviation/${volatility}%.3f"
val provisionalDeviation = 110
val cluelessDeviation = 230
val cluelessDeviation = 230
case class Player(
glicko: Glicko,
+1 -1
View File
@@ -8,7 +8,7 @@ object IntRatingDiff extends RichOpaqueInt[IntRatingDiff]:
extension (diff: IntRatingDiff)
def positive: Boolean = diff > 0
def negative: Boolean = diff < 0
def zero: Boolean = diff == 0
def zero: Boolean = diff == 0
given Zero[IntRatingDiff] = Zero(0)
opaque type Rating = Double
@@ -20,7 +20,7 @@ object ChessTreeArbitraries:
if seed.end then Gen.const(LazyList(seed))
else
for
board <- Gen.oneOf(seed.legalMoves.map(_.after))
board <- Gen.oneOf(seed.legalMoves.map(_.after))
boards <- genBoards(board)
yield board #:: boards
@@ -43,10 +43,10 @@ object ChessTreeArbitraries:
else
val nextSeeds = seed.legalMoves
for
value <- Gen.oneOf(nextSeeds)
withMove <- value.next(none)
value <- Gen.oneOf(nextSeeds)
withMove <- value.next(none)
variations <- nextSeeds.filter(_ != value).traverse(_.next(none))
node <- genNode(withMove, variations)
node <- genNode(withMove, variations)
yield node.some
def genNodeWithPath[A](seed: Position)(using FromMove[A]): Gen[(Option[Node[WithMove[A]]], List[A])] =
@@ -54,17 +54,17 @@ object ChessTreeArbitraries:
else
val nextSeeds = seed.legalMoves
for
value <- Gen.oneOf(nextSeeds)
withMove <- value.next(none)
value <- Gen.oneOf(nextSeeds)
withMove <- value.next(none)
variations <- nextSeeds.filter(_ != value).traverse(_.next(none))
node <- genNode(withMove, variations)
path <- NodeArbitraries.genPath(node).map(_.map(_.data))
node <- genNode(withMove, variations)
path <- NodeArbitraries.genPath(node).map(_.map(_.data))
yield (node.some, path)
def genComments(size: Int) =
for
commentSize <- Gen.choose(0, size)
xs <- Gen.listOfN(commentSize, Gen.alphaStr)
xs <- Gen.listOfN(commentSize, Gen.alphaStr)
comments = xs.collect { case s if s.nonEmpty => Comment(s) }
yield comments
@@ -79,7 +79,7 @@ object ChessTreeArbitraries:
def next: Gen[List[WithMove[A]]] =
for
variations <- pickSome(move.move.after.legalMoves)
nextMoves <- variations.traverse(_.next(move.data.some))
nextMoves <- variations.traverse(_.next(move.data.some))
yield nextMoves
given FromMove[PgnMove] with
@@ -87,8 +87,8 @@ object ChessTreeArbitraries:
def next(m: Option[PgnMove]): Gen[WithMove[PgnMove]] =
for
comments <- genComments(5)
glyphs <- Gen.someOf(Glyphs.all).map(xs => Glyphs.fromList(xs.toList))
clock <- Gen.posNum[Int]
glyphs <- Gen.someOf(Glyphs.all).map(xs => Glyphs.fromList(xs.toList))
clock <- Gen.posNum[Int]
yield WithMove(move, PgnMove(move.toSanStr, comments, glyphs, timeLeft = Seconds(clock).some))
def genNode[A: Generator](value: A, variations: List[A] = Nil): Gen[Node[A]] =
@@ -6,32 +6,32 @@ import chess.variant.{ Crazyhouse, Variant }
import org.scalacheck.{ Arbitrary, Cogen, Gen }
object CoreArbitraries:
given Arbitrary[Color] = Arbitrary(Gen.oneOf(Color.all))
given Arbitrary[Side] = Arbitrary(Gen.oneOf(Side.all))
given Arbitrary[Role] = Arbitrary(Gen.oneOf(Role.all))
given Arbitrary[File] = Arbitrary(Gen.oneOf(File.all))
given Arbitrary[Rank] = Arbitrary(Gen.oneOf(Rank.all))
given Arbitrary[Square] = Arbitrary(Gen.oneOf(Square.all))
given Arbitrary[Color] = Arbitrary(Gen.oneOf(Color.all))
given Arbitrary[Side] = Arbitrary(Gen.oneOf(Side.all))
given Arbitrary[Role] = Arbitrary(Gen.oneOf(Role.all))
given Arbitrary[File] = Arbitrary(Gen.oneOf(File.all))
given Arbitrary[Rank] = Arbitrary(Gen.oneOf(Rank.all))
given Arbitrary[Square] = Arbitrary(Gen.oneOf(Square.all))
given Arbitrary[Variant] = Arbitrary(Gen.oneOf(Variant.list.all))
given Arbitrary[Glyph] = Arbitrary(Gen.oneOf(Glyphs.all))
given Arbitrary[Glyphs] = Arbitrary:
given Arbitrary[Glyph] = Arbitrary(Gen.oneOf(Glyphs.all))
given Arbitrary[Glyphs] = Arbitrary:
Gen.listOf(Arbitrary.arbitrary[Glyph]).map(Glyphs.fromList)
given Arbitrary[Centis] = Arbitrary(Gen.posNum[Int].map(Centis(_)))
given Arbitrary[Piece] = Arbitrary:
for
color <- Arbitrary.arbitrary[Color]
role <- Arbitrary.arbitrary[Role]
role <- Arbitrary.arbitrary[Role]
yield Piece(color, role)
given Arbitrary[Castles] = Arbitrary(castlesGen)
given Arbitrary[Castles] = Arbitrary(castlesGen)
given Arbitrary[Bitboard] = Arbitrary(Gen.long.map(Bitboard(_)))
given Arbitrary[PromotableRole] = Arbitrary(Gen.oneOf(Rook, Knight, Bishop, Queen))
given Arbitrary[Uci] = Arbitrary(Gen.oneOf(normalUciMoveGen, promotionUciMoveGen, dropUciMoveGen))
given Cogen[Color] = Cogen.cogenBoolean.contramap(_.white)
given Cogen[Color] = Cogen.cogenBoolean.contramap(_.white)
given Cogen[Square] = Cogen(_.value.toLong)
given Cogen[Centis] = Cogen(_.value.toLong)
@@ -43,16 +43,16 @@ object CoreArbitraries:
given Arbitrary[Crazyhouse.Pocket] = Arbitrary:
for
pawn <- Gen.oneOf(0 to 8)
pawn <- Gen.oneOf(0 to 8)
knight <- Gen.oneOf(0 to 2)
bishop <- Gen.oneOf(0 to 2)
rook <- Gen.oneOf(0 to 2)
queen <- Gen.oneOf(0 to 2)
rook <- Gen.oneOf(0 to 2)
queen <- Gen.oneOf(0 to 2)
yield Crazyhouse.Pocket(pawn, knight, bishop, rook, queen)
given Arbitrary[Crazyhouse.Data] = Arbitrary:
for
pockets <- Arbitrary.arbitrary[ByColor[Crazyhouse.Pocket]]
pockets <- Arbitrary.arbitrary[ByColor[Crazyhouse.Pocket]]
promoted <- Arbitrary.arbitrary[Bitboard]
yield Crazyhouse.Data(pockets, promoted)
@@ -64,13 +64,13 @@ object CoreArbitraries:
def promotionUciMoveGen =
for
file <- Arbitrary.arbitrary[File]
rank <- Gen.oneOf(Rank.Second, Rank.Seventh)
role <- Gen.oneOf(Role.allPromotable)
file <- Arbitrary.arbitrary[File]
rank <- Gen.oneOf(Rank.Second, Rank.Seventh)
role <- Gen.oneOf(Role.allPromotable)
offset <- Gen.oneOf(-1, 1)
destFile = File(file.value + offset).getOrElse(file)
orig = Square(file, rank)
dest = Square(destFile, UciCharPair.implementation.lastRank(orig))
orig = Square(file, rank)
dest = Square(destFile, UciCharPair.implementation.lastRank(orig))
yield Uci.Move(orig, dest, Some(role))
def dropUciMoveGen =
@@ -79,7 +79,7 @@ object CoreArbitraries:
role <- Gen.oneOf(Pawn, Knight, Bishop, Rook, Queen)
yield Uci.Drop(role, dest)
private val genBool = Gen.prob(0.5)
private val genBool = Gen.prob(0.5)
private val castlesGen =
for
wks <- genBool
@@ -5,15 +5,15 @@ import org.scalacheck.{ Arbitrary, Gen }
object NodeArbitraries:
given [A](using Arbitrary[A]): Arbitrary[Tree[A]] = Arbitrary(Gen.oneOf(genNode, genVariation))
given [A](using Arbitrary[A]): Arbitrary[Node[A]] = Arbitrary(genNode)
given [A](using Arbitrary[A]): Arbitrary[Tree[A]] = Arbitrary(Gen.oneOf(genNode, genVariation))
given [A](using Arbitrary[A]): Arbitrary[Node[A]] = Arbitrary(genNode)
given [A](using Arbitrary[A]): Arbitrary[Variation[A]] = Arbitrary(genVariation)
type NodeWithPath[A] = (Node[A], List[A])
given [A](using Arbitrary[A]): Arbitrary[NodeWithPath[A]] = Arbitrary(genNodeWithPath)
given treeEq[A]: Eq[Tree[A]] = Eq.fromUniversalEquals
given nodeEq[A]: Eq[Node[A]] = Eq.fromUniversalEquals
given treeEq[A]: Eq[Tree[A]] = Eq.fromUniversalEquals
given nodeEq[A]: Eq[Node[A]] = Eq.fromUniversalEquals
given variationEq[A]: Eq[Variation[A]] = Eq.fromUniversalEquals
def genNodeWithPath[A](using Arbitrary[A]) =
@@ -27,7 +27,7 @@ object NodeArbitraries:
Gen
.prob(prob)
.flatMap:
case true => node.child.fold(Gen.const(Nil))(genPath(_)).map(node.value :: _)
case true => node.child.fold(Gen.const(Nil))(genPath(_)).map(node.value :: _)
case false =>
if node.variations.isEmpty
then Gen.const(Nil)
@@ -35,7 +35,7 @@ object NodeArbitraries:
Gen
.prob(0.95)
.flatMap:
case true => Gen.oneOf(node.variations).flatMap(v => genPath(v.toNode))
case true => Gen.oneOf(node.variations).flatMap(v => genPath(v.toNode))
case false => Gen.const(node.value :: Nil)
def genNode[A](using Arbitrary[A]): Gen[Node[A]] =
+2 -2
View File
@@ -16,11 +16,11 @@ object macros:
object PgnLiteral extends Literally[ParsedPgn]:
def validate(s: String)(using Quotes) =
Parser.full(PgnStr(s)) match
case Right(_) => Right('{ Parser.full(PgnStr(${ Expr(s) })).toOption.get })
case Right(_) => Right('{ Parser.full(PgnStr(${ Expr(s) })).toOption.get })
case Left(err) => Left(err.toString)
object UciLiteral extends Literally[Uci]:
def validate(s: String)(using Quotes) =
Uci(s) match
case Some(_) => Right('{ Uci(${ Expr(s) }).get })
case _ => Left(s"Invalid UCI: $s")
case _ => Left(s"Invalid UCI: $s")
@@ -52,14 +52,14 @@ g4 {[%emt 0.200]} 34. Rxg4 {[%emt 0.172]} 0-1"""
test("Allow an opening move for white taking into account a player may move without taking if possible"):
val startingPosition = Game(Antichess)
val newGame = startingPosition.playMove(Square.E2, Square.E4, None).get
val fen = Fen.write(newGame)
val newGame = startingPosition.playMove(Square.E2, Square.E4, None).get
val fen = Fen.write(newGame)
assertEquals(fen, FullFen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b - - 0 1"))
test("Not allow a player to make a non capturing move if a capturing move is available"):
val game = Game(Antichess)
val game = Game(Antichess)
val gameAfterOpening = game.playMoves((Square.E2, Square.E4), (Square.F7, Square.F5))
val invalidGame = gameAfterOpening.flatMap(_.playMove(Square.H2, Square.H4))
val invalidGame = gameAfterOpening.flatMap(_.playMove(Square.H2, Square.H4))
assertEquals(invalidGame, Left(ErrorStr("Piece on h2 cannot move to h4")))
test("A board in antichess should only present the capturing moves if the player can capture"):
@@ -120,18 +120,18 @@ g4 {[%emt 0.200]} 34. Rxg4 {[%emt 0.172]} 0-1"""
test("Allow a pawn to be promoted to a king"):
val position = FullFen("8/5P2/8/2b5/8/8/4B3/8 w - -")
val game = fenToGame(position, Antichess)
val newGame = game(Square.F7, Square.F8, Option(King)).get._1
val game = fenToGame(position, Antichess)
val newGame = game(Square.F7, Square.F8, Option(King)).get._1
assertEquals(newGame.position.pieceAt(Square.F8), Option(White - King))
test("deal with 2 white kings"):
val position = FullFen("K3k1nr/p2q2pp/p2p1p2/8/2PP4/8/PP4PP/RNBQK1NR w - - 0 11")
val game = fenToGame(position, Antichess)
val game = fenToGame(position, Antichess)
assertEquals(game.position.destinations, Map(Square.A8 -> Square.A7.bb))
test("Be drawn when there are only opposite colour bishops remaining"):
val position = FullFen("8/2b5/8/8/8/6Q1/4B3/8 b - -")
val game = fenToGame(position, Antichess)(Square.C7, Square.G3, None).get._1
val game = fenToGame(position, Antichess)(Square.C7, Square.G3, None).get._1
assert(game.position.end)
assert(game.position.autoDraw)
assertEquals(game.position.winner, None)
@@ -139,7 +139,7 @@ g4 {[%emt 0.200]} 34. Rxg4 {[%emt 0.172]} 0-1"""
test("Be drawn on multiple bishops on the opposite color"):
val position = FullFen("8/6P1/8/8/1b6/8/8/5B2 w - -")
val game = fenToGame(position, Antichess)(Square.G7, Square.G8, Bishop.some).get._1
val game = fenToGame(position, Antichess)(Square.G7, Square.G8, Bishop.some).get._1
assert(game.position.end)
assert(game.position.autoDraw)
assertEquals(game.position.winner, None)
@@ -147,7 +147,7 @@ g4 {[%emt 0.200]} 34. Rxg4 {[%emt 0.172]} 0-1"""
test("Not be drawn when the black and white bishops are on the same coloured squares "):
val position = FullFen("7b/8/1p6/8/8/8/5B2/8 w - -")
val game = fenToGame(position, Antichess)(Square.F2, Square.B6, None).get._1
val game = fenToGame(position, Antichess)(Square.F2, Square.B6, None).get._1
assertNot(game.position.end)
assertNot(game.position.autoDraw)
assertEquals(game.position.winner, None)
@@ -156,69 +156,69 @@ g4 {[%emt 0.200]} 34. Rxg4 {[%emt 0.172]} 0-1"""
"Be drawn when there are only opposite colour bishops and pawns which could not attack those bishops remaining"
):
val position = FullFen("8/6p1/4B1P1/4p3/4P3/8/2p5/8 b - - 1 28")
val game = fenToGame(position, Antichess)(Square.C2, Square.C1, Option(Bishop)).get._1
val game = fenToGame(position, Antichess)(Square.C2, Square.C1, Option(Bishop)).get._1
assert(game.position.end)
assert(game.position.autoDraw)
assertEquals(game.position.status, Some(Status.Draw))
test("Not be drawn on opposite color bishops but with pawns that could be forced to attack a bishop"):
val position = FullFen("8/6p1/1B4P1/4p3/4P3/8/3p4/8 b - -")
val game = fenToGame(position, Antichess)(Square.D2, Square.D1, Option(Bishop)).get._1
val game = fenToGame(position, Antichess)(Square.D2, Square.D1, Option(Bishop)).get._1
assertNot(game.position.end)
assertNot(game.position.autoDraw)
assertEquals(game.position.winner, None)
test("Not be drawn where a white bishop can attack a black pawn in an almost closed position"):
val position = FullFen("5b2/1P4p1/4B1P1/4p3/4P3/8/8/8 w - -")
val game = fenToGame(position, Antichess)(Square.B7, Square.B8, Bishop.some).get._1
val game = fenToGame(position, Antichess)(Square.B7, Square.B8, Bishop.some).get._1
assertNot(game.position.end)
assertNot(game.position.autoDraw)
assertEquals(game.position.winner, None)
test("Not be drawn where a pawn is unattackable, but is blocked by a bishop, not a pawn"):
val position = FullFen("8/8/4BbP1/4p3/4P3/8/8/8 b - -")
val game = fenToGame(position, Antichess).playMoves(Square.F6 -> Square.G7).get
val game = fenToGame(position, Antichess).playMoves(Square.F6 -> Square.G7).get
assertNot(game.position.end)
assertNot(game.position.autoDraw)
assertEquals(game.position.status, None)
test("Opponent has insufficient material when there are only two remaining knights on same color squares"):
val position = FullFen("8/8/3n2N1/8/8/8/8/8 w - -")
val game = fenToGame(position, Antichess).playMoves(Square.G6 -> Square.F4).get
val game = fenToGame(position, Antichess).playMoves(Square.G6 -> Square.F4).get
assert(game.position.opponentHasInsufficientMaterial)
test(
"Opponent has sufficient material when there are only two remaining knights on opposite color squares"
):
val position = FullFen("7n/8/8/8/8/8/8/N7 w - -")
val game = fenToGame(position, Antichess).playMoves(Square.A1 -> Square.B3).get
val game = fenToGame(position, Antichess).playMoves(Square.A1 -> Square.B3).get
assertNot(game.position.opponentHasInsufficientMaterial)
test(
"Player has insufficient material when there are only two remaining knights on opposite color squares"
):
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
val game = fenToGame(position, Antichess).playMoves(Square.E3 -> Square.D1).get
assertEquals(game.position.playerHasInsufficientMaterial, Some(true))
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
val game = fenToGame(position, Antichess).playMoves(Square.H1 -> Square.G3).get
assertEquals(game.position.playerHasInsufficientMaterial, Some(false))
test("Not be drawn on insufficient mating material"):
val position = FullFen("4K3/8/1b6/8/8/8/5B2/3k4 b - -")
val game = fenToGame(position, Antichess)
val game = fenToGame(position, Antichess)
assertNot(game.position.end)
test("Be drawn on a three move repetition"):
val game = Game(Antichess)
val game = Game(Antichess)
val moves =
List((Square.G1, Square.F3), (Square.G8, Square.F6), (Square.F3, Square.G1), (Square.F6, Square.G8))
val repeatedMoves: List[(Square, Square)] = List.fill(3)(moves).flatten
val g = game.playMoveList(repeatedMoves).get
val g = game.playMoveList(repeatedMoves).get
assert(g.position.threefoldRepetition)
test("Successfully play through a full game until one player loses all their pieces"):
@@ -233,12 +233,12 @@ g4 {[%emt 0.200]} 34. Rxg4 {[%emt 0.172]} 0-1"""
test("Win on a traditional stalemate where the player has no valid moves"):
val position = FullFen("8/p7/8/P7/8/8/8/8 w - -")
val game = fenToGame(position, Antichess).playMoves(Square.A5 -> Square.A6).get
val game = fenToGame(position, Antichess).playMoves(Square.A5 -> Square.A6).get
assert(game.position.end)
assertEquals(game.position.winner, Some(Black))
test("Stalemate is a win - second test"):
val fen = FullFen("2Q5/8/p7/8/8/8/6PR/8 w - -")
val fen = FullFen("2Q5/8/p7/8/8/8/6PR/8 w - -")
val game = fenToGame(fen, Antichess).playMoves(Square.C8 -> Square.A6).get
assert(game.position.end)
assertEquals(game.position.status, Some(Status.VariantEnd))
+30 -30
View File
@@ -8,10 +8,10 @@ import chess.variant.Atomic
class AtomicVariantTest extends ChessTest:
test("Must explode surrounding non pawn pieces on capture"):
val fenPosition = FullFen("rnbqkbnr/1ppppp1p/p5p1/8/8/1P6/PBPPPPPP/RN1QKBNR w KQkq -")
val game = fenToGame(fenPosition, Atomic)
val fenPosition = FullFen("rnbqkbnr/1ppppp1p/p5p1/8/8/1P6/PBPPPPPP/RN1QKBNR w KQkq -")
val game = fenToGame(fenPosition, Atomic)
val explodedSquares = List(Square.H8, Square.G8)
val intactPawns = List(Square.F7, Square.G6, Square.H7)
val intactPawns = List(Square.F7, Square.G6, Square.H7)
game
.playMoves((Square.B2, Square.H8))
@@ -20,8 +20,8 @@ class AtomicVariantTest extends ChessTest:
assert(intactPawns.forall(square => game.position.pieceAt(square).isDefined))
test("Must explode all surrounding non pawn pieces on capture (contrived board)"):
val fenPosition = FullFen("k7/3bbn2/3rqn2/3qr3/8/7B/8/1K6 w - -")
val game = fenToGame(fenPosition, Atomic)
val fenPosition = FullFen("k7/3bbn2/3rqn2/3qr3/8/7B/8/1K6 w - -")
val game = fenToGame(fenPosition, Atomic)
val explodedSquares =
List(Square.D5, Square.E5, Square.D6, Square.E6, Square.F6, Square.D7, Square.E7, Square.F7)
@@ -33,8 +33,8 @@ class AtomicVariantTest extends ChessTest:
test(
"Must explode all surrounding non pawn pieces on capture (contrived board with bottom right position)"
):
val fenPosition = FullFen("k7/3bbn2/3rqn2/4rq2/8/1B6/8/K7 w - -")
val game = fenToGame(fenPosition, Atomic)
val fenPosition = FullFen("k7/3bbn2/3rqn2/4rq2/8/1B6/8/K7 w - -")
val game = fenToGame(fenPosition, Atomic)
val explodedSquares =
List(Square.F5, Square.E5, Square.D6, Square.E6, Square.F6, Square.D7, Square.E7, Square.F7)
game
@@ -44,12 +44,12 @@ class AtomicVariantTest extends ChessTest:
test("Not allow a king to capture a piece"):
val fenPosition = FullFen("8/8/8/1k6/8/8/8/1Kr5 w - -")
val game = fenToGame(fenPosition, Atomic)
val game = fenToGame(fenPosition, Atomic)
assertEquals(game.playMoves((Square.B1, Square.C1)), Left(ErrorStr("Piece on b1 cannot move to c1")))
test("The game must end with the correct winner when a king explodes in the perimeter of a captured piece"):
val fenPosition = FullFen("rnb1kbnr/ppp1pppp/8/3q4/8/7P/PPPP1PP1/RNBQKBNR b KQkq -")
val game = fenToGame(fenPosition, Atomic)
val game = fenToGame(fenPosition, Atomic)
game
.playMoves((Square.D5, Square.D2))
.assertRight: g =>
@@ -59,7 +59,7 @@ class AtomicVariantTest extends ChessTest:
test("The game must end by a traditional checkmate (atomic mate)"):
val fenPosition = FullFen("1k6/8/8/8/8/8/PP5r/K7 b - -")
val game = fenToGame(fenPosition, Atomic)
val game = fenToGame(fenPosition, Atomic)
game
.playMoves((Square.H2, Square.H1))
.assertRight: g =>
@@ -69,7 +69,7 @@ class AtomicVariantTest extends ChessTest:
test("Must be a stalemate if a king could usually take a piece, but can't because it would explode"):
val positionFen = FullFen("k7/8/1R6/8/8/8/8/5K2 w - -")
val game = fenToGame(positionFen, Atomic)
val game = fenToGame(positionFen, Atomic)
game
.playMoves((Square.B6, Square.B7))
.assertRight: g =>
@@ -78,7 +78,7 @@ class AtomicVariantTest extends ChessTest:
test("It is stalemate if there are only two kings and two opposite square coloured bishops remaining"):
val positionFen = FullFen("4K3/8/2b5/8/8/8/5B2/3k4 b - -")
val game = fenToGame(positionFen, Atomic)
val game = fenToGame(positionFen, Atomic)
assert(game.position.end)
assert(game.position.autoDraw)
assertEquals(game.position.winner, None)
@@ -88,7 +88,7 @@ class AtomicVariantTest extends ChessTest:
"In atomic check, an opportunity at exploding the opponent's king takes priority over getting out of check"
):
val positionFen = FullFen("k1K5/pp5R/8/8/3Q4/P7/1P6/2r5 w - -")
val game = fenToGame(positionFen, Atomic)
val game = fenToGame(positionFen, Atomic)
assert(game.position.check.yes)
assertNot(game.position.end)
assertEquals(game.position.winner, None)
@@ -120,7 +120,7 @@ class AtomicVariantTest extends ChessTest:
"In atomic mate, an opportunity at exploding the opponent's king takes priority over getting out of mate"
):
val positionFen = FullFen("k1r5/pp5R/8/8/3Q4/8/PP6/K7 b - -")
val game = fenToGame(positionFen, Atomic)
val game = fenToGame(positionFen, Atomic)
game
.playMoves((Square.C8, Square.C1))
.assertRight: game =>
@@ -133,7 +133,7 @@ class AtomicVariantTest extends ChessTest:
"In atomic chess a king may walk into a square that is in the perimeter of the opponent king since it can't capture"
):
val positionFen = FullFen("3k4/8/3K4/8/8/8/7r/8 w - -")
val game = fenToGame(positionFen, Atomic)
val game = fenToGame(positionFen, Atomic)
game
.playMoves((Square.D6, Square.D7))
.assertRight: game =>
@@ -142,7 +142,7 @@ class AtomicVariantTest extends ChessTest:
test("Draw on knight and king vs king"):
val position = FullFen("8/1n6/8/8/8/8/k7/2K1b2R w - -")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.H1, Square.E1))
.assertRight: game =>
@@ -151,7 +151,7 @@ class AtomicVariantTest extends ChessTest:
test("Draw on bishop and king vs king"):
val position = FullFen("8/1b6/8/8/8/8/k7/2K1n2R w - -")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.H1, Square.E1))
.assertRight: game =>
@@ -160,7 +160,7 @@ class AtomicVariantTest extends ChessTest:
test("Draw on a rook and king vs king"):
val position = FullFen("8/8/8/8/8/8/N4r2/5k1K b - -")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.F2, Square.A2))
.assertRight: game =>
@@ -169,7 +169,7 @@ class AtomicVariantTest extends ChessTest:
test("Draw on a king vs a king"):
val position = FullFen("6r1/8/8/1k6/8/8/2K5/6R1 w - -")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.G1, Square.G8))
.assertRight: game =>
@@ -178,14 +178,14 @@ class AtomicVariantTest extends ChessTest:
test("It should not be possible to capture a piece resulting in your own king exploding"):
val position = FullFen("rnbqkbnr/pppNp1pp/5p2/3p4/8/8/PPPPPPPP/RNBQKB1R b KQkq - 1 3")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
assertEquals(game.playMoves((Square.D8, Square.D7)), Left(ErrorStr("Piece on d8 cannot move to d7")))
test(
"In an en-passant capture, the pieces surrounding the pawn's destination are exploded along with the pawn"
):
val position = FullFen("4k3/2pppb1p/3r1r2/3P1b2/8/8/1K6/4NB2 b - -")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.E7, Square.E5), (Square.D5, Square.E6))
.assertRight: game =>
@@ -197,17 +197,17 @@ class AtomicVariantTest extends ChessTest:
test("Verify it is not possible to walk into check"):
val position = FullFen("rnbqkbnr/ppp1pppp/8/3pN3/8/8/PPPPPPPP/RNBQKB1R b KQkq - 1 2")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
assertEquals(game.playMoves((Square.E8, Square.D7)), Left(ErrorStr("Piece on e8 cannot move to d7")))
test("Verify that a king can move into what would traditionally be check when touching the opponent king"):
val position = FullFen("r1bq1bnr/pppp1ppp/5k2/4p3/4P1K1/8/PPPP1PPP/RNBQ1B1R b - - 5 6")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
assert(game.playMoves((Square.F6, Square.F5)).isRight)
test("After kings have been touching, and one moves away, a king that was protected is under attack again"):
val position = FullFen("r1bq1bnr/pppp1ppp/5k2/4p3/4P1K1/8/PPPP1PPP/RNBQ1B1R b - - 5 6")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.F6, Square.F5), (Square.G4, Square.H3))
.assertRight: game =>
@@ -215,7 +215,7 @@ class AtomicVariantTest extends ChessTest:
test("Can move into discovered check in order to explode the opponent's king"):
val position = FullFen("R2r2k1/1p2ppbp/8/6p1/2p5/5P1N/P2Pn1PP/2B1K2R b K - 3 19")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.D8, Square.D2))
.assertRight: game =>
@@ -226,7 +226,7 @@ class AtomicVariantTest extends ChessTest:
"It must be possible to remove yourself from check by exploding a piece next to the piece threatening the king"
):
val position = FullFen("5k1r/p1ppq1pp/5p2/1B6/1b3P2/2P5/PP4PP/RNB1K2R w KQ - 0 12")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.B5, Square.D7))
.assertRight: game =>
@@ -236,14 +236,14 @@ class AtomicVariantTest extends ChessTest:
"It should not be possible to explode a piece, exploding a piece next to it which would result in a check"
):
val position = FullFen("r1b1k2r/pp1pBppp/2p1p2n/q3P3/B2P4/2N2Q2/PPn2PPP/R3K1NR w KQkq - 9 11")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
assertEquals(game.playMoves((Square.A4, Square.C2)), Left(ErrorStr("Piece on a4 cannot move to c2")))
test(
"Game is not a draw when the last piece a player has other than their king is a pawn that is blocked by a mobile piece"
):
val position = FullFen("3Q4/2b2k2/5P2/8/8/8/6K1/8 b - - 0 57")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game
.playMoves((Square.C7, Square.D8))
.assertRight: game =>
@@ -254,7 +254,7 @@ class AtomicVariantTest extends ChessTest:
test("There are no repeated moves in the list of available moves for the board"):
// Board where the queen can capture a pawn to both win and remove itself from check
val position = FullFen("k1r5/pp5Q/8/8/8/8/PP6/2K5 w - -")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
game.position.moves
.get(Square.H7)
.assertSome: queenMoves =>
@@ -287,7 +287,7 @@ class AtomicVariantTest extends ChessTest:
test("Identify that a player does not have sufficient material to win when they only have a king"):
val position = FullFen("8/8/8/8/7p/2k4q/2K3P1/8 w - - 19 54")
val game = fenToGame(position, Atomic)
val game = fenToGame(position, Atomic)
assertNot(game.position.end)
game
.playMoves(Square.G2 -> Square.H3)
+2 -2
View File
@@ -378,7 +378,7 @@ K bB""".autoDraw
test("do not detect insufficient material: on two knights"):
val position = FullFen("1n2k1n1/8/8/8/8/8/8/4K3 w - - 0 1")
val game = fenToGame(position, Standard)
val game = fenToGame(position, Standard)
assertNot(game.position.autoDraw)
assertNot(game.position.end)
assertNot(game.position.opponentHasInsufficientMaterial)
@@ -440,7 +440,7 @@ K bB""".autoDraw
assertNot(game.position.opponentHasInsufficientMaterial)
test("do not detect insufficient material: on same-color bishops on both sides"):
val position = FullFen("5K2/8/8/1B6/8/k7/6b1/8 w - - 0 39")
val game = fenToGame(position, Standard)
val game = fenToGame(position, Standard)
assert(game.position.autoDraw)
assert(game.position.end)
assert(game.position.opponentHasInsufficientMaterial)
+14 -14
View File
@@ -13,8 +13,8 @@ import format.Uci
class CanPlayTest extends MunitExtensions with SnapshotAssertions:
test("playPositions from standard prod games"):
val nb = 10
val games = Fixtures.prod500standard
val nb = 10
val games = Fixtures.prod500standard
val result = games
.take(nb)
.map(g => SanStr.from(g.split(' ').toList))
@@ -27,20 +27,20 @@ class CanPlayTest extends MunitExtensions with SnapshotAssertions:
assertFileSnapshot(result.writeFen, "canplay/playPositions_racing_kings.txt")
test("validate from prod games"):
val games = Fixtures.prod500standard
val games = Fixtures.prod500standard
val result = games
.map(g => SanStr.from(g.split(' ').toList))
.traverse_(moves => Standard.initialPosition.validate(moves))
assertEquals(result, ().asRight)
test("validate return left on invalid move"):
val moves = List(uci"e2e4", uci"e7e5", uci"g1f3", uci"b8b6")
val moves = List(uci"e2e4", uci"e7e5", uci"g1f3", uci"b8b6")
val result = Standard.initialPosition.validate(moves)
assert(result.isLeft)
test("forwad from prod games"):
val nb = 10
val games = Fixtures.prod500standard
val nb = 10
val games = Fixtures.prod500standard
val result = games
.take(nb)
.map(g => SanStr.from(g.split(' ').toList))
@@ -49,16 +49,16 @@ class CanPlayTest extends MunitExtensions with SnapshotAssertions:
test("playWhileValid and playWhileValidReverse from prod games"):
val sans = SanStr.from(Fixtures.fromProd2.split(' ').toList)
val x = Standard.initialPosition.playWhileValid(sans, Ply.initial)(_.move.toUci).toOption.get
val y = Standard.initialPosition.playWhileValidReverse(sans, Ply.initial)(_.move.toUci).toOption.get
val x = Standard.initialPosition.playWhileValid(sans, Ply.initial)(_.move.toUci).toOption.get
val y = Standard.initialPosition.playWhileValidReverse(sans, Ply.initial)(_.move.toUci).toOption.get
assertEquals(x.moves, y.moves.reverse)
assertEquals(x.error, y.error)
assertEquals(x.state.board, y.state.board)
test("foldLeft and foldRight has correct plies"):
val sans = SanStr.from(Fixtures.fromProd2.split(' ').toList)
val x = Standard.initialPosition.foldLeft(sans, Ply.initial)(List.empty, (xs, step) => step.ply :: xs)
val y = Standard.initialPosition.foldRight(sans, Ply.initial)(List.empty, (step, xs) => step.ply :: xs)
val x = Standard.initialPosition.foldLeft(sans, Ply.initial)(List.empty, (xs, step) => step.ply :: xs)
val y = Standard.initialPosition.foldRight(sans, Ply.initial)(List.empty, (step, xs) => step.ply :: xs)
assertEquals(y.result(0), Ply.initial.next)
assertEquals(x.result, y.result.reverse)
assertEquals(x.error, None)
@@ -66,10 +66,10 @@ class CanPlayTest extends MunitExtensions with SnapshotAssertions:
test("foldLeft from foreach"):
val sans = SanStr.from(Fixtures.fromProd2.split(' ').toList)
val x =
val x =
Standard.initialPosition.foldLeft(sans, Ply.initial)(List.empty, (xs, step) => xs :+ step.move.toUci)
val builder = scala.collection.mutable.ListBuffer.empty[Uci]
val y = Standard.initialPosition.foreach(sans, Ply.initial)(step => builder += step.move.toUci)
val y = Standard.initialPosition.foreach(sans, Ply.initial)(step => builder += step.move.toUci)
assertEquals(x.result, builder.toList)
assertEquals(x.error, None)
assertEquals(y, None)
@@ -77,12 +77,12 @@ class CanPlayTest extends MunitExtensions with SnapshotAssertions:
/*============== Error Messages ==============*/
test("error message for white"):
val sans = List(SanStr("Nf7"))
val sans = List(SanStr("Nf7"))
val error = Standard.initialPosition.playPositions(sans).swap.toOption.get
assertEquals(error, ErrorStr("Cannot play Nf7 at move 1 by white"))
test("error message for black"):
val sans = List("e4", "e4").map(SanStr(_))
val sans = List("e4", "e4").map(SanStr(_))
val error = Standard.initialPosition.playPositions(sans).swap.toOption.get
assertEquals(error, ErrorStr("Cannot play e4 at move 1 by black"))
@@ -12,7 +12,7 @@ class CastlingKingSideTest extends ChessTest:
val goodHist = """
PPPPPPPP
R QK R"""
val badHist = goodHist.updateHistory(_.withoutCastles(White))
val badHist = goodHist.updateHistory(_.withoutCastles(White))
test("impossible"):
assertEquals(goodHist.place(White.bishop, F1).flatMap(_.destsFrom(E1)), Set())
assertEquals(goodHist.place(White.knight, G1).flatMap(_.destsFrom(E1)), Set(F1))
@@ -13,7 +13,7 @@ class CastlingQueenSideTest extends ChessTest:
val goodHist = """
PPPPPPPP
R KB R"""
val badHist = goodHist.updateHistory(_.withoutCastles(White))
val badHist = goodHist.updateHistory(_.withoutCastles(White))
test("impossible: near queen in the way"):
assertEquals(goodHist.place(White.queen, D1).flatMap(_.destsFrom(E1)), Set())
test("impossible: bishop in the way"):
@@ -39,7 +39,7 @@ PPPPPPPP
R K R""".updateHistory(_ => castleHistory(White, kingSide = true, queenSide = true))
test("impact history: if king castles kingside"):
val game = Game(board)
val g2 = game.playMove(E1, G1).get
val g2 = game.playMove(E1, G1).get
assertGame(
g2,
"""
@@ -58,7 +58,7 @@ R RK """
)
test("impact history: if king castles queenside"):
val game = Game(board)
val g2 = game.playMove(E1, C1).get
val g2 = game.playMove(E1, C1).get
assertGame(
g2,
"""
@@ -77,37 +77,37 @@ PPPPPPPP
)
test("impact history: if king moves to the right"):
val game = Game(board)
val g2 = game.playMove(E1, F1).get.as(White)
val g2 = game.playMove(E1, F1).get.as(White)
assertEquals(g2.position.destsFrom(F1), Set(E1, G1))
val g3 = g2.playMove(F1, E1).get.as(White)
assertEquals(g3.position.destsFrom(E1), Set(D1, F1))
test("impact history: if king moves to the left"):
val game = Game(board)
val g2 = game.playMove(E1, D1).get.as(White)
val g2 = game.playMove(E1, D1).get.as(White)
assertEquals(g2.position.destsFrom(D1), Set(C1, E1))
val g3 = g2.playMove(D1, E1).get.as(White)
assertEquals(g3.position.destsFrom(E1), Set(D1, F1))
test("impact history: if kingside rook moves"):
val game = Game(board)
val g2 = game.playMove(H1, G1).get.as(White)
val g2 = game.playMove(H1, G1).get.as(White)
assertEquals(g2.position.destsFrom(E1), Set(C1, D1, F1, A1))
val g3 = g2.playMove(A1, B1).get
assertEquals(g3.position.destsFrom(E1), Set(D1, F1))
test("impact history: if queenside rook moves"):
val game = Game(board)
val g2 = game.playMove(A1, B1).get.as(White)
val g2 = game.playMove(A1, B1).get.as(White)
assertEquals(g2.position.destsFrom(E1), Set(D1, F1, G1, H1))
val g3 = g2.playMove(H1, G1).get
assertEquals(g3.position.destsFrom(E1), Set(D1, F1))
test("chess960"):
val fenPosition = FullFen("r3k2r/8/8/8/8/8/8/1R2K2R b KQk - 1 1")
val init = fenToGame(fenPosition, Chess960)
val game = init.playMoves((A8, A1), (H1, H2), (A1, A7)).get
val init = fenToGame(fenPosition, Chess960)
val game = init.playMoves((A8, A1), (H1, H2), (A1, A7)).get
assert(game.position.legalMoves.exists(_.castles))
test("test games"):
val fenPosition = FullFen("1rnk1bqr/pppp1bpp/1n2p3/5p2/5P2/1N1N4/PPPPPBPP/1R1K1BQR w KQkq - 0 5")
val init = fenToGame(fenPosition, Chess960)
val init = fenToGame(fenPosition, Chess960)
assert(init.position.legalMoves.exists(_.castles))
assert(init.position.legalMoves.exists(_.castle.exists(_.side == QueenSide)))
+13 -13
View File
@@ -11,17 +11,17 @@ import variant.{ Chess960, Variant, Standard, Crazyhouse }
trait ChessTestCommon:
given Conversion[String, Position] = Visual.<<
given Conversion[String, PgnStr] = PgnStr(_)
given Conversion[PgnStr, String] = _.value
given Conversion[String, PgnStr] = PgnStr(_)
given Conversion[PgnStr, String] = _.value
extension (str: String)
def chess960: Position = makeBoard(str, chess.variant.Chess960)
def kingOfTheHill: Position = makeBoard(str, chess.variant.KingOfTheHill)
def threeCheck: Position = makeBoard(str, chess.variant.ThreeCheck)
def chess960: Position = makeBoard(str, chess.variant.Chess960)
def kingOfTheHill: Position = makeBoard(str, chess.variant.KingOfTheHill)
def threeCheck: Position = makeBoard(str, chess.variant.ThreeCheck)
def as(color: Color): Position = (Visual << str).withColor(color)
extension (board: Position)
def visual = Visual >> board
def visual = Visual >> board
def destsFrom(from: Square): Option[List[Square]] =
board
.pieceAt(from)
@@ -93,7 +93,7 @@ trait ChessTestCommon:
def makeChess960Board(position: Int) =
Position(Board.fromMap(Chess960.initialPieces(position)), Chess960, White)
def makeChess960Game(position: Int) = Game(makeChess960Board(position))
def chess960Boards = (0 to 959).map(makeChess960Board).toList
def chess960Boards = (0 to 959).map(makeChess960Board).toList
def makeEmptyBoard: Position = Position(Board.empty, Standard, White)
@@ -150,8 +150,8 @@ trait MunitExtensions extends munit.FunSuite:
given [A, B](using comp: munit.Compare[A, B]): munit.Compare[Option[A], Option[B]] with
def isEqual(obtained: Option[A], expected: Option[B]): Boolean = (obtained, expected) match
case (Some(o), Some(e)) => comp.isEqual(o, e)
case (None, None) => true
case _ => false
case (None, None) => true
case _ => false
given [A, B](using sr: SameRuntime[B, A]): munit.Compare[List[A], List[B]] with
def isEqual(obtained: List[A], expected: List[B]): Boolean =
@@ -164,21 +164,21 @@ trait MunitExtensions extends munit.FunSuite:
extension [A](v: Option[A])
def assertSome(f: PartialFunction[A, Unit])(using Location): Any = v match
case Some(a) => f.lift(a).getOrElse(fail(s"Unexpected Some value: $a"))
case None => fail(s"Expected Some but received None")
case None => fail(s"Expected Some but received None")
extension [E, A](v: Either[E, A])
def assertRight(f: PartialFunction[A, Unit])(using Location): Any = v match
case Right(r) => f.lift(r).getOrElse(fail(s"Unexpected Right value: $r"))
case Left(_) => fail(s"Expected Right but received $v")
case Left(_) => fail(s"Expected Right but received $v")
def get: A = v match
case Right(r) => r
case Left(_) => fail(s"Expected Right but received $v")
case Left(_) => fail(s"Expected Right but received $v")
trait ChessTest extends munit.FunSuite with ChessTestCommon with MunitExtensions:
import munit.Location
object clockConv:
given Conversion[Int, Clock.LimitSeconds] = Clock.LimitSeconds(_)
given Conversion[Int, Clock.LimitSeconds] = Clock.LimitSeconds(_)
given Conversion[Int, Clock.IncrementSeconds] = Clock.IncrementSeconds(_)
object compare:
+2 -2
View File
@@ -9,7 +9,7 @@ class ClockTest extends ChessTest:
import clockConv.given
val clock = Clock(5 * 60 * 1000, 0)
val game = makeGame.withClock(clock.start)
val game = makeGame.withClock(clock.start)
test("play with a clock: new game"):
assertEquals(game.clock.map { _.color }, Option(White))
@@ -58,7 +58,7 @@ class ClockLagCompTest extends ChessTest:
.remainingTime(Black))
.centis
def clockStep60(w: Int, l: Int*) = clockStep(fakeClock60, w, l*)
def clockStep60(w: Int, l: Int*) = clockStep(fakeClock60, w, l*)
def clockStep600(w: Int, l: Int*) = clockStep(fakeClock600, w, l*)
def clockStart(lag: Int) =
@@ -47,7 +47,7 @@ class CrazyhouseDataTest extends ScalaCheckSuite:
property("store and drop multiple pieces with promotion"):
forAll: (ps: List[Piece], square: Square, promoted: Bitboard) =>
val filtered = ps.filter(_.role != King)
val data = filtered.foldLeft(Data.init.copy(promoted = promoted)) { (data, piece) =>
val data = filtered.foldLeft(Data.init.copy(promoted = promoted)) { (data, piece) =>
data.store(piece, square)
}
assertEquals(data.size, filtered.size)
@@ -10,19 +10,19 @@ class CrazyhouseVariantTest extends ChessTest:
test("nothing to drop"):
val fenPosition = FullFen("3Nkb1r/1pQP1ppp/4p3/3N4/N5N1/6B1/PPPPBPPP/R1B2RK1 b - - 0 25")
val game = fenToGame(fenPosition, Crazyhouse).updatePosition: b =>
val game = fenToGame(fenPosition, Crazyhouse).updatePosition: b =>
b.withCrazyData(Crazyhouse.Data.init)
assert(game.position.checkMate)
assertNot(game.position.opponentHasInsufficientMaterial)
test("checkmate"):
val fenPosition = FullFen("r2q1b1r/ppp1kPpp/2p5/2PpN3/1n1Pb3/3PK3/PPr1BPPP/n1q1N2R/b w - - 0 20")
val game = fenToGame(fenPosition, Crazyhouse)
val game = fenToGame(fenPosition, Crazyhouse)
assert(game.position.checkMate)
test("pieces to drop, in vain"):
val fenPosition = FullFen("3Nkb1r/1pQP1ppp/4p3/3N4/N5N1/6B1/PPPPBPPP/R1B2RK1 b - - 0 25")
val game = fenToGame(fenPosition, Crazyhouse).updatePosition: b =>
val game = fenToGame(fenPosition, Crazyhouse).updatePosition: b =>
b.withCrazyData(
Crazyhouse.Data(
pockets = ByColor(
@@ -260,7 +260,7 @@ class CrazyhouseVariantTest extends ChessTest:
test("autodraw: not draw when only kings left"):
val fenPosition = FullFen("k6K/8/8/8/8/8/8/8 w - - 0 25")
val game = fenToGame(fenPosition, Crazyhouse)
val game = fenToGame(fenPosition, Crazyhouse)
assertNot(game.position.autoDraw)
assertNot(game.position.end)
assertNot(game.position.opponentHasInsufficientMaterial)
@@ -343,7 +343,7 @@ class CrazyhouseVariantTest extends ChessTest:
test("Index out of bounds when hashing pockets"):
val fenPosition = FullFen("2q1k1nr/B3bbrb/8/8/8/8/3qN1RB/1Q2KB1R/RRRQQQQQQrrrqqq w Kk - 0 11")
val game = fenToGame(fenPosition, Crazyhouse)
val game = fenToGame(fenPosition, Crazyhouse)
assert(game.apply(E1, D2).isRight)
case class DropTestCase(fen: FullFen, drops: Option[Set[Square]])
@@ -17,9 +17,9 @@ class DecayingStatsTest extends ChessTest:
.toFloat / (elts.size - 1)
val randoms: Array[Float] = Array.fill(1000) { random.nextGaussian.toFloat }
val data10: Array[Float] = randoms.map { _ + 10 }
val data10: Array[Float] = randoms.map { _ + 10 }
val stats10 = DS(10, 100, .9f).record(data10)
val stats10 = DS(10, 100, .9f).record(data10)
val stats10d = DS(10, 100, 0.99f).record(data10)
test("gaussian data: eventually converge with constant mean"):
@@ -30,7 +30,7 @@ class DecayingStatsTest extends ChessTest:
assertCloseTo(stats10d.mean, 10f, 0.1f)
test("gaussian data: eventually converge with second mean"):
val stats2 = stats10.record(randoms)
val stats2 = stats10.record(randoms)
val stats2d = stats10d.record(randoms)
assertCloseTo(stats2.deviation, 1f, 0.25f)
@@ -45,7 +45,7 @@ class DecayingStatsTest extends ChessTest:
assertCloseTo(stats10d.record(randoms.take(100)).mean, 0f, 4f)
test("gaussian data: converge with interleave"):
val dataI = Array(data10, randoms).flatMap(_.zipWithIndex).sortBy(_._2).map(_._1)
val dataI = Array(data10, randoms).flatMap(_.zipWithIndex).sortBy(_._2).map(_._1)
val statsIa = DS(10, 100, .9f).record(dataI)
val statsIb = DS(10, 100, 0.99f).record(dataI)
@@ -56,7 +56,7 @@ class DecayingStatsTest extends ChessTest:
assertCloseTo(statsIb.mean, 5f, 0.25f)
test("flip flop data should converge reasonably"):
val data = Array.iterate(0f, 1000) { 1f - _ }
val data = Array.iterate(0f, 1000) { 1f - _ }
val stats = DS(0, 10, .9f).record(data)
assertCloseTo(stats.mean, .5f, 0.05f)
assertCloseTo(stats.deviation, .5f, 0.05f)
+1 -1
View File
@@ -20,7 +20,7 @@ class HasIdTest extends ScalaCheckSuite:
test("removeById reserves order"):
forAll: (xs: List[Int], id: Int) =>
val sorted = xs.sorted
val sorted = xs.sorted
val removed = sorted.removeById(id)
removed == removed.sorted
+25 -25
View File
@@ -8,51 +8,51 @@ import variant.{ Antichess, Atomic, Crazyhouse, Standard, ThreeCheck }
class HashTest extends ChessTest:
test("Polyglot hasher: match on the starting position"):
val fen = FullFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
val fen = FullFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x463b_9618))
test("Polyglot hasher: match after 1. e4"):
val fen = FullFen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1")
val fen = FullFen("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x823c_9b50))
test("Polyglot hasher: match after 1. e4 d5"):
val fen = FullFen("rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
val fen = FullFen("rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x0756_b944))
test("Polyglot hasher: match after 1. e4 d5 2. e5"):
val fen = FullFen("rnbqkbnr/ppp1pppp/8/3pP3/8/8/PPPP1PPP/RNBQKBNR b KQkq - 0 2")
val fen = FullFen("rnbqkbnr/ppp1pppp/8/3pP3/8/8/PPPP1PPP/RNBQKBNR b KQkq - 0 2")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x662f_afb9))
test("Polyglot hasher: match after 1. e4 d5 2. e5 f5"):
// note that en-passant matters
val fen = FullFen("rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3")
val fen = FullFen("rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x22a4_8b5a))
test("Polyglot hasher: match after 1. e4 d5 2. e5 f5 3. Ke2"):
// 3. Ke2 forfeits castling rights
val fen = FullFen("rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPPKPPP/RNBQ1BNR b kq - 1 3")
val fen = FullFen("rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPPKPPP/RNBQ1BNR b kq - 1 3")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x652a_607c))
test("Polyglot hasher: match after 1. e4 d5 2. e5 f5 3. Ke2 Kf7"):
val fen = FullFen("rnbq1bnr/ppp1pkpp/8/3pPp2/8/8/PPPPKPPP/RNBQ1BNR w - - 2 4")
val fen = FullFen("rnbq1bnr/ppp1pkpp/8/3pPp2/8/8/PPPPKPPP/RNBQ1BNR w - - 2 4")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x00fd_d303))
test("Polyglot hasher: match after 1. a4 b5 2. h4 b4 3. c4"):
// again, note en-passant matters
val fen = FullFen("rnbqkbnr/p1pppppp/8/8/PpP4P/8/1P1PPPP1/RNBQKBNR b KQkq c3 0 3")
val fen = FullFen("rnbqkbnr/p1pppppp/8/8/PpP4P/8/1P1PPPP1/RNBQKBNR b KQkq c3 0 3")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x3c81_23ea))
test("Polyglot hasher: match after 1. a4 b5 2. h4 b4 3. c4 bxc3 4. Ra3"):
// 4. Ra3 partially forfeits castling rights
val fen = FullFen("rnbqkbnr/p1pppppp/8/8/P6P/R1p5/1P1PPPP1/1NBQKBNR b Kkq - 1 4")
val fen = FullFen("rnbqkbnr/p1pppppp/8/8/P6P/R1p5/1P1PPPP1/1NBQKBNR b Kkq - 1 4")
val game = fenToGame(fen, Standard)
assertEquals(Hash(game.position), Hash(0x5c3f_9b82))
@@ -106,45 +106,45 @@ class HashTest extends ChessTest:
test("Hasher: be consistent in crazyhouse"):
// from https://lichess.org/j4r7XHTB/black
val fen = FullFen("r2qkb1r/ppp1pppp/2n2n2/3p2B1/3P2b1/4PN2/PPP1BPPP/RN1QK2R/ b KQkq - 9 5")
val board = Fen.read(Crazyhouse, fen).get
val move = board.move(Square.G4, Square.F3, None).get
val fen = FullFen("r2qkb1r/ppp1pppp/2n2n2/3p2B1/3P2b1/4PN2/PPP1BPPP/RN1QK2R/ b KQkq - 9 5")
val board = Fen.read(Crazyhouse, fen).get
val move = board.move(Square.G4, Square.F3, None).get
val hashAfterMove = Hash(move.after)
// 5 ... Bxf3
val fenAfter = FullFen("r2qkb1r/ppp1pppp/2n2n2/3p2B1/3P4/4Pb2/PPP1BPPP/RN1QK2R/n w KQkq - 10 6")
val fenAfter = FullFen("r2qkb1r/ppp1pppp/2n2n2/3p2B1/3P4/4Pb2/PPP1BPPP/RN1QK2R/n w KQkq - 10 6")
val boardAfter = Fen.read(Crazyhouse, fenAfter).get
val hashAfter = Hash(boardAfter)
val hashAfter = Hash(boardAfter)
assertEquals(hashAfterMove, hashAfter)
test("Hasher: be consistent when king is captured in antichess"):
val fen = FullFen("rnbqkb1r/ppp1pppp/3p1n2/1B6/8/4P3/PPPP1PPP/RNBQK1NR w KQkq - 2 3")
val board = Fen.read(Antichess, fen).get
val move = board.move(Square.B5, Square.E8, None).get
val fen = FullFen("rnbqkb1r/ppp1pppp/3p1n2/1B6/8/4P3/PPPP1PPP/RNBQK1NR w KQkq - 2 3")
val board = Fen.read(Antichess, fen).get
val move = board.move(Square.B5, Square.E8, None).get
val hashAfterMove = Hash(move.after)
// 3. BxK
val fenAfter = FullFen("rnbqBb1r/ppp1pppp/3p1n2/8/8/4P3/PPPP1PPP/RNBQK1NR b KQkq - 0 3")
val fenAfter = FullFen("rnbqBb1r/ppp1pppp/3p1n2/8/8/4P3/PPPP1PPP/RNBQK1NR b KQkq - 0 3")
val boardAfter = Fen.read(Antichess, fenAfter).get
val hashAfter = Hash(boardAfter)
val hashAfter = Hash(boardAfter)
assertEquals(hashAfterMove, hashAfter)
test("Hasher: be consistent when rook is exploded in atomic"):
val fen = FullFen("rnbqkb1r/ppppp1pp/5p1n/6N1/8/8/PPPPPPPP/RNBQKB1R w KQkq - 2 3")
val board = Fen.read(Atomic, fen).get
val move = board.move(Square.G5, Square.H7, None).get
val fen = FullFen("rnbqkb1r/ppppp1pp/5p1n/6N1/8/8/PPPPPPPP/RNBQKB1R w KQkq - 2 3")
val board = Fen.read(Atomic, fen).get
val move = board.move(Square.G5, Square.H7, None).get
val hashAfterMove = Hash(move.after)
// 3. Nxh7
val fenAfter = FullFen("rnbqkb2/ppppp1p1/5p2/8/8/8/PPPPPPPP/RNBQKB1R b KQkq - 0 3")
val fenAfter = FullFen("rnbqkb2/ppppp1p1/5p2/8/8/8/PPPPPPPP/RNBQKB1R b KQkq - 0 3")
val boardAfter = Fen.read(Atomic, fenAfter).get
val hashAfter = Hash(boardAfter)
val hashAfter = Hash(boardAfter)
assertEquals(hashAfterMove, hashAfter)
test("Index out of bounds when hashing pockets"):
val fenPosition = FullFen("2q1k1nr/B3bbrb/8/8/8/8/3qN1RB/1Q2KB1R/RRRQQQQQQrrrqqq w Kk - 0 11")
val game = fenToGame(fenPosition, Crazyhouse)
val game = fenToGame(fenPosition, Crazyhouse)
assert(game.apply(E1, D2).isRight)
+1 -1
View File
@@ -2,7 +2,7 @@ package chess
class ThreefoldRepetitionTest extends ChessTest:
def toHash(a: Int) = PositionHash(Hash(a << 8))
def toHash(a: Int) = PositionHash(Hash(a << 8))
def makeHistory(positions: List[Int]) =
(positions
.map(toHash))
@@ -15,7 +15,7 @@ object HordeInsufficientMaterialTest extends SimpleIOSuite:
run("test-kit/src/test/resources/horde_insufficient_material.csv", Horde).map(expect(_))
given Monoid[Boolean] with
def empty = true
def empty = true
def combine(x: Boolean, y: Boolean) = x && y
private def run(file: String, variant: Variant): IO[Boolean] =
+19 -19
View File
@@ -9,84 +9,84 @@ class HordeVariantTest extends ChessTest:
test("Must not be insufficient winning material for horde with only 1 pawn left"):
val position = FullFen("k7/ppP5/brp5/8/8/8/8/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.opponentHasInsufficientMaterial)
test("Must recognise insufficient winning material for horde with only 1 pawn left"):
val position = FullFen("8/2k5/3q4/8/8/8/1P6/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assert(game.position.opponentHasInsufficientMaterial)
test("Must not be insufficient winning material for king with only 1 pawn left"):
val position = FullFen("8/2k5/3q4/8/8/8/1P6/8 w - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.opponentHasInsufficientMaterial)
test("Must recognise insufficient winning material for horde with only 1 bishop left"):
val position = FullFen("r7/2Bb4/q3k3/8/8/3q4/8/5qqr b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.autoDraw)
assertNot(game.position.end)
assert(game.position.opponentHasInsufficientMaterial)
test("Must recognise insufficient winning material for horde with only 1 queen left"):
val position = FullFen("8/2k5/3q4/8/8/1Q6/8/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assert(game.position.opponentHasInsufficientMaterial)
test("Must not be insufficient winning material for king with only 1 queen left"):
val position = FullFen("8/2k5/3q4/8/8/1Q6/8/8 w - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.opponentHasInsufficientMaterial)
test("Must recognise insufficient winning material for horde with only 2 minor pieces left"):
val position = FullFen("8/2k5/3q4/8/8/1B2N3/8/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assert(game.position.opponentHasInsufficientMaterial)
test("Must not be insufficient winning material for king with only 2 minor pieces left"):
val position = FullFen("8/2k5/3q4/8/8/1B2N3/8/8 w - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.opponentHasInsufficientMaterial)
test("Must not be insufficient winning material for horde with 3 minor pieces left"):
val position = FullFen("8/2k5/3q4/8/8/3B4/4NB2/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.opponentHasInsufficientMaterial)
test("Must not be insufficient winning material for king with queen and rook left"):
val position = FullFen("8/5k2/7q/7P/6rP/6P1/6P1/8 b - - 0 52")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.opponentHasInsufficientMaterial)
assertNot(game.position.autoDraw)
test("Must auto-draw in simple pawn fortress"):
val position = FullFen("8/p7/pk6/P7/P7/8/8/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assert(game.position.autoDraw)
assert(game.position.opponentHasInsufficientMaterial)
test("Must auto-draw if horde is stalemated and only king can move"):
val position = FullFen("QNBRRBNQ/PPpPPpPP/P1P2PkP/8/8/8/8/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assert(game.position.autoDraw)
assert(game.position.opponentHasInsufficientMaterial)
test("Must auto-draw if horde is stalemated and only king can move"):
val position = FullFen("b7/pk6/P7/P7/8/8/8/8 b - - 0 1")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assert(game.position.autoDraw)
assert(game.position.opponentHasInsufficientMaterial)
test("Must not auto-draw if horde is not stalemated after the only king move"):
val position = FullFen("8/1b5r/1P6/1Pk3q1/1PP5/r1P5/P1P5/2P5 b - - 0 52")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.autoDraw)
assertNot(game.position.opponentHasInsufficientMaterial)
test("Must not auto-draw if not all black King moves leads to stalemate"):
val position = FullFen("8/8/8/7k/7P/7P/8/8 b - - 0 58")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.autoDraw)
assertNot(game.position.end)
assertEquals(game.position.status, None)
@@ -95,18 +95,18 @@ class HordeVariantTest extends ChessTest:
test("Must not auto-draw in B vs K endgame, king can win"):
val position = FullFen("7B/6k1/8/8/8/8/8/8 b - -")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assertNot(game.position.autoDraw)
assert(game.position.opponentHasInsufficientMaterial)
test("Pawn on first rank should able to move two squares"):
val position = FullFen("8/pp1k2q1/3P2p1/8/P3PP2/PPP2r2/PPP5/PPPP4 w - - 1 2")
val game = fenToGame(position, Horde)
val game = fenToGame(position, Horde)
assert(game.position.legalMoves.exists(m => m.orig == Square.D1 && m.dest == Square.D3))
test("Cannot en passant a pawn from the first rank"):
val position = FullFen("k7/5p2/4p2P/3p2P1/2p2P2/1p2P2P/p2P2P1/2P2P2 w - - 0 1")
val game = fenToGame(position, Horde)(Square.C1, Square.C3).get
val game = fenToGame(position, Horde)(Square.C1, Square.C3).get
assertNot(game._1.position.legalMoves.exists(m => m.orig == Square.B3 && m.dest == Square.C2))
test("Castle with one rook moved"):
@@ -125,6 +125,6 @@ class HordeVariantTest extends ChessTest:
test("the h8 rooks move"):
val position = FullFen("r3kbnr/p1pqppp1/1pnp3P/PPPP1P1P/PPP1PPP1/1PPP1PPP/PPPPPPPP/PPPPPPPP b kq - 0 7")
val game = fenToGame(position, Horde)(Square.H8, Square.H6).get
val game = fenToGame(position, Horde)(Square.H8, Square.H6).get
assertEquals(game._1.position.history.unmovedRooks, UnmovedRooks(Set(Square.A8)))
assertEquals(game._1.position.history.castles, Castles(false, false, false, true))

Some files were not shown because too many files have changed in this diff Show More