perf(core): reduce inter-service HTTP calls from 11 to 4 per move
- Add `postMoveStatus` batch method to `RuleSet` trait (default impl composes individual calls; `RuleSetRestAdapter` overrides with single HTTP round-trip) - Collapse 5 sequential rule checks in `GameEngine.executeMove` into one `postMoveStatus` call - Add `POST /api/rules/post-move-status` endpoint to rule-service - Add `exportCombined` to `IoServiceClient` and `POST /io/export/combined` endpoint to io-service, replacing two separate FEN/PGN HTTP calls - Fix `statusOf` to pattern-match on `WinReason` from `ctx.result` instead of making a redundant `isCheckmate` HTTP call - Remove duplicate `legalMoves` pre-validation in `GameResource.makeMove`; engine already validates and fires `InvalidMoveEvent` - Add `scalafix:off` guards for pre-existing `var`/`return` usage in `DefaultRules` hot-path code - Apply spotless formatting to previously unformatted files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.Move
|
||||
import de.nowchess.rules.dto.*
|
||||
import de.nowchess.api.rules.PostMoveStatus
|
||||
import de.nowchess.rules.sets.DefaultRules
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.ws.rs.*
|
||||
@@ -88,3 +89,10 @@ class RuleSetResource:
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def applyMove(req: ContextMoveRequest): GameContext =
|
||||
rules.applyMove(req.context)(req.move)
|
||||
|
||||
@POST
|
||||
@Path("/post-move-status")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def postMoveStatus(ctx: GameContext): PostMoveStatus =
|
||||
rules.postMoveStatus(ctx)
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.nowchess.rules.sets
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.api.rules.RuleSet
|
||||
import de.nowchess.api.rules.{PostMoveStatus, RuleSet}
|
||||
|
||||
/** Standard chess rules — optimized hot path.
|
||||
*
|
||||
@@ -12,52 +12,54 @@ import de.nowchess.api.rules.RuleSet
|
||||
* avoid heap allocation in tight loops. Check detection uses make/unmake on the mutable array instead of copying the
|
||||
* immutable Board map.
|
||||
*/
|
||||
// scalafix:off DisableSyntax.var
|
||||
// scalafix:off DisableSyntax.return
|
||||
object DefaultRules extends RuleSet:
|
||||
|
||||
// ─── Piece constants ──────────────────────────────────────────────────────
|
||||
private val PAWN = 1; private val KNIGHT = 2; private val BISHOP = 3
|
||||
private val ROOK = 4; private val QUEEN = 5; private val KING = 6
|
||||
|
||||
private inline def idx(f: Int, r: Int): Int = f + (r << 3)
|
||||
private inline def fileOf(sq: Int): Int = sq & 7
|
||||
private inline def rankOf(sq: Int): Int = sq >> 3
|
||||
private inline def isEmpty(p: Int): Boolean = p == 0
|
||||
private inline def isWhitePiece(p: Int): Boolean = p > 0
|
||||
private inline def pieceType(p: Int): Int = if p > 0 then p else -p
|
||||
private inline def idx(f: Int, r: Int): Int = f + (r << 3)
|
||||
private inline def fileOf(sq: Int): Int = sq & 7
|
||||
private inline def rankOf(sq: Int): Int = sq >> 3
|
||||
private inline def isEmpty(p: Int): Boolean = p == 0
|
||||
private inline def isWhitePiece(p: Int): Boolean = p > 0
|
||||
private inline def pieceType(p: Int): Int = if p > 0 then p else -p
|
||||
|
||||
private def encodePiece(c: Color, pt: PieceType): Int =
|
||||
val raw = pt match
|
||||
case PieceType.Pawn => PAWN; case PieceType.Knight => KNIGHT
|
||||
case PieceType.Pawn => PAWN; case PieceType.Knight => KNIGHT
|
||||
case PieceType.Bishop => BISHOP; case PieceType.Rook => ROOK
|
||||
case PieceType.Queen => QUEEN; case PieceType.King => KING
|
||||
case PieceType.Queen => QUEEN; case PieceType.King => KING
|
||||
if c == Color.White then raw else -raw
|
||||
|
||||
// ─── Pre-computed tables ──────────────────────────────────────────────────
|
||||
|
||||
private val KNIGHT_TARGETS: Array[Array[Int]] = Array.tabulate(64) { sq =>
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
Array((2,1),(2,-1),(-2,1),(-2,-1),(1,2),(1,-2),(-1,2),(-1,-2)).collect {
|
||||
case (df, dr) if f+df >= 0 && f+df < 8 && r+dr >= 0 && r+dr < 8 => idx(f+df, r+dr)
|
||||
Array((2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2)).collect {
|
||||
case (df, dr) if f + df >= 0 && f + df < 8 && r + dr >= 0 && r + dr < 8 => idx(f + df, r + dr)
|
||||
}
|
||||
}
|
||||
|
||||
private val KING_TARGETS: Array[Array[Int]] = Array.tabulate(64) { sq =>
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
Array((-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)).collect {
|
||||
case (df, dr) if f+df >= 0 && f+df < 8 && r+dr >= 0 && r+dr < 8 => idx(f+df, r+dr)
|
||||
Array((-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)).collect {
|
||||
case (df, dr) if f + df >= 0 && f + df < 8 && r + dr >= 0 && r + dr < 8 => idx(f + df, r + dr)
|
||||
}
|
||||
}
|
||||
|
||||
// Directions 0-3: rook (N,S,E,W); 4-7: bishop (NE,NW,SE,SW)
|
||||
private val DIR_VECS: Array[(Int, Int)] =
|
||||
Array((0,1),(0,-1),(1,0),(-1,0),(1,1),(-1,1),(1,-1),(-1,-1))
|
||||
Array((0, 1), (0, -1), (1, 0), (-1, 0), (1, 1), (-1, 1), (1, -1), (-1, -1))
|
||||
|
||||
// RAY_TABLES(sq)(d) = squares along direction d from sq, nearest first
|
||||
private val RAY_TABLES: Array[Array[Array[Int]]] = Array.tabulate(64, 8) { (sq, d) =>
|
||||
val (df, dr) = DIR_VECS(d)
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
val buf = new scala.collection.mutable.ArrayBuffer[Int](7)
|
||||
var nf = f + df; var nr = r + dr
|
||||
val buf = new scala.collection.mutable.ArrayBuffer[Int](7)
|
||||
var nf = f + df; var nr = r + dr
|
||||
while nf >= 0 && nf < 8 && nr >= 0 && nr < 8 do
|
||||
buf += idx(nf, nr); nf += df; nr += dr
|
||||
buf.toArray
|
||||
@@ -71,18 +73,18 @@ object DefaultRules extends RuleSet:
|
||||
Array.tabulate(64) { sq =>
|
||||
val (f, r) = (fileOf(sq), rankOf(sq))
|
||||
Array(-1, 1).collect {
|
||||
case df if f+df >= 0 && f+df < 8 && r-fwd >= 0 && r-fwd < 8 => idx(f+df, r-fwd)
|
||||
case df if f + df >= 0 && f + df < 8 && r - fwd >= 0 && r - fwd < 8 => idx(f + df, r - fwd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-computed castling square indices (no runtime string parsing)
|
||||
private val A1 = idx(0,0); private val B1 = idx(1,0); private val C1 = idx(2,0)
|
||||
private val D1 = idx(3,0); private val E1 = idx(4,0); private val F1 = idx(5,0)
|
||||
private val G1 = idx(6,0); private val H1 = idx(7,0)
|
||||
private val A8 = idx(0,7); private val B8 = idx(1,7); private val C8 = idx(2,7)
|
||||
private val D8 = idx(3,7); private val E8 = idx(4,7); private val F8 = idx(5,7)
|
||||
private val G8 = idx(6,7); private val H8 = idx(7,7)
|
||||
private val A1 = idx(0, 0); private val B1 = idx(1, 0); private val C1 = idx(2, 0)
|
||||
private val D1 = idx(3, 0); private val E1 = idx(4, 0); private val F1 = idx(5, 0)
|
||||
private val G1 = idx(6, 0); private val H1 = idx(7, 0)
|
||||
private val A8 = idx(0, 7); private val B8 = idx(1, 7); private val C8 = idx(2, 7)
|
||||
private val D8 = idx(3, 7); private val E8 = idx(4, 7); private val F8 = idx(5, 7)
|
||||
private val G8 = idx(6, 7); private val H8 = idx(7, 7)
|
||||
|
||||
// Thread-local mutable board and move buffer — zero heap allocation in hot loops
|
||||
private val tlBoard = ThreadLocal.withInitial[Array[Int]](() => new Array[Int](64))
|
||||
@@ -91,15 +93,15 @@ object DefaultRules extends RuleSet:
|
||||
|
||||
// ─── Move word encoding ───────────────────────────────────────────────────
|
||||
// bits 0-5: from square, bits 6-11: to square, bits 12-15: move kind
|
||||
private val KIND_QUIET = 0; private val KIND_CAPTURE = 1; private val KIND_EP = 2
|
||||
private val KIND_QUIET = 0; private val KIND_CAPTURE = 1; private val KIND_EP = 2
|
||||
private val KIND_CASTLEK = 3; private val KIND_CASTLEQ = 4
|
||||
private val KIND_PROMO_Q = 5; private val KIND_PROMO_R = 6
|
||||
private val KIND_PROMO_B = 7; private val KIND_PROMO_N = 8
|
||||
|
||||
private inline def encMove(from: Int, to: Int, kind: Int): Int = from | (to << 6) | (kind << 12)
|
||||
private inline def moveFrom(m: Int): Int = m & 63
|
||||
private inline def moveTo(m: Int): Int = (m >> 6) & 63
|
||||
private inline def moveKind(m: Int): Int = m >> 12
|
||||
private inline def moveFrom(m: Int): Int = m & 63
|
||||
private inline def moveTo(m: Int): Int = (m >> 6) & 63
|
||||
private inline def moveKind(m: Int): Int = m >> 12
|
||||
|
||||
// ─── Board ↔ Array[Int] ──────────────────────────────────────────────────
|
||||
|
||||
@@ -174,10 +176,10 @@ object DefaultRules extends RuleSet:
|
||||
// Applies move on mutable arr, tests check, undoes — no Map copy.
|
||||
|
||||
private def leavesKingInCheck(arr: Array[Int], move: Int, whiteMoved: Boolean): Boolean =
|
||||
val from = moveFrom(move); val to = moveTo(move); val kind = moveKind(move)
|
||||
val from = moveFrom(move); val to = moveTo(move); val kind = moveKind(move)
|
||||
val savedFrom = arr(from); val savedTo = arr(to)
|
||||
var epSq = -1
|
||||
var rookFrom = -1; var savedRookPiece = 0; var rookTo = -1
|
||||
var rookFrom = -1; var savedRookPiece = 0; var rookTo = -1
|
||||
|
||||
kind match
|
||||
case KIND_EP =>
|
||||
@@ -186,20 +188,20 @@ object DefaultRules extends RuleSet:
|
||||
|
||||
case KIND_CASTLEK =>
|
||||
rookFrom = if whiteMoved then H1 else H8
|
||||
rookTo = if whiteMoved then F1 else F8
|
||||
rookTo = if whiteMoved then F1 else F8
|
||||
savedRookPiece = arr(rookFrom)
|
||||
arr(to) = savedFrom; arr(from) = 0; arr(rookTo) = savedRookPiece; arr(rookFrom) = 0
|
||||
|
||||
case KIND_CASTLEQ =>
|
||||
rookFrom = if whiteMoved then A1 else A8
|
||||
rookTo = if whiteMoved then D1 else D8
|
||||
rookTo = if whiteMoved then D1 else D8
|
||||
savedRookPiece = arr(rookFrom)
|
||||
arr(to) = savedFrom; arr(from) = 0; arr(rookTo) = savedRookPiece; arr(rookFrom) = 0
|
||||
|
||||
case k if k >= KIND_PROMO_Q =>
|
||||
val promoted = k match
|
||||
case KIND_PROMO_Q => if whiteMoved then QUEEN else -QUEEN
|
||||
case KIND_PROMO_R => if whiteMoved then ROOK else -ROOK
|
||||
case KIND_PROMO_Q => if whiteMoved then QUEEN else -QUEEN
|
||||
case KIND_PROMO_R => if whiteMoved then ROOK else -ROOK
|
||||
case KIND_PROMO_B => if whiteMoved then BISHOP else -BISHOP
|
||||
case _ => if whiteMoved then KNIGHT else -KNIGHT
|
||||
arr(to) = promoted; arr(from) = 0
|
||||
@@ -225,22 +227,36 @@ object DefaultRules extends RuleSet:
|
||||
var n = 0; var sq = 0
|
||||
while sq < 64 do
|
||||
val p = arr(sq)
|
||||
if !isEmpty(p) && isWhitePiece(p) == isWhite then
|
||||
n = generatePiece(arr, sq, pieceType(p), isWhite, ctx, buf, n)
|
||||
if !isEmpty(p) && isWhitePiece(p) == isWhite then n = generatePiece(arr, sq, pieceType(p), isWhite, ctx, buf, n)
|
||||
sq += 1
|
||||
n
|
||||
|
||||
private def generatePiece(arr: Array[Int], sq: Int, pt: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], n: Int): Int =
|
||||
if pt == PAWN then generatePawnMoves(arr, sq, isWhite, ctx, buf, n)
|
||||
private def generatePiece(
|
||||
arr: Array[Int],
|
||||
sq: Int,
|
||||
pt: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
n: Int,
|
||||
): Int =
|
||||
if pt == PAWN then generatePawnMoves(arr, sq, isWhite, ctx, buf, n)
|
||||
else if pt == KNIGHT then generateJumps(arr, sq, isWhite, KNIGHT_TARGETS(sq), buf, n)
|
||||
else if pt == BISHOP then generateRays(arr, sq, isWhite, buf, n, rookRays = false)
|
||||
else if pt == ROOK then generateRays(arr, sq, isWhite, buf, n, rookRays = true)
|
||||
else if pt == QUEEN then
|
||||
else if pt == ROOK then generateRays(arr, sq, isWhite, buf, n, rookRays = true)
|
||||
else if pt == QUEEN then
|
||||
val n2 = generateRays(arr, sq, isWhite, buf, n, rookRays = true)
|
||||
generateRays(arr, sq, isWhite, buf, n2, rookRays = false)
|
||||
else generateKingMoves(arr, sq, isWhite, ctx, buf, n)
|
||||
|
||||
private def generateJumps(arr: Array[Int], from: Int, isWhite: Boolean, targets: Array[Int], buf: Array[Int], start: Int): Int =
|
||||
private def generateJumps(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
targets: Array[Int],
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
var n = start; var i = 0
|
||||
while i < targets.length do
|
||||
val to = targets(i); val tgt = arr(to)
|
||||
@@ -251,8 +267,15 @@ object DefaultRules extends RuleSet:
|
||||
i += 1
|
||||
n
|
||||
|
||||
private def generateRays(arr: Array[Int], from: Int, isWhite: Boolean, buf: Array[Int], start: Int, rookRays: Boolean): Int =
|
||||
var n = start
|
||||
private def generateRays(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
rookRays: Boolean,
|
||||
): Int =
|
||||
var n = start
|
||||
val rays = RAY_TABLES(from)
|
||||
val d0 = if rookRays then 0 else 4
|
||||
val d1 = if rookRays then 4 else 8
|
||||
@@ -271,42 +294,67 @@ object DefaultRules extends RuleSet:
|
||||
d += 1
|
||||
n
|
||||
|
||||
private def generateKingMoves(arr: Array[Int], from: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], start: Int): Int =
|
||||
private def generateKingMoves(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
val n = generateJumps(arr, from, isWhite, KING_TARGETS(from), buf, start)
|
||||
generateCastlingMoves(arr, from, isWhite, ctx, buf, n)
|
||||
|
||||
private def generateCastlingMoves(arr: Array[Int], from: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], start: Int): Int =
|
||||
private def generateCastlingMoves(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
var n = start
|
||||
val cr = ctx.castlingRights
|
||||
if isWhite && from == E1 then
|
||||
if cr.whiteKingSide && isEmpty(arr(F1)) && isEmpty(arr(G1)) &&
|
||||
arr(E1) == KING && arr(H1) == ROOK &&
|
||||
!isAttackedByColor(arr, E1, false) &&
|
||||
!isAttackedByColor(arr, F1, false) &&
|
||||
!isAttackedByColor(arr, G1, false) then
|
||||
arr(E1) == KING && arr(H1) == ROOK &&
|
||||
!isAttackedByColor(arr, E1, false) &&
|
||||
!isAttackedByColor(arr, F1, false) &&
|
||||
!isAttackedByColor(arr, G1, false)
|
||||
then
|
||||
buf(n) = encMove(E1, G1, KIND_CASTLEK); n += 1
|
||||
if cr.whiteQueenSide && isEmpty(arr(D1)) && isEmpty(arr(C1)) && isEmpty(arr(B1)) &&
|
||||
arr(E1) == KING && arr(A1) == ROOK &&
|
||||
!isAttackedByColor(arr, E1, false) &&
|
||||
!isAttackedByColor(arr, D1, false) &&
|
||||
!isAttackedByColor(arr, C1, false) then
|
||||
arr(E1) == KING && arr(A1) == ROOK &&
|
||||
!isAttackedByColor(arr, E1, false) &&
|
||||
!isAttackedByColor(arr, D1, false) &&
|
||||
!isAttackedByColor(arr, C1, false)
|
||||
then
|
||||
buf(n) = encMove(E1, C1, KIND_CASTLEQ); n += 1
|
||||
else if !isWhite && from == E8 then
|
||||
if cr.blackKingSide && isEmpty(arr(F8)) && isEmpty(arr(G8)) &&
|
||||
arr(E8) == -KING && arr(H8) == -ROOK &&
|
||||
!isAttackedByColor(arr, E8, true) &&
|
||||
!isAttackedByColor(arr, F8, true) &&
|
||||
!isAttackedByColor(arr, G8, true) then
|
||||
arr(E8) == -KING && arr(H8) == -ROOK &&
|
||||
!isAttackedByColor(arr, E8, true) &&
|
||||
!isAttackedByColor(arr, F8, true) &&
|
||||
!isAttackedByColor(arr, G8, true)
|
||||
then
|
||||
buf(n) = encMove(E8, G8, KIND_CASTLEK); n += 1
|
||||
if cr.blackQueenSide && isEmpty(arr(D8)) && isEmpty(arr(C8)) && isEmpty(arr(B8)) &&
|
||||
arr(E8) == -KING && arr(A8) == -ROOK &&
|
||||
!isAttackedByColor(arr, E8, true) &&
|
||||
!isAttackedByColor(arr, D8, true) &&
|
||||
!isAttackedByColor(arr, C8, true) then
|
||||
arr(E8) == -KING && arr(A8) == -ROOK &&
|
||||
!isAttackedByColor(arr, E8, true) &&
|
||||
!isAttackedByColor(arr, D8, true) &&
|
||||
!isAttackedByColor(arr, C8, true)
|
||||
then
|
||||
buf(n) = encMove(E8, C8, KIND_CASTLEQ); n += 1
|
||||
n
|
||||
|
||||
private def generatePawnMoves(arr: Array[Int], from: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], start: Int): Int =
|
||||
private def generatePawnMoves(
|
||||
arr: Array[Int],
|
||||
from: Int,
|
||||
isWhite: Boolean,
|
||||
ctx: GameContext,
|
||||
buf: Array[Int],
|
||||
start: Int,
|
||||
): Int =
|
||||
var n = start
|
||||
val f = fileOf(from); val r = rankOf(from)
|
||||
val fwd = if isWhite then 1 else -1
|
||||
@@ -376,27 +424,27 @@ object DefaultRules extends RuleSet:
|
||||
val sqI = idx(square.file.ordinal, square.rank.ordinal)
|
||||
val piece = arr(sqI)
|
||||
if isEmpty(piece) || isWhitePiece(piece) != (context.turn == Color.White) then return Nil
|
||||
val buf = new Array[Int](64)
|
||||
val n = generatePiece(arr, sqI, pieceType(piece), context.turn == Color.White, context, buf, 0)
|
||||
val buf = new Array[Int](64)
|
||||
val n = generatePiece(arr, sqI, pieceType(piece), context.turn == Color.White, context, buf, 0)
|
||||
(0 until n).map(i => decodeMoveToApi(buf(i))).toList
|
||||
|
||||
override def legalMoves(context: GameContext)(square: Square): List[Move] =
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val sqI = idx(square.file.ordinal, square.rank.ordinal)
|
||||
val piece = arr(sqI)
|
||||
val isWhite = context.turn == Color.White
|
||||
if isEmpty(piece) || isWhitePiece(piece) != isWhite then return Nil
|
||||
val buf = tlMoves.get()
|
||||
val n = generatePiece(arr, sqI, pieceType(piece), isWhite, context, buf, 0)
|
||||
val result = new scala.collection.mutable.ListBuffer[Move]()
|
||||
var i = 0
|
||||
val buf = tlMoves.get()
|
||||
val n = generatePiece(arr, sqI, pieceType(piece), isWhite, context, buf, 0)
|
||||
val result = new scala.collection.mutable.ListBuffer[Move]()
|
||||
var i = 0
|
||||
while i < n do
|
||||
if !leavesKingInCheck(arr, buf(i), isWhite) then result += decodeMoveToApi(buf(i))
|
||||
i += 1
|
||||
result.toList
|
||||
|
||||
override def allLegalMoves(context: GameContext): List[Move] =
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val isWhite = context.turn == Color.White
|
||||
val buf = tlMoves.get()
|
||||
val n = generateAll(arr, isWhite, context, buf)
|
||||
@@ -408,7 +456,7 @@ object DefaultRules extends RuleSet:
|
||||
result.toList
|
||||
|
||||
override def isCheck(context: GameContext): Boolean =
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val arr = tlBoard.get(); fillBoard(context.board, arr)
|
||||
val isWhite = context.turn == Color.White
|
||||
val kingSq = findKing(arr, isWhite)
|
||||
kingSq >= 0 && isAttackedByColor(arr, kingSq, !isWhite)
|
||||
@@ -433,7 +481,7 @@ object DefaultRules extends RuleSet:
|
||||
|
||||
override def applyMove(context: GameContext)(move: Move): GameContext =
|
||||
val color = context.turn
|
||||
val board = context.board
|
||||
val board = context.board
|
||||
|
||||
val newBoard = move.moveType match
|
||||
case MoveType.CastleKingside => applyCastle(board, color, kingside = true)
|
||||
@@ -487,8 +535,8 @@ object DefaultRules extends RuleSet:
|
||||
val isKingMove = piece.exists(_.pieceType == PieceType.King)
|
||||
val isRookMove = piece.exists(_.pieceType == PieceType.Rook)
|
||||
|
||||
val whiteKingsideRook = Square(File.H, Rank.R1); val whiteQueensideRook = Square(File.A, Rank.R1)
|
||||
val blackKingsideRook = Square(File.H, Rank.R8); val blackQueensideRook = Square(File.A, Rank.R8)
|
||||
val whiteKingsideRook = Square(File.H, Rank.R1); val whiteQueensideRook = Square(File.A, Rank.R1)
|
||||
val blackKingsideRook = Square(File.H, Rank.R8); val blackQueensideRook = Square(File.A, Rank.R8)
|
||||
|
||||
val afterKingMove = if isKingMove then rights.revokeColor(color) else rights
|
||||
val afterRookMove =
|
||||
@@ -523,7 +571,7 @@ object DefaultRules extends RuleSet:
|
||||
private def insufficientMaterial(board: Board): Boolean =
|
||||
val nonKings = board.pieces.toList.filter { case (_, p) => p.pieceType != PieceType.King }
|
||||
nonKings match
|
||||
case Nil => true
|
||||
case Nil => true
|
||||
case List((_, p)) if p.pieceType == PieceType.Bishop || p.pieceType == PieceType.Knight => true
|
||||
case bishops if bishops.forall { case (_, p) => p.pieceType == PieceType.Bishop } =>
|
||||
bishops.map { case (sq, _) => squareColor(sq) }.distinct.sizeIs == 1
|
||||
@@ -531,18 +579,23 @@ object DefaultRules extends RuleSet:
|
||||
|
||||
// ─── Threefold repetition ─────────────────────────────────────────────────
|
||||
|
||||
private case class Position(board: Board, turn: Color, castlingRights: CastlingRights, enPassantSquare: Option[Square])
|
||||
private case class Position(
|
||||
board: Board,
|
||||
turn: Color,
|
||||
castlingRights: CastlingRights,
|
||||
enPassantSquare: Option[Square],
|
||||
)
|
||||
|
||||
private def countPositionOccurrences(context: GameContext, target: Position): Int =
|
||||
try
|
||||
val initialCtx = GameContext(
|
||||
board = context.initialBoard,
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights.Initial,
|
||||
board = context.initialBoard,
|
||||
turn = Color.White,
|
||||
castlingRights = CastlingRights.Initial,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty,
|
||||
initialBoard = context.initialBoard,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty,
|
||||
initialBoard = context.initialBoard,
|
||||
)
|
||||
|
||||
def positionOf(ctx: GameContext): Position =
|
||||
@@ -557,5 +610,13 @@ object DefaultRules extends RuleSet:
|
||||
(nextCtx, nextCount)
|
||||
}
|
||||
._2
|
||||
catch
|
||||
case _: Exception => 1
|
||||
catch case _: Exception => 1
|
||||
|
||||
override def postMoveStatus(context: GameContext): PostMoveStatus =
|
||||
PostMoveStatus(
|
||||
isCheckmate = isCheckmate(context),
|
||||
isStalemate = isStalemate(context),
|
||||
isInsufficientMaterial = isInsufficientMaterial(context),
|
||||
isCheck = isCheck(context),
|
||||
isThreefoldRepetition = isThreefoldRepetition(context),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user