refactor(rule): implement StandardRules with GameContext
This commit is contained in:
@@ -14,6 +14,9 @@ object Board:
|
||||
val captured = b.get(to)
|
||||
val updatedBoard = b.removed(from).updated(to, b(from))
|
||||
(updatedBoard, captured)
|
||||
def applyMove(move: de.nowchess.api.move.Move): Board =
|
||||
val (updatedBoard, _) = b.withMove(move.from, move.to)
|
||||
updatedBoard
|
||||
def pieces: Map[Square, Piece] = b
|
||||
|
||||
val initial: Board =
|
||||
|
||||
@@ -39,3 +39,19 @@ object Square:
|
||||
if n >= 1 && n <= 8 then Some(Rank.values(n - 1)) else None
|
||||
)
|
||||
for f <- fileOpt; r <- rankOpt yield Square(f, r)
|
||||
|
||||
val all: IndexedSeq[Square] =
|
||||
for
|
||||
r <- Rank.values.toIndexedSeq
|
||||
f <- File.values.toIndexedSeq
|
||||
yield Square(f, r)
|
||||
|
||||
/** Compute a target square by offsetting file and rank.
|
||||
* Returns None if the resulting square is outside the board (0-7 range). */
|
||||
extension (sq: Square)
|
||||
def offset(fileDelta: Int, rankDelta: Int): Option[Square] =
|
||||
val newFileOrd = sq.file.ordinal + fileDelta
|
||||
val newRankOrd = sq.rank.ordinal + rankDelta
|
||||
if newFileOrd >= 0 && newFileOrd < 8 && newRankOrd >= 0 && newRankOrd < 8 then
|
||||
Some(Square(File.values(newFileOrd), Rank.values(newRankOrd)))
|
||||
else None
|
||||
@@ -1,192 +1,242 @@
|
||||
package de.nowchess.rules
|
||||
|
||||
import org.maichess.mono.model.*
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.{Board, Color, Square, PieceType, Piece}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/** Standard chess rules implementation.
|
||||
* Handles move generation, validation, check/checkmate/stalemate detection.
|
||||
*/
|
||||
object StandardRules extends RuleSet:
|
||||
|
||||
// ── Directions ────────────────────────────────────────────────────────────
|
||||
private val rookDirs: List[(Int, Int)] = List((1,0),(-1,0),(0,1),(0,-1))
|
||||
private val bishopDirs: List[(Int, Int)] = List((1,1),(1,-1),(-1,1),(-1,-1))
|
||||
private val queenDirs: List[(Int, Int)] = rookDirs ++ bishopDirs
|
||||
private val knightJumps: List[(Int, Int)] =
|
||||
// ── Direction vectors ──────────────────────────────────────────────
|
||||
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
|
||||
private val BishopDirs: List[(Int, Int)] = List((1, 1), (1, -1), (-1, 1), (-1, -1))
|
||||
private val QueenDirs: List[(Int, Int)] = RookDirs ++ BishopDirs
|
||||
private val KnightJumps: List[(Int, Int)] =
|
||||
List((2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2))
|
||||
|
||||
// ── Pawn configuration helpers ────────────────────────────────────────────
|
||||
// curried: fix color, get the forward direction for that color's pawns
|
||||
// ── Pawn configuration helpers ─────────────────────────────────────
|
||||
private def pawnForward(color: Color): Int = if color == Color.White then 1 else -1
|
||||
private def pawnStartRank(color: Color): Int = if color == Color.White then 1 else 6
|
||||
private def pawnPromoRank(color: Color): Int = if color == Color.White then 7 else 0
|
||||
|
||||
// ── Public API ────────────────────────────────────────────────────────────
|
||||
def candidateMoves(sit: Situation, sq: Square): List[Move] =
|
||||
sit.board.pieceAt(sq).fold(List.empty[Move]) { piece =>
|
||||
if piece.color != sit.turn then List.empty[Move]
|
||||
// ── Public API ─────────────────────────────────────────────────────
|
||||
|
||||
override def candidateMoves(context: GameContext, square: Square): List[Move] =
|
||||
context.board.pieceAt(square).fold(List.empty[Move]) { piece =>
|
||||
if piece.color != context.turn then List.empty[Move]
|
||||
else piece.pieceType match
|
||||
case PieceType.Pawn => pawnCandidates(sit, sq, piece.color)
|
||||
case PieceType.Knight => knightCandidates(sit, sq, piece.color)
|
||||
case PieceType.Bishop => slidingMoves(sit, sq, piece.color, bishopDirs)
|
||||
case PieceType.Rook => slidingMoves(sit, sq, piece.color, rookDirs)
|
||||
case PieceType.Queen => slidingMoves(sit, sq, piece.color, queenDirs)
|
||||
case PieceType.King => kingCandidates(sit, sq, piece.color)
|
||||
case PieceType.Pawn => pawnCandidates(context, square, piece.color)
|
||||
case PieceType.Knight => knightCandidates(context, square, piece.color)
|
||||
case PieceType.Bishop => slidingMoves(context, square, piece.color, BishopDirs)
|
||||
case PieceType.Rook => slidingMoves(context, square, piece.color, RookDirs)
|
||||
case PieceType.Queen => slidingMoves(context, square, piece.color, QueenDirs)
|
||||
case PieceType.King => kingCandidates(context, square, piece.color)
|
||||
}
|
||||
|
||||
def legalMoves(sit: Situation, sq: Square): List[Move] =
|
||||
candidateMoves(sit, sq).filter { move =>
|
||||
!leavesKingInCheck(sit, move) && !castlesThroughCheck(sit, move)
|
||||
override def legalMoves(context: GameContext, square: Square): List[Move] =
|
||||
candidateMoves(context, square).filter { move =>
|
||||
!leavesKingInCheck(context, move)
|
||||
}
|
||||
|
||||
def allLegalMoves(sit: Situation): List[Move] =
|
||||
Square.all.toList.flatMap(sq => legalMoves(sit, sq))
|
||||
override def allLegalMoves(context: GameContext): List[Move] =
|
||||
Square.all.flatMap(sq => legalMoves(context, sq)).toList
|
||||
|
||||
def isCheck(sit: Situation): Boolean =
|
||||
kingSquare(sit.board, sit.turn)
|
||||
.fold(false)(sq => isAttackedBy(sit.board, sq, sit.turn.opposite))
|
||||
override def isCheck(context: GameContext): Boolean =
|
||||
kingSquare(context.board, context.turn)
|
||||
.fold(false)(sq => isAttackedBy(context.board, sq, context.turn.opposite))
|
||||
|
||||
def isCheckmate(sit: Situation): Boolean =
|
||||
isCheck(sit) && allLegalMoves(sit).isEmpty
|
||||
override def isCheckmate(context: GameContext): Boolean =
|
||||
isCheck(context) && allLegalMoves(context).isEmpty
|
||||
|
||||
def isStalemate(sit: Situation): Boolean =
|
||||
!isCheck(sit) && allLegalMoves(sit).isEmpty
|
||||
override def isStalemate(context: GameContext): Boolean =
|
||||
!isCheck(context) && allLegalMoves(context).isEmpty
|
||||
|
||||
def isInsufficientMaterial(sit: Situation): Boolean =
|
||||
insufficientMaterial(sit.board)
|
||||
override def isInsufficientMaterial(context: GameContext): Boolean =
|
||||
insufficientMaterial(context.board)
|
||||
|
||||
def isFiftyMoveRule(sit: Situation): Boolean =
|
||||
sit.halfMoveClock >= 100
|
||||
override def isFiftyMoveRule(context: GameContext): Boolean =
|
||||
context.halfMoveClock >= 100
|
||||
|
||||
// ── Sliding pieces (Bishop, Rook, Queen) ─────────────────────────────────
|
||||
private def slidingMoves(sit: Situation, from: Square, color: Color, dirs: List[(Int, Int)]): List[Move] =
|
||||
dirs.flatMap(dir => castRay(sit.board, from, color, dir))
|
||||
// ── Sliding pieces (Bishop, Rook, Queen) ───────────────────────────
|
||||
|
||||
private def castRay(board: Board, from: Square, color: Color, dir: (Int, Int)): List[Move] =
|
||||
@annotation.tailrec
|
||||
private def slidingMoves(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dirs: List[(Int, Int)]
|
||||
): List[Move] =
|
||||
dirs.flatMap(dir => castRay(context.board, from, color, dir))
|
||||
|
||||
private def castRay(
|
||||
board: Board,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dir: (Int, Int)
|
||||
): List[Move] =
|
||||
@tailrec
|
||||
def loop(sq: Square, acc: List[Move]): List[Move] =
|
||||
sq.offset(dir._1, dir._2) match
|
||||
case None => acc
|
||||
case Some(next) =>
|
||||
board.pieceAt(next) match
|
||||
case None => loop(next, NormalMove(from, next) :: acc)
|
||||
case Some(p) if p.color != color => NormalMove(from, next) :: acc
|
||||
case None => loop(next, Move(from, next) :: acc)
|
||||
case Some(p) if p.color != color => Move(from, next) :: acc
|
||||
case Some(_) => acc
|
||||
loop(from, Nil).reverse
|
||||
|
||||
// ── Knight ────────────────────────────────────────────────────────────────
|
||||
private def knightCandidates(sit: Situation, from: Square, color: Color): List[Move] =
|
||||
knightJumps.flatMap { (df, dr) =>
|
||||
// ── Knight ─────────────────────────────────────────────────────────
|
||||
|
||||
private def knightCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
): List[Move] =
|
||||
KnightJumps.flatMap { (df, dr) =>
|
||||
from.offset(df, dr).flatMap { to =>
|
||||
sit.board.pieceAt(to) match
|
||||
context.board.pieceAt(to) match
|
||||
case Some(p) if p.color == color => None
|
||||
case _ => Some(NormalMove(from, to))
|
||||
case _ => Some(Move(from, to))
|
||||
}
|
||||
}
|
||||
|
||||
// ── King ──────────────────────────────────────────────────────────────────
|
||||
private def kingCandidates(sit: Situation, from: Square, color: Color): List[Move] =
|
||||
val steps = queenDirs.flatMap { (df, dr) =>
|
||||
from.offset(df, dr).flatMap { to =>
|
||||
sit.board.pieceAt(to) match
|
||||
case Some(p) if p.color == color => None
|
||||
case _ => Some(NormalMove(from, to))
|
||||
}
|
||||
}
|
||||
steps ++ castlingCandidates(sit, from, color)
|
||||
// ── King ───────────────────────────────────────────────────────────
|
||||
|
||||
// ── Pawn ──────────────────────────────────────────────────────────────────
|
||||
private def pawnCandidates(sit: Situation, from: Square, color: Color): List[Move] =
|
||||
private def kingCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
): List[Move] =
|
||||
val steps = QueenDirs.flatMap { (df, dr) =>
|
||||
from.offset(df, dr).flatMap { to =>
|
||||
context.board.pieceAt(to) match
|
||||
case Some(p) if p.color == color => None
|
||||
case _ => Some(Move(from, to))
|
||||
}
|
||||
}
|
||||
steps ++ castlingCandidates(context, from, color)
|
||||
|
||||
// ── Castling ───────────────────────────────────────────────────────
|
||||
|
||||
private def castlingCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
): List[Move] =
|
||||
color match
|
||||
case Color.White => whiteCastles(context, from)
|
||||
case Color.Black => blackCastles(context, from)
|
||||
|
||||
private def whiteCastles(context: GameContext, from: Square): List[Move] =
|
||||
val expected = Square.fromAlgebraic("e1").getOrElse(from)
|
||||
if from != expected then List.empty
|
||||
else
|
||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||
|
||||
// King-side castling
|
||||
if context.castlingRights.whiteKingSide then
|
||||
val clearSqs = List("f1", "g1").flatMap(Square.fromAlgebraic)
|
||||
if squaresEmpty(context.board, clearSqs) then
|
||||
for
|
||||
kf <- Square.fromAlgebraic("e1")
|
||||
kt <- Square.fromAlgebraic("g1")
|
||||
do moves += Move(kf, kt, MoveType.CastleKingside)
|
||||
|
||||
// Queen-side castling
|
||||
if context.castlingRights.whiteQueenSide then
|
||||
val clearSqs = List("b1", "c1", "d1").flatMap(Square.fromAlgebraic)
|
||||
if squaresEmpty(context.board, clearSqs) then
|
||||
for
|
||||
kf <- Square.fromAlgebraic("e1")
|
||||
kt <- Square.fromAlgebraic("c1")
|
||||
do moves += Move(kf, kt, MoveType.CastleQueenside)
|
||||
|
||||
moves.toList
|
||||
|
||||
private def blackCastles(context: GameContext, from: Square): List[Move] =
|
||||
val expected = Square.fromAlgebraic("e8").getOrElse(from)
|
||||
if from != expected then List.empty
|
||||
else
|
||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||
|
||||
// King-side castling
|
||||
if context.castlingRights.blackKingSide then
|
||||
val clearSqs = List("f8", "g8").flatMap(Square.fromAlgebraic)
|
||||
if squaresEmpty(context.board, clearSqs) then
|
||||
for
|
||||
kf <- Square.fromAlgebraic("e8")
|
||||
kt <- Square.fromAlgebraic("g8")
|
||||
do moves += Move(kf, kt, MoveType.CastleKingside)
|
||||
|
||||
// Queen-side castling
|
||||
if context.castlingRights.blackQueenSide then
|
||||
val clearSqs = List("b8", "c8", "d8").flatMap(Square.fromAlgebraic)
|
||||
if squaresEmpty(context.board, clearSqs) then
|
||||
for
|
||||
kf <- Square.fromAlgebraic("e8")
|
||||
kt <- Square.fromAlgebraic("c8")
|
||||
do moves += Move(kf, kt, MoveType.CastleQueenside)
|
||||
|
||||
moves.toList
|
||||
|
||||
private def squaresEmpty(board: Board, squares: List[Square]): Boolean =
|
||||
squares.forall(sq => board.pieceAt(sq).isEmpty)
|
||||
|
||||
// ── Pawn ───────────────────────────────────────────────────────────
|
||||
|
||||
private def pawnCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
): List[Move] =
|
||||
val fwd = pawnForward(color)
|
||||
val startRank = pawnStartRank(color)
|
||||
val promoRank = pawnPromoRank(color)
|
||||
|
||||
val single = from.offset(0, fwd).filter(to => sit.board.pieceAt(to).isEmpty)
|
||||
|
||||
val double = Option.when(from.rank.toInt == startRank) {
|
||||
val single = from.offset(0, fwd).filter(to => context.board.pieceAt(to).isEmpty)
|
||||
val double = Option.when(from.rank.ordinal == startRank) {
|
||||
from.offset(0, fwd).flatMap { mid =>
|
||||
Option.when(sit.board.pieceAt(mid).isEmpty) {
|
||||
from.offset(0, fwd * 2).filter(to => sit.board.pieceAt(to).isEmpty)
|
||||
Option.when(context.board.pieceAt(mid).isEmpty) {
|
||||
from.offset(0, fwd * 2).filter(to => context.board.pieceAt(to).isEmpty)
|
||||
}.flatten
|
||||
}
|
||||
}.flatten
|
||||
|
||||
val diagonalCaptures = List(-1, 1).flatMap { df =>
|
||||
from.offset(df, fwd).flatMap { to =>
|
||||
sit.board.pieceAt(to).filter(_.color != color).map(_ => to)
|
||||
context.board.pieceAt(to).filter(_.color != color).map(_ => to)
|
||||
}
|
||||
}
|
||||
|
||||
val epCaptures: List[EnPassantMove] = sit.enPassantSquare.toList.flatMap { epSq =>
|
||||
val epCaptures: List[Move] = context.enPassantSquare.toList.flatMap { epSq =>
|
||||
List(-1, 1).flatMap { df =>
|
||||
from.offset(df, fwd).filter(_ == epSq).map { to =>
|
||||
EnPassantMove(from, to, Square(to.file, from.rank))
|
||||
Move(from, epSq, MoveType.EnPassant)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def toMoves(dest: Square): List[NormalMove] =
|
||||
if dest.rank.toInt == promoRank then
|
||||
List(PieceType.Queen, PieceType.Rook, PieceType.Bishop, PieceType.Knight)
|
||||
.map(pt => NormalMove(from, dest, Some(pt)))
|
||||
else List(NormalMove(from, dest))
|
||||
def toMoves(dest: Square): List[Move] =
|
||||
if dest.rank.ordinal == promoRank then
|
||||
List(
|
||||
PromotionPiece.Queen, PromotionPiece.Rook,
|
||||
PromotionPiece.Bishop, PromotionPiece.Knight
|
||||
).map(pt => Move(from, dest, MoveType.Promotion(pt)))
|
||||
else List(Move(from, dest))
|
||||
|
||||
val stepSquares = single.toList ++ double.toList
|
||||
val stepMoves = stepSquares.flatMap(toMoves)
|
||||
val captureMoves = diagonalCaptures.flatMap(toMoves)
|
||||
stepMoves ++ captureMoves ++ epCaptures
|
||||
|
||||
// ── Castling ──────────────────────────────────────────────────────────────
|
||||
private def castlingCandidates(sit: Situation, from: Square, color: Color): List[CastlingMove] =
|
||||
color match
|
||||
case Color.White => whiteCastles(sit, from)
|
||||
case Color.Black => blackCastles(sit, from)
|
||||
// ── Check detection ────────────────────────────────────────────────
|
||||
|
||||
private def squaresEmpty(board: Board, squares: List[String]): Boolean =
|
||||
squares.forall(alg => Square.fromAlgebraic(alg).fold(false)(sq => board.pieceAt(sq).isEmpty))
|
||||
|
||||
private def castleMoves(
|
||||
sit: Situation,
|
||||
from: Square,
|
||||
kingSideRight: Boolean,
|
||||
queenSideRight: Boolean,
|
||||
kingSquareAlg: String,
|
||||
kingSideSquares: List[String],
|
||||
queenSideSquares: List[String],
|
||||
kingSideCoords: (String, String, String, String),
|
||||
queenSideCoords: (String, String, String, String)
|
||||
): List[CastlingMove] =
|
||||
val expected = Square.fromAlgebraic(kingSquareAlg).getOrElse(from)
|
||||
|
||||
def makeCastle(rights: Boolean, clearSquares: List[String], coords: (String, String, String, String)): Option[CastlingMove] =
|
||||
for
|
||||
_ <- Option.when(rights && from == expected && squaresEmpty(sit.board, clearSquares))(())
|
||||
kf <- Square.fromAlgebraic(coords._1)
|
||||
kt <- Square.fromAlgebraic(coords._2)
|
||||
rf <- Square.fromAlgebraic(coords._3)
|
||||
rt <- Square.fromAlgebraic(coords._4)
|
||||
yield CastlingMove(kf, kt, rf, rt)
|
||||
|
||||
List(
|
||||
makeCastle(kingSideRight, kingSideSquares, kingSideCoords),
|
||||
makeCastle(queenSideRight, queenSideSquares, queenSideCoords)
|
||||
).flatten
|
||||
|
||||
private def whiteCastles(sit: Situation, from: Square): List[CastlingMove] =
|
||||
castleMoves(sit, from,
|
||||
sit.castlingRights.whiteKingSide, sit.castlingRights.whiteQueenSide,
|
||||
"e1",
|
||||
List("f1", "g1"), List("b1", "c1", "d1"),
|
||||
("e1", "g1", "h1", "f1"), ("e1", "c1", "a1", "d1")
|
||||
)
|
||||
|
||||
private def blackCastles(sit: Situation, from: Square): List[CastlingMove] =
|
||||
castleMoves(sit, from,
|
||||
sit.castlingRights.blackKingSide, sit.castlingRights.blackQueenSide,
|
||||
"e8",
|
||||
List("f8", "g8"), List("b8", "c8", "d8"),
|
||||
("e8", "g8", "h8", "f8"), ("e8", "c8", "a8", "d8")
|
||||
)
|
||||
|
||||
// ── Check detection ───────────────────────────────────────────────────────
|
||||
private def kingSquare(board: Board, color: Color): Option[Square] =
|
||||
Square.all.find(sq => board.pieceAt(sq).contains(Piece(color, PieceType.King)))
|
||||
Square.all.find(sq =>
|
||||
board.pieceAt(sq).exists(p => p.color == color && p.pieceType == PieceType.King)
|
||||
)
|
||||
|
||||
private def isAttackedBy(board: Board, target: Square, attacker: Color): Boolean =
|
||||
Square.all.exists { sq =>
|
||||
@@ -201,16 +251,16 @@ object StandardRules extends RuleSet:
|
||||
case PieceType.Pawn =>
|
||||
from.offset(-1, fwd).contains(target) || from.offset(1, fwd).contains(target)
|
||||
case PieceType.Knight =>
|
||||
knightJumps.exists { (df, dr) => from.offset(df, dr).contains(target) }
|
||||
case PieceType.Bishop => rayReaches(board, from, bishopDirs, target)
|
||||
case PieceType.Rook => rayReaches(board, from, rookDirs, target)
|
||||
case PieceType.Queen => rayReaches(board, from, queenDirs, target)
|
||||
KnightJumps.exists { (df, dr) => from.offset(df, dr).contains(target) }
|
||||
case PieceType.Bishop => rayReaches(board, from, BishopDirs, target)
|
||||
case PieceType.Rook => rayReaches(board, from, RookDirs, target)
|
||||
case PieceType.Queen => rayReaches(board, from, QueenDirs, target)
|
||||
case PieceType.King =>
|
||||
queenDirs.exists { (df, dr) => from.offset(df, dr).contains(target) }
|
||||
QueenDirs.exists { (df, dr) => from.offset(df, dr).contains(target) }
|
||||
|
||||
private def rayReaches(board: Board, from: Square, dirs: List[(Int, Int)], target: Square): Boolean =
|
||||
dirs.exists { dir =>
|
||||
@annotation.tailrec
|
||||
@tailrec
|
||||
def loop(sq: Square): Boolean = sq.offset(dir._1, dir._2) match
|
||||
case None => false
|
||||
case Some(next) if next == target => true
|
||||
@@ -219,24 +269,16 @@ object StandardRules extends RuleSet:
|
||||
loop(from)
|
||||
}
|
||||
|
||||
private def leavesKingInCheck(sit: Situation, move: Move): Boolean =
|
||||
val nextBoard = sit.board.applyMove(move)
|
||||
kingSquare(nextBoard, sit.turn).fold(false) { sq =>
|
||||
isAttackedBy(nextBoard, sq, sit.turn.opposite)
|
||||
}
|
||||
private def leavesKingInCheck(context: GameContext, move: Move): Boolean =
|
||||
val nextBoard = context.board.applyMove(move)
|
||||
val nextContext = context.withBoard(nextBoard)
|
||||
isCheck(nextContext)
|
||||
|
||||
private def castlesThroughCheck(sit: Situation, move: Move): Boolean = move match
|
||||
case CastlingMove(from, to, _, _) =>
|
||||
val passSq = if to.file.toInt > from.file.toInt
|
||||
then from.offset(1, 0)
|
||||
else from.offset(-1, 0)
|
||||
isCheck(sit) || passSq.fold(false)(sq => isAttackedBy(sit.board, sq, sit.turn.opposite))
|
||||
case _ => false
|
||||
// ── Insufficient material ──────────────────────────────────────────
|
||||
|
||||
// ── Insufficient material ─────────────────────────────────────────────────
|
||||
private def insufficientMaterial(board: Board): Boolean =
|
||||
val nonKings = board.pieces.values.iterator.toList.filter(_.pieceType != PieceType.King)
|
||||
nonKings match
|
||||
val pieces = board.pieces.values.toList.filter(_.pieceType != PieceType.King)
|
||||
pieces match
|
||||
case Nil => true
|
||||
case List(p) if p.pieceType == PieceType.Bishop || p.pieceType == PieceType.Knight => true
|
||||
case List(p1, p2)
|
||||
|
||||
Reference in New Issue
Block a user