diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/CastleSide.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/CastleSide.scala deleted file mode 100644 index 6d607fd..0000000 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/CastleSide.scala +++ /dev/null @@ -1,23 +0,0 @@ -package de.nowchess.chess.logic - -import de.nowchess.api.board.* - -enum CastleSide: - case Kingside, Queenside - -extension (b: Board) - def withCastle(color: Color, side: CastleSide): Board = - val rank = if color == Color.White then Rank.R1 else Rank.R8 - val kingFrom = Square(File.E, rank) - val (kingTo, rookFrom, rookTo) = side match - case CastleSide.Kingside => - (Square(File.G, rank), Square(File.H, rank), Square(File.F, rank)) - case CastleSide.Queenside => - (Square(File.C, rank), Square(File.A, rank), Square(File.D, rank)) - - val king = b.pieceAt(kingFrom).get - val rook = b.pieceAt(rookFrom).get - - b.removed(kingFrom).removed(rookFrom) - .updated(kingTo, king) - .updated(rookTo, rook) diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/CastlingRightsCalculator.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/CastlingRightsCalculator.scala deleted file mode 100644 index 47b2978..0000000 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/CastlingRightsCalculator.scala +++ /dev/null @@ -1,31 +0,0 @@ -package de.nowchess.chess.logic - -import de.nowchess.api.board.{Color, File, Rank, Square} -import de.nowchess.api.game.{CastlingRights, GameHistory} - -/** Derives castling rights from move history. */ -object CastlingRightsCalculator: - - def deriveCastlingRights(history: GameHistory, color: Color): CastlingRights = - val (kingRow, kingsideRookFile, queensideRookFile) = color match - case Color.White => (Rank.R1, File.H, File.A) - case Color.Black => (Rank.R8, File.H, File.A) - - // Check if king has moved - val kingHasMoved = history.moves.exists: move => - move.from == Square(File.E, kingRow) || move.castleSide.isDefined - - if kingHasMoved then - CastlingRights.None - else - // Check if kingside rook has moved or was captured - val kingsideLost = history.moves.exists: move => - move.from == Square(kingsideRookFile, kingRow) || - move.to == Square(kingsideRookFile, kingRow) - - // Check if queenside rook has moved or was captured - val queensideLost = history.moves.exists: move => - move.from == Square(queensideRookFile, kingRow) || - move.to == Square(queensideRookFile, kingRow) - - CastlingRights(kingSide = !kingsideLost, queenSide = !queensideLost) diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/EnPassantCalculator.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/EnPassantCalculator.scala deleted file mode 100644 index 5e53e3c..0000000 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/EnPassantCalculator.scala +++ /dev/null @@ -1,33 +0,0 @@ -package de.nowchess.chess.logic - -import de.nowchess.api.board.* -import de.nowchess.api.game.GameHistory - -object EnPassantCalculator: - - /** Returns the en passant target square if the last move was a double pawn push. - * The target is the square the pawn passed through (e.g. e2→e4 yields e3). - */ - def enPassantTarget(board: Board, history: GameHistory): Option[Square] = - history.moves.lastOption.flatMap: move => - val rankDiff = move.to.rank.ordinal - move.from.rank.ordinal - val isDoublePush = math.abs(rankDiff) == 2 - val isPawn = board.pieceAt(move.to).exists(_.pieceType == PieceType.Pawn) - if isDoublePush && isPawn then - val midRankIdx = move.from.rank.ordinal + rankDiff / 2 - Some(Square(move.to.file, Rank.values(midRankIdx))) - else None - - /** True if moving from→to is an en passant capture. */ - def isEnPassant(board: Board, history: GameHistory, from: Square, to: Square): Boolean = - board.pieceAt(from).exists(_.pieceType == PieceType.Pawn) && - enPassantTarget(board, history).contains(to) && - math.abs(to.file.ordinal - from.file.ordinal) == 1 - - /** Returns the square of the pawn to remove when an en passant capture lands on `to`. - * White captures upward → captured pawn is one rank below `to`. - * Black captures downward → captured pawn is one rank above `to`. - */ - def capturedPawnSquare(to: Square, color: Color): Square = - val capturedRankIdx = to.rank.ordinal + (if color == Color.White then -1 else 1) - Square(to.file, Rank.values(capturedRankIdx)) diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala deleted file mode 100644 index ea1c0db..0000000 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala +++ /dev/null @@ -1,47 +0,0 @@ -package de.nowchess.chess.logic - -import de.nowchess.api.board.* -import de.nowchess.api.game.GameHistory - -enum PositionStatus: - case Normal, InCheck, Mated, Drawn - -object GameRules: - - /** True if `color`'s king is under attack on this board. */ - def isInCheck(board: Board, color: Color): Boolean = - board.pieces - .collectFirst { case (sq, p) if p.color == color && p.pieceType == PieceType.King => sq } - .exists { kingSq => - board.pieces.exists { case (sq, piece) => - piece.color != color && - MoveValidator.legalTargets(board, sq).contains(kingSq) - } - } - - /** All (from, to) moves for `color` that do not leave their own king in check. */ - def legalMoves(board: Board, history: GameHistory, color: Color): Set[(Square, Square)] = - board.pieces - .collect { case (from, piece) if piece.color == color => from } - .flatMap { from => - MoveValidator.legalTargets(board, history, from) // context-aware: includes castling - .filter { to => - val newBoard = - if MoveValidator.isCastle(board, from, to) then - board.withCastle(color, MoveValidator.castleSide(from, to)) - else - board.withMove(from, to)._1 - !isInCheck(newBoard, color) - } - .map(to => from -> to) - } - .toSet - - /** Position status for the side whose turn it is (`color`). */ - def gameStatus(board: Board, history: GameHistory, color: Color): PositionStatus = - val moves = legalMoves(board, history, color) - val inCheck = isInCheck(board, color) - if moves.isEmpty && inCheck then PositionStatus.Mated - else if moves.isEmpty then PositionStatus.Drawn - else if inCheck then PositionStatus.InCheck - else PositionStatus.Normal diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala deleted file mode 100644 index f93d051..0000000 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala +++ /dev/null @@ -1,184 +0,0 @@ -package de.nowchess.chess.logic - -import de.nowchess.api.board.* -import de.nowchess.chess.logic.CastleSide -import de.nowchess.api.game.GameHistory - -object MoveValidator: - - /** Returns true if the move is geometrically legal for the piece on `from`, - * ignoring check/pin but respecting: - * - correct movement pattern for the piece type - * - cannot capture own pieces - * - sliding pieces (bishop, rook, queen) are blocked by intervening pieces - */ - def isLegal(board: Board, from: Square, to: Square): Boolean = - legalTargets(board, from).contains(to) - - /** All squares a piece on `from` can legally move to (same rules as isLegal). */ - def legalTargets(board: Board, from: Square): Set[Square] = - board.pieceAt(from) match - case None => Set.empty - case Some(piece) => - piece.pieceType match - case PieceType.Pawn => pawnTargets(board, from, piece.color) - case PieceType.Knight => knightTargets(board, from, piece.color) - case PieceType.Bishop => slide(board, from, piece.color, diagonalDeltas) - case PieceType.Rook => slide(board, from, piece.color, orthogonalDeltas) - case PieceType.Queen => slide(board, from, piece.color, diagonalDeltas ++ orthogonalDeltas) - case PieceType.King => kingTargets(board, from, piece.color) - - // ── helpers ──────────────────────────────────────────────────────────────── - - private val diagonalDeltas: List[(Int, Int)] = List((1, 1), (1, -1), (-1, 1), (-1, -1)) - private val orthogonalDeltas: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1)) - private val knightDeltas: List[(Int, Int)] = - List((1, 2), (1, -2), (-1, 2), (-1, -2), (2, 1), (2, -1), (-2, 1), (-2, -1)) - - /** Try to construct a Square from integer file/rank indices (0-based). */ - private def squareAt(fileIdx: Int, rankIdx: Int): Option[Square] = - Option.when(fileIdx >= 0 && fileIdx <= 7 && rankIdx >= 0 && rankIdx <= 7)( - Square(File.values(fileIdx), Rank.values(rankIdx)) - ) - - /** True when `sq` is occupied by a piece of `color`. */ - private def isOwnPiece(board: Board, sq: Square, color: Color): Boolean = - board.pieceAt(sq).exists(_.color == color) - - /** True when `sq` is occupied by a piece of the opposite color. */ - private def isEnemyPiece(board: Board, sq: Square, color: Color): Boolean = - board.pieceAt(sq).exists(_.color != color) - - /** Sliding move generation along a list of direction deltas. - * Each direction continues until the board edge, an own piece, or the first - * enemy piece (which is included as a capture target). - */ - private def slide(board: Board, from: Square, color: Color, deltas: List[(Int, Int)]): Set[Square] = - val fi = from.file.ordinal - val ri = from.rank.ordinal - deltas.flatMap: (df, dr) => - Iterator - .iterate((fi + df, ri + dr)) { case (f, r) => (f + df, r + dr) } - .takeWhile { case (f, r) => f >= 0 && f <= 7 && r >= 0 && r <= 7 } - .map { case (f, r) => Square(File.values(f), Rank.values(r)) } - .foldLeft((List.empty[Square], false)): - case ((acc, stopped), sq) => - if stopped then (acc, true) - else if isOwnPiece(board, sq, color) then (acc, true) // blocked — stop, no capture - else if isEnemyPiece(board, sq, color) then (acc :+ sq, true) // capture — stop after - else (acc :+ sq, false) // empty — continue - ._1 - .toSet - - private def pawnTargets(board: Board, from: Square, color: Color): Set[Square] = - val fi = from.file.ordinal - val ri = from.rank.ordinal - val dir = if color == Color.White then 1 else -1 - val startRank = if color == Color.White then Rank.R2.ordinal else Rank.R7.ordinal - - val oneStep = squareAt(fi, ri + dir) - - // Forward one square (only if empty) - val forward1: Set[Square] = oneStep match - case Some(sq) if board.pieceAt(sq).isEmpty => Set(sq) - case _ => Set.empty - - // Forward two squares from starting rank (only if both intermediate squares are empty) - val forward2: Set[Square] = - if ri == startRank && forward1.nonEmpty then - squareAt(fi, ri + 2 * dir) match - case Some(sq) if board.pieceAt(sq).isEmpty => Set(sq) - case _ => Set.empty - else Set.empty - - // Diagonal captures (only if enemy piece present) - val captures: Set[Square] = - List(-1, 1).flatMap: df => - squareAt(fi + df, ri + dir).filter(sq => isEnemyPiece(board, sq, color)) - .toSet - - forward1 ++ forward2 ++ captures - - private def knightTargets(board: Board, from: Square, color: Color): Set[Square] = - val fi = from.file.ordinal - val ri = from.rank.ordinal - knightDeltas.flatMap: (df, dr) => - squareAt(fi + df, ri + dr).filterNot(sq => isOwnPiece(board, sq, color)) - .toSet - - private def kingTargets(board: Board, from: Square, color: Color): Set[Square] = - val fi = from.file.ordinal - val ri = from.rank.ordinal - (diagonalDeltas ++ orthogonalDeltas).flatMap: (df, dr) => - squareAt(fi + df, ri + dr).filterNot(sq => isOwnPiece(board, sq, color)) - .toSet - - // ── Castling helpers ──────────────────────────────────────────────────────── - - private def isAttackedBy(board: Board, sq: Square, attackerColor: Color): Boolean = - board.pieces.exists { case (from, piece) => - piece.color == attackerColor && legalTargets(board, from).contains(sq) - } - - def isCastle(board: Board, from: Square, to: Square): Boolean = - board.pieceAt(from).exists(_.pieceType == PieceType.King) && - math.abs(to.file.ordinal - from.file.ordinal) == 2 - - def castleSide(from: Square, to: Square): CastleSide = - if to.file.ordinal > from.file.ordinal then CastleSide.Kingside else CastleSide.Queenside - - def castlingTargets(board: Board, history: GameHistory, color: Color): Set[Square] = - val rights = CastlingRightsCalculator.deriveCastlingRights(history, color) - val rank = if color == Color.White then Rank.R1 else Rank.R8 - val kingSq = Square(File.E, rank) - val enemy = color.opposite - - if !board.pieceAt(kingSq).contains(Piece(color, PieceType.King)) || - GameRules.isInCheck(board, color) then Set.empty - else - val kingsideSq = Option.when( - rights.kingSide && - board.pieceAt(Square(File.H, rank)).contains(Piece(color, PieceType.Rook)) && - List(Square(File.F, rank), Square(File.G, rank)).forall(s => board.pieceAt(s).isEmpty) && - !List(Square(File.F, rank), Square(File.G, rank)).exists(s => isAttackedBy(board, s, enemy)) - )(Square(File.G, rank)) - - val queensideSq = Option.when( - rights.queenSide && - board.pieceAt(Square(File.A, rank)).contains(Piece(color, PieceType.Rook)) && - List(Square(File.B, rank), Square(File.C, rank), Square(File.D, rank)).forall(s => board.pieceAt(s).isEmpty) && - !List(Square(File.D, rank), Square(File.C, rank)).exists(s => isAttackedBy(board, s, enemy)) - )(Square(File.C, rank)) - - kingsideSq.toSet ++ queensideSq.toSet - - def legalTargets(board: Board, history: GameHistory, from: Square): Set[Square] = - board.pieceAt(from) match - case Some(piece) if piece.pieceType == PieceType.King => - legalTargets(board, from) ++ castlingTargets(board, history, piece.color) - case Some(piece) if piece.pieceType == PieceType.Pawn => - pawnTargets(board, history, from, piece.color) - case _ => - legalTargets(board, from) - - private def pawnTargets(board: Board, history: GameHistory, from: Square, color: Color): Set[Square] = - val existing = pawnTargets(board, from, color) - val fi = from.file.ordinal - val ri = from.rank.ordinal - val dir = if color == Color.White then 1 else -1 - val epCapture: Set[Square] = - EnPassantCalculator.enPassantTarget(board, history).filter: target => - squareAt(fi - 1, ri + dir).contains(target) || squareAt(fi + 1, ri + dir).contains(target) - .toSet - existing ++ epCapture - - def isLegal(board: Board, history: GameHistory, from: Square, to: Square): Boolean = - legalTargets(board, history, from).contains(to) - - /** Returns true if the piece on `from` is a pawn moving to its back rank (promotion). */ - def isPromotionMove(board: Board, from: Square, to: Square): Boolean = - board.pieceAt(from) match - case Some(Piece(_, PieceType.Pawn)) => - (from.rank == Rank.R7 && to.rank == Rank.R8) || - (from.rank == Rank.R2 && to.rank == Rank.R1) - case _ => false