This commit is contained in:
@@ -7,11 +7,11 @@ object Board:
|
||||
def apply(pieces: Map[Square, Piece]): Board = pieces
|
||||
|
||||
extension (b: Board)
|
||||
def pieceAt(sq: Square): Option[Piece] = b.get(sq)
|
||||
def pieceAt(sq: Square): Option[Piece] = b.get(sq)
|
||||
def updated(sq: Square, piece: Piece): Board = b.updated(sq, piece)
|
||||
def removed(sq: Square): Board = b.removed(sq)
|
||||
def removed(sq: Square): Board = b.removed(sq)
|
||||
def withMove(from: Square, to: Square): (Board, Option[Piece]) =
|
||||
val captured = b.get(to)
|
||||
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 =
|
||||
@@ -21,8 +21,14 @@ object Board:
|
||||
|
||||
val initial: Board =
|
||||
val backRank: Vector[PieceType] = Vector(
|
||||
PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen,
|
||||
PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook
|
||||
PieceType.Rook,
|
||||
PieceType.Knight,
|
||||
PieceType.Bishop,
|
||||
PieceType.Queen,
|
||||
PieceType.King,
|
||||
PieceType.Bishop,
|
||||
PieceType.Knight,
|
||||
PieceType.Rook,
|
||||
)
|
||||
val entries = for
|
||||
fileIdx <- 0 until 8
|
||||
@@ -30,7 +36,7 @@ object Board:
|
||||
(Color.White, Rank.R1, backRank(fileIdx)),
|
||||
(Color.White, Rank.R2, PieceType.Pawn),
|
||||
(Color.Black, Rank.R8, backRank(fileIdx)),
|
||||
(Color.Black, Rank.R7, PieceType.Pawn)
|
||||
(Color.Black, Rank.R7, PieceType.Pawn),
|
||||
)
|
||||
yield Square(File.values(fileIdx), rank) -> Piece(color, pieceType)
|
||||
Board(entries.toMap)
|
||||
|
||||
@@ -1,50 +1,48 @@
|
||||
package de.nowchess.api.board
|
||||
|
||||
/**
|
||||
* Unified castling rights tracker for all four sides.
|
||||
* Tracks whether castling is still available for each side and direction.
|
||||
*
|
||||
* @param whiteKingSide White's king-side castling (0-0) still legally available
|
||||
* @param whiteQueenSide White's queen-side castling (0-0-0) still legally available
|
||||
* @param blackKingSide Black's king-side castling (0-0) still legally available
|
||||
* @param blackQueenSide Black's queen-side castling (0-0-0) still legally available
|
||||
*/
|
||||
/** Unified castling rights tracker for all four sides. Tracks whether castling is still available for each side and
|
||||
* direction.
|
||||
*
|
||||
* @param whiteKingSide
|
||||
* White's king-side castling (0-0) still legally available
|
||||
* @param whiteQueenSide
|
||||
* White's queen-side castling (0-0-0) still legally available
|
||||
* @param blackKingSide
|
||||
* Black's king-side castling (0-0) still legally available
|
||||
* @param blackQueenSide
|
||||
* Black's queen-side castling (0-0-0) still legally available
|
||||
*/
|
||||
final case class CastlingRights(
|
||||
whiteKingSide: Boolean,
|
||||
whiteQueenSide: Boolean,
|
||||
blackKingSide: Boolean,
|
||||
blackQueenSide: Boolean
|
||||
whiteKingSide: Boolean,
|
||||
whiteQueenSide: Boolean,
|
||||
blackKingSide: Boolean,
|
||||
blackQueenSide: Boolean,
|
||||
):
|
||||
/**
|
||||
* Check if either side has any castling rights remaining.
|
||||
*/
|
||||
/** Check if either side has any castling rights remaining.
|
||||
*/
|
||||
def hasAnyRights: Boolean =
|
||||
whiteKingSide || whiteQueenSide || blackKingSide || blackQueenSide
|
||||
|
||||
/**
|
||||
* Check if a specific color has any castling rights remaining.
|
||||
*/
|
||||
/** Check if a specific color has any castling rights remaining.
|
||||
*/
|
||||
def hasRights(color: Color): Boolean = color match
|
||||
case Color.White => whiteKingSide || whiteQueenSide
|
||||
case Color.Black => blackKingSide || blackQueenSide
|
||||
|
||||
/**
|
||||
* Revoke all castling rights for a specific color.
|
||||
*/
|
||||
/** Revoke all castling rights for a specific color.
|
||||
*/
|
||||
def revokeColor(color: Color): CastlingRights = color match
|
||||
case Color.White => copy(whiteKingSide = false, whiteQueenSide = false)
|
||||
case Color.Black => copy(blackKingSide = false, blackQueenSide = false)
|
||||
|
||||
/**
|
||||
* Revoke a specific castling right.
|
||||
*/
|
||||
/** Revoke a specific castling right.
|
||||
*/
|
||||
def revokeKingSide(color: Color): CastlingRights = color match
|
||||
case Color.White => copy(whiteKingSide = false)
|
||||
case Color.Black => copy(blackKingSide = false)
|
||||
|
||||
/**
|
||||
* Revoke a specific castling right.
|
||||
*/
|
||||
/** Revoke a specific castling right.
|
||||
*/
|
||||
def revokeQueenSide(color: Color): CastlingRights = color match
|
||||
case Color.White => copy(whiteQueenSide = false)
|
||||
case Color.Black => copy(blackQueenSide = false)
|
||||
@@ -55,7 +53,7 @@ object CastlingRights:
|
||||
whiteKingSide = false,
|
||||
whiteQueenSide = false,
|
||||
blackKingSide = false,
|
||||
blackQueenSide = false
|
||||
blackQueenSide = false,
|
||||
)
|
||||
|
||||
/** All castling rights available. */
|
||||
@@ -63,7 +61,7 @@ object CastlingRights:
|
||||
whiteKingSide = true,
|
||||
whiteQueenSide = true,
|
||||
blackKingSide = true,
|
||||
blackQueenSide = true
|
||||
blackQueenSide = true,
|
||||
)
|
||||
|
||||
/** Standard starting position castling rights (both sides can castle both ways). */
|
||||
|
||||
@@ -5,16 +5,16 @@ final case class Piece(color: Color, pieceType: PieceType)
|
||||
|
||||
object Piece:
|
||||
// Convenience constructors
|
||||
val WhitePawn: Piece = Piece(Color.White, PieceType.Pawn)
|
||||
val WhitePawn: Piece = Piece(Color.White, PieceType.Pawn)
|
||||
val WhiteKnight: Piece = Piece(Color.White, PieceType.Knight)
|
||||
val WhiteBishop: Piece = Piece(Color.White, PieceType.Bishop)
|
||||
val WhiteRook: Piece = Piece(Color.White, PieceType.Rook)
|
||||
val WhiteQueen: Piece = Piece(Color.White, PieceType.Queen)
|
||||
val WhiteKing: Piece = Piece(Color.White, PieceType.King)
|
||||
val WhiteRook: Piece = Piece(Color.White, PieceType.Rook)
|
||||
val WhiteQueen: Piece = Piece(Color.White, PieceType.Queen)
|
||||
val WhiteKing: Piece = Piece(Color.White, PieceType.King)
|
||||
|
||||
val BlackPawn: Piece = Piece(Color.Black, PieceType.Pawn)
|
||||
val BlackPawn: Piece = Piece(Color.Black, PieceType.Pawn)
|
||||
val BlackKnight: Piece = Piece(Color.Black, PieceType.Knight)
|
||||
val BlackBishop: Piece = Piece(Color.Black, PieceType.Bishop)
|
||||
val BlackRook: Piece = Piece(Color.Black, PieceType.Rook)
|
||||
val BlackQueen: Piece = Piece(Color.Black, PieceType.Queen)
|
||||
val BlackKing: Piece = Piece(Color.Black, PieceType.King)
|
||||
val BlackRook: Piece = Piece(Color.Black, PieceType.Rook)
|
||||
val BlackQueen: Piece = Piece(Color.Black, PieceType.Queen)
|
||||
val BlackKing: Piece = Piece(Color.Black, PieceType.King)
|
||||
|
||||
@@ -1,43 +1,38 @@
|
||||
package de.nowchess.api.board
|
||||
|
||||
/**
|
||||
* A file (column) on the chess board, a–h.
|
||||
* Ordinal values 0–7 correspond to a–h.
|
||||
*/
|
||||
/** A file (column) on the chess board, a–h. Ordinal values 0–7 correspond to a–h.
|
||||
*/
|
||||
enum File:
|
||||
case A, B, C, D, E, F, G, H
|
||||
|
||||
/**
|
||||
* A rank (row) on the chess board, 1–8.
|
||||
* Ordinal values 0–7 correspond to ranks 1–8.
|
||||
*/
|
||||
/** A rank (row) on the chess board, 1–8. Ordinal values 0–7 correspond to ranks 1–8.
|
||||
*/
|
||||
enum Rank:
|
||||
case R1, R2, R3, R4, R5, R6, R7, R8
|
||||
|
||||
/**
|
||||
* A unique square on the board, identified by its file and rank.
|
||||
*
|
||||
* @param file the column, a–h
|
||||
* @param rank the row, 1–8
|
||||
*/
|
||||
/** A unique square on the board, identified by its file and rank.
|
||||
*
|
||||
* @param file
|
||||
* the column, a–h
|
||||
* @param rank
|
||||
* the row, 1–8
|
||||
*/
|
||||
final case class Square(file: File, rank: Rank):
|
||||
/** Algebraic notation string, e.g. "e4". */
|
||||
override def toString: String =
|
||||
s"${file.toString.toLowerCase}${rank.ordinal + 1}"
|
||||
|
||||
object Square:
|
||||
/** Parse a square from algebraic notation (e.g. "e4").
|
||||
* Returns None if the input is not a valid square name. */
|
||||
/** Parse a square from algebraic notation (e.g. "e4"). Returns None if the input is not a valid square name.
|
||||
*/
|
||||
def fromAlgebraic(s: String): Option[Square] =
|
||||
if s.length != 2 then None
|
||||
else
|
||||
val fileChar = s.charAt(0)
|
||||
val rankChar = s.charAt(1)
|
||||
val fileOpt = File.values.find(_.toString.equalsIgnoreCase(fileChar.toString))
|
||||
val fileOpt = File.values.find(_.toString.equalsIgnoreCase(fileChar.toString))
|
||||
val rankOpt =
|
||||
rankChar.toString.toIntOption.flatMap(n =>
|
||||
if n >= 1 && n <= 8 then Some(Rank.values(n - 1)) else None
|
||||
)
|
||||
rankChar.toString.toIntOption.flatMap(n => 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] =
|
||||
@@ -46,12 +41,13 @@ object Square:
|
||||
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). */
|
||||
/** 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
|
||||
else None
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
package de.nowchess.api.game
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, Square, CastlingRights}
|
||||
import de.nowchess.api.board.{Board, CastlingRights, Color, Square}
|
||||
import de.nowchess.api.move.Move
|
||||
|
||||
/** Immutable bundle of complete game state.
|
||||
* All state changes produce new GameContext instances.
|
||||
*/
|
||||
/** Immutable bundle of complete game state. All state changes produce new GameContext instances.
|
||||
*/
|
||||
case class GameContext(
|
||||
board: Board,
|
||||
turn: Color,
|
||||
castlingRights: CastlingRights,
|
||||
enPassantSquare: Option[Square],
|
||||
halfMoveClock: Int,
|
||||
moves: List[Move]
|
||||
board: Board,
|
||||
turn: Color,
|
||||
castlingRights: CastlingRights,
|
||||
enPassantSquare: Option[Square],
|
||||
halfMoveClock: Int,
|
||||
moves: List[Move],
|
||||
):
|
||||
/** Create new context with updated board. */
|
||||
def withBoard(newBoard: Board): GameContext = copy(board = newBoard)
|
||||
@@ -40,5 +39,5 @@ object GameContext:
|
||||
castlingRights = CastlingRights.Initial,
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty
|
||||
moves = List.empty,
|
||||
)
|
||||
|
||||
@@ -10,24 +10,30 @@ enum PromotionPiece:
|
||||
enum MoveType:
|
||||
/** A normal move or capture with no special rule. */
|
||||
case Normal(isCapture: Boolean = false)
|
||||
|
||||
/** Kingside castling (O-O). */
|
||||
case CastleKingside
|
||||
|
||||
/** Queenside castling (O-O-O). */
|
||||
case CastleQueenside
|
||||
|
||||
/** En-passant pawn capture. */
|
||||
case EnPassant
|
||||
|
||||
/** Pawn promotion; carries the chosen promotion piece. */
|
||||
case Promotion(piece: PromotionPiece)
|
||||
|
||||
/**
|
||||
* A half-move (ply) in a chess game.
|
||||
*
|
||||
* @param from origin square
|
||||
* @param to destination square
|
||||
* @param moveType special semantics; defaults to Normal
|
||||
*/
|
||||
/** A half-move (ply) in a chess game.
|
||||
*
|
||||
* @param from
|
||||
* origin square
|
||||
* @param to
|
||||
* destination square
|
||||
* @param moveType
|
||||
* special semantics; defaults to Normal
|
||||
*/
|
||||
final case class Move(
|
||||
from: Square,
|
||||
to: Square,
|
||||
moveType: MoveType = MoveType.Normal()
|
||||
from: Square,
|
||||
to: Square,
|
||||
moveType: MoveType = MoveType.Normal(),
|
||||
)
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
package de.nowchess.api.player
|
||||
|
||||
/**
|
||||
* An opaque player identifier.
|
||||
*
|
||||
* Wraps a plain String so that IDs are not accidentally interchanged with
|
||||
* other String values at compile time.
|
||||
*/
|
||||
/** An opaque player identifier.
|
||||
*
|
||||
* Wraps a plain String so that IDs are not accidentally interchanged with other String values at compile time.
|
||||
*/
|
||||
opaque type PlayerId = String
|
||||
|
||||
object PlayerId:
|
||||
def apply(value: String): PlayerId = value
|
||||
def apply(value: String): PlayerId = value
|
||||
extension (id: PlayerId) def value: String = id
|
||||
|
||||
/**
|
||||
* The minimal cross-service identity stub for a player.
|
||||
*
|
||||
* Full profile data (email, rating history, etc.) lives in the user-management
|
||||
* service. Only what every service needs is held here.
|
||||
*
|
||||
* @param id unique identifier
|
||||
* @param displayName human-readable name shown in the UI
|
||||
*/
|
||||
/** The minimal cross-service identity stub for a player.
|
||||
*
|
||||
* Full profile data (email, rating history, etc.) lives in the user-management service. Only what every service needs
|
||||
* is held here.
|
||||
*
|
||||
* @param id
|
||||
* unique identifier
|
||||
* @param displayName
|
||||
* human-readable name shown in the UI
|
||||
*/
|
||||
final case class PlayerInfo(
|
||||
id: PlayerId,
|
||||
displayName: String
|
||||
id: PlayerId,
|
||||
displayName: String,
|
||||
)
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package de.nowchess.api.response
|
||||
|
||||
/**
|
||||
* A standardised envelope for every API response.
|
||||
*
|
||||
* Success and failure are modelled as subtypes so that callers
|
||||
* can pattern-match exhaustively.
|
||||
*
|
||||
* @tparam A the payload type for a successful response
|
||||
*/
|
||||
/** A standardised envelope for every API response.
|
||||
*
|
||||
* Success and failure are modelled as subtypes so that callers can pattern-match exhaustively.
|
||||
*
|
||||
* @tparam A
|
||||
* the payload type for a successful response
|
||||
*/
|
||||
sealed trait ApiResponse[+A]
|
||||
|
||||
object ApiResponse:
|
||||
@@ -20,43 +19,49 @@ object ApiResponse:
|
||||
/** Convenience constructor for a single-error failure. */
|
||||
def error(err: ApiError): Failure = Failure(List(err))
|
||||
|
||||
/**
|
||||
* A structured error descriptor.
|
||||
*
|
||||
* @param code machine-readable error code (e.g. "INVALID_MOVE", "NOT_FOUND")
|
||||
* @param message human-readable explanation
|
||||
* @param field optional field name when the error relates to a specific input
|
||||
*/
|
||||
/** A structured error descriptor.
|
||||
*
|
||||
* @param code
|
||||
* machine-readable error code (e.g. "INVALID_MOVE", "NOT_FOUND")
|
||||
* @param message
|
||||
* human-readable explanation
|
||||
* @param field
|
||||
* optional field name when the error relates to a specific input
|
||||
*/
|
||||
final case class ApiError(
|
||||
code: String,
|
||||
message: String,
|
||||
field: Option[String] = None
|
||||
code: String,
|
||||
message: String,
|
||||
field: Option[String] = None,
|
||||
)
|
||||
|
||||
/**
|
||||
* Pagination metadata for list responses.
|
||||
*
|
||||
* @param page current 0-based page index
|
||||
* @param pageSize number of items per page
|
||||
* @param totalItems total number of items across all pages
|
||||
*/
|
||||
/** Pagination metadata for list responses.
|
||||
*
|
||||
* @param page
|
||||
* current 0-based page index
|
||||
* @param pageSize
|
||||
* number of items per page
|
||||
* @param totalItems
|
||||
* total number of items across all pages
|
||||
*/
|
||||
final case class Pagination(
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
totalItems: Long
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
totalItems: Long,
|
||||
):
|
||||
def totalPages: Int =
|
||||
if pageSize <= 0 then 0
|
||||
else Math.ceil(totalItems.toDouble / pageSize).toInt
|
||||
|
||||
/**
|
||||
* A paginated list response envelope.
|
||||
*
|
||||
* @param items the items on the current page
|
||||
* @param pagination pagination metadata
|
||||
* @tparam A the item type
|
||||
*/
|
||||
/** A paginated list response envelope.
|
||||
*
|
||||
* @param items
|
||||
* the items on the current page
|
||||
* @param pagination
|
||||
* pagination metadata
|
||||
* @tparam A
|
||||
* the item type
|
||||
*/
|
||||
final case class PagedResponse[A](
|
||||
items: List[A],
|
||||
pagination: Pagination
|
||||
items: List[A],
|
||||
pagination: Pagination,
|
||||
)
|
||||
|
||||
@@ -22,9 +22,9 @@ class BoardTest extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("withMove returns captured piece when destination is occupied") {
|
||||
val from = Square(File.A, Rank.R1)
|
||||
val to = Square(File.A, Rank.R8)
|
||||
val b = Board(Map(from -> Piece.WhiteRook, to -> Piece.BlackRook))
|
||||
val from = Square(File.A, Rank.R1)
|
||||
val to = Square(File.A, Rank.R8)
|
||||
val b = Board(Map(from -> Piece.WhiteRook, to -> Piece.BlackRook))
|
||||
val (board, captured) = b.withMove(from, to)
|
||||
captured shouldBe Some(Piece.BlackRook)
|
||||
board.pieceAt(to) shouldBe Some(Piece.WhiteRook)
|
||||
@@ -51,8 +51,14 @@ class BoardTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("initial board white back rank") {
|
||||
val expectedBackRank = Vector(
|
||||
PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen,
|
||||
PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook
|
||||
PieceType.Rook,
|
||||
PieceType.Knight,
|
||||
PieceType.Bishop,
|
||||
PieceType.Queen,
|
||||
PieceType.King,
|
||||
PieceType.Bishop,
|
||||
PieceType.Knight,
|
||||
PieceType.Rook,
|
||||
)
|
||||
File.values.zipWithIndex.foreach { (file, i) =>
|
||||
Board.initial.pieceAt(Square(file, Rank.R1)) shouldBe
|
||||
@@ -62,8 +68,14 @@ class BoardTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("initial board black back rank") {
|
||||
val expectedBackRank = Vector(
|
||||
PieceType.Rook, PieceType.Knight, PieceType.Bishop, PieceType.Queen,
|
||||
PieceType.King, PieceType.Bishop, PieceType.Knight, PieceType.Rook
|
||||
PieceType.Rook,
|
||||
PieceType.Knight,
|
||||
PieceType.Bishop,
|
||||
PieceType.Queen,
|
||||
PieceType.King,
|
||||
PieceType.Bishop,
|
||||
PieceType.Knight,
|
||||
PieceType.Rook,
|
||||
)
|
||||
File.values.zipWithIndex.foreach { (file, i) =>
|
||||
Board.initial.pieceAt(Square(file, Rank.R8)) shouldBe
|
||||
@@ -76,12 +88,11 @@ class BoardTest extends AnyFunSuite with Matchers:
|
||||
for
|
||||
rank <- emptyRanks
|
||||
file <- File.values
|
||||
do
|
||||
Board.initial.pieceAt(Square(file, rank)) shouldBe None
|
||||
do Board.initial.pieceAt(Square(file, rank)) shouldBe None
|
||||
}
|
||||
|
||||
test("updated adds and replaces piece at squares") {
|
||||
val b = Board(Map(e2 -> Piece.WhitePawn))
|
||||
val b = Board(Map(e2 -> Piece.WhitePawn))
|
||||
val added = b.updated(e4, Piece.WhiteKnight)
|
||||
added.pieceAt(e2) shouldBe Some(Piece.WhitePawn)
|
||||
added.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
|
||||
@@ -91,7 +102,7 @@ class BoardTest extends AnyFunSuite with Matchers:
|
||||
}
|
||||
|
||||
test("removed deletes piece from board") {
|
||||
val b = Board(Map(e2 -> Piece.WhitePawn, e4 -> Piece.WhiteKnight))
|
||||
val b = Board(Map(e2 -> Piece.WhitePawn, e4 -> Piece.WhiteKnight))
|
||||
val removed = b.removed(e2)
|
||||
removed.pieceAt(e2) shouldBe None
|
||||
removed.pieceAt(e4) shouldBe Some(Piece.WhiteKnight)
|
||||
@@ -105,4 +116,3 @@ class BoardTest extends AnyFunSuite with Matchers:
|
||||
moved.pieceAt(e4) shouldBe Some(Piece.WhitePawn)
|
||||
moved.pieceAt(e2) shouldBe None
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class CastlingRightsTest extends AnyFunSuite with Matchers:
|
||||
whiteKingSide = true,
|
||||
whiteQueenSide = false,
|
||||
blackKingSide = false,
|
||||
blackQueenSide = true
|
||||
blackQueenSide = true,
|
||||
)
|
||||
|
||||
rights.hasAnyRights shouldBe true
|
||||
@@ -54,4 +54,3 @@ class CastlingRightsTest extends AnyFunSuite with Matchers:
|
||||
val blackQueenSideRevoked = all.revokeQueenSide(Color.Black)
|
||||
blackQueenSideRevoked.blackKingSide shouldBe true
|
||||
blackQueenSideRevoked.blackQueenSide shouldBe false
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class ColorTest extends AnyFunSuite with Matchers:
|
||||
test("Color values expose opposite and label consistently"):
|
||||
val cases = List(
|
||||
(Color.White, Color.Black, "White"),
|
||||
(Color.Black, Color.White, "Black")
|
||||
(Color.Black, Color.White, "Black"),
|
||||
)
|
||||
|
||||
cases.foreach { (color, opposite, label) =>
|
||||
|
||||
@@ -7,24 +7,24 @@ class PieceTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("Piece holds color and pieceType") {
|
||||
val p = Piece(Color.White, PieceType.Queen)
|
||||
p.color shouldBe Color.White
|
||||
p.color shouldBe Color.White
|
||||
p.pieceType shouldBe PieceType.Queen
|
||||
}
|
||||
|
||||
test("all convenience constants map to expected color and piece type") {
|
||||
val expected = List(
|
||||
Piece.WhitePawn -> Piece(Color.White, PieceType.Pawn),
|
||||
Piece.WhitePawn -> Piece(Color.White, PieceType.Pawn),
|
||||
Piece.WhiteKnight -> Piece(Color.White, PieceType.Knight),
|
||||
Piece.WhiteBishop -> Piece(Color.White, PieceType.Bishop),
|
||||
Piece.WhiteRook -> Piece(Color.White, PieceType.Rook),
|
||||
Piece.WhiteQueen -> Piece(Color.White, PieceType.Queen),
|
||||
Piece.WhiteKing -> Piece(Color.White, PieceType.King),
|
||||
Piece.BlackPawn -> Piece(Color.Black, PieceType.Pawn),
|
||||
Piece.WhiteRook -> Piece(Color.White, PieceType.Rook),
|
||||
Piece.WhiteQueen -> Piece(Color.White, PieceType.Queen),
|
||||
Piece.WhiteKing -> Piece(Color.White, PieceType.King),
|
||||
Piece.BlackPawn -> Piece(Color.Black, PieceType.Pawn),
|
||||
Piece.BlackKnight -> Piece(Color.Black, PieceType.Knight),
|
||||
Piece.BlackBishop -> Piece(Color.Black, PieceType.Bishop),
|
||||
Piece.BlackRook -> Piece(Color.Black, PieceType.Rook),
|
||||
Piece.BlackQueen -> Piece(Color.Black, PieceType.Queen),
|
||||
Piece.BlackKing -> Piece(Color.Black, PieceType.King)
|
||||
Piece.BlackRook -> Piece(Color.Black, PieceType.Rook),
|
||||
Piece.BlackQueen -> Piece(Color.Black, PieceType.Queen),
|
||||
Piece.BlackKing -> Piece(Color.Black, PieceType.King),
|
||||
)
|
||||
|
||||
expected.foreach { case (actual, wanted) =>
|
||||
|
||||
@@ -7,12 +7,12 @@ class PieceTypeTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("PieceType values expose the expected labels"):
|
||||
val expectedLabels = List(
|
||||
PieceType.Pawn -> "Pawn",
|
||||
PieceType.Pawn -> "Pawn",
|
||||
PieceType.Knight -> "Knight",
|
||||
PieceType.Bishop -> "Bishop",
|
||||
PieceType.Rook -> "Rook",
|
||||
PieceType.Queen -> "Queen",
|
||||
PieceType.King -> "King"
|
||||
PieceType.Rook -> "Rook",
|
||||
PieceType.Queen -> "Queen",
|
||||
PieceType.King -> "King",
|
||||
)
|
||||
|
||||
expectedLabels.foreach { (pieceType, expectedLabel) =>
|
||||
|
||||
@@ -16,7 +16,7 @@ class SquareTest extends AnyFunSuite with Matchers:
|
||||
"a1" -> Square(File.A, Rank.R1),
|
||||
"e4" -> Square(File.E, Rank.R4),
|
||||
"h8" -> Square(File.H, Rank.R8),
|
||||
"E4" -> Square(File.E, Rank.R4)
|
||||
"E4" -> Square(File.E, Rank.R4),
|
||||
)
|
||||
expected.foreach { case (raw, sq) =>
|
||||
Square.fromAlgebraic(raw) shouldBe Some(sq)
|
||||
@@ -34,4 +34,3 @@ class SquareTest extends AnyFunSuite with Matchers:
|
||||
Square(File.A, Rank.R1).offset(-1, 0) shouldBe None
|
||||
Square(File.H, Rank.R8).offset(0, 1) shouldBe None
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ class GameContextTest extends AnyFunSuite with Matchers:
|
||||
initial.moves shouldBe List.empty
|
||||
|
||||
test("withBoard updates only board"):
|
||||
val square = Square(File.E, Rank.R4)
|
||||
val square = Square(File.E, Rank.R4)
|
||||
val updatedBoard = Board.initial.updated(square, de.nowchess.api.board.Piece.WhiteQueen)
|
||||
val updated = GameContext.initial.withBoard(updatedBoard)
|
||||
val updated = GameContext.initial.withBoard(updatedBoard)
|
||||
updated.board shouldBe updatedBoard
|
||||
updated.turn shouldBe GameContext.initial.turn
|
||||
updated.castlingRights shouldBe GameContext.initial.castlingRights
|
||||
@@ -34,13 +34,13 @@ class GameContextTest extends AnyFunSuite with Matchers:
|
||||
whiteKingSide = true,
|
||||
whiteQueenSide = false,
|
||||
blackKingSide = false,
|
||||
blackQueenSide = true
|
||||
blackQueenSide = true,
|
||||
)
|
||||
val square = Some(Square(File.E, Rank.R3))
|
||||
val updatedTurn = initial.withTurn(Color.Black)
|
||||
val square = Some(Square(File.E, Rank.R3))
|
||||
val updatedTurn = initial.withTurn(Color.Black)
|
||||
val updatedRights = initial.withCastlingRights(rights)
|
||||
val updatedEp = initial.withEnPassantSquare(square)
|
||||
val updatedClock = initial.withHalfMoveClock(17)
|
||||
val updatedEp = initial.withEnPassantSquare(square)
|
||||
val updatedClock = initial.withHalfMoveClock(17)
|
||||
|
||||
updatedTurn.turn shouldBe Color.Black
|
||||
updatedTurn.board shouldBe initial.board
|
||||
@@ -57,4 +57,3 @@ class GameContextTest extends AnyFunSuite with Matchers:
|
||||
test("withMove appends move to history"):
|
||||
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
|
||||
GameContext.initial.withMove(move).moves shouldBe List(move)
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class MoveTest extends AnyFunSuite with Matchers:
|
||||
MoveType.Promotion(PromotionPiece.Queen),
|
||||
MoveType.Promotion(PromotionPiece.Rook),
|
||||
MoveType.Promotion(PromotionPiece.Bishop),
|
||||
MoveType.Promotion(PromotionPiece.Knight)
|
||||
MoveType.Promotion(PromotionPiece.Knight),
|
||||
)
|
||||
|
||||
moveTypes.foreach { moveType =>
|
||||
|
||||
@@ -7,12 +7,12 @@ class PlayerInfoTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("PlayerId and PlayerInfo preserve constructor values") {
|
||||
val raw = "player-123"
|
||||
val id = PlayerId(raw)
|
||||
val id = PlayerId(raw)
|
||||
|
||||
id.value shouldBe raw
|
||||
|
||||
val playerId = PlayerId("p1")
|
||||
val info = PlayerInfo(playerId, "Magnus")
|
||||
info.id.value shouldBe "p1"
|
||||
info.displayName shouldBe "Magnus"
|
||||
val info = PlayerInfo(playerId, "Magnus")
|
||||
info.id.value shouldBe "p1"
|
||||
info.displayName shouldBe "Magnus"
|
||||
}
|
||||
|
||||
@@ -14,9 +14,9 @@ class ApiResponseTest extends AnyFunSuite with Matchers:
|
||||
ApiResponse.error(err) shouldBe ApiResponse.Failure(List(err))
|
||||
|
||||
val e = ApiError("CODE", "message")
|
||||
e.code shouldBe "CODE"
|
||||
e.code shouldBe "CODE"
|
||||
e.message shouldBe "message"
|
||||
e.field shouldBe None
|
||||
e.field shouldBe None
|
||||
ApiError("INVALID", "bad value", Some("email")).field shouldBe Some("email")
|
||||
}
|
||||
|
||||
@@ -31,6 +31,6 @@ class ApiResponseTest extends AnyFunSuite with Matchers:
|
||||
test("PagedResponse holds items and pagination") {
|
||||
val pagination = Pagination(page = 1, pageSize = 5, totalItems = 20)
|
||||
val pr = PagedResponse(List("a", "b"), pagination)
|
||||
pr.items shouldBe List("a", "b")
|
||||
pr.items shouldBe List("a", "b")
|
||||
pr.pagination shouldBe pagination
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user