chore: Set up shared-models library and initial project structure for NowChessSystems

This commit is contained in:
2026-03-21 17:07:28 +01:00
parent a8d457a612
commit 9c2456e928
28 changed files with 815 additions and 12 deletions
@@ -0,0 +1,8 @@
package de.nowchess.api.board
enum Color:
case White, Black
def opposite: Color = this match
case White => Black
case Black => White
@@ -0,0 +1,20 @@
package de.nowchess.api.board
/** A chess piece on the board — a combination of a color and a piece type. */
final case class Piece(color: Color, pieceType: PieceType)
object Piece:
// Convenience constructors
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 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)
@@ -0,0 +1,4 @@
package de.nowchess.api.board
enum PieceType:
case Pawn, Knight, Bishop, Rook, Queen, King
@@ -0,0 +1,41 @@
package de.nowchess.api.board
/**
* A file (column) on the chess board, ah.
* Ordinal values 07 correspond to ah.
*/
enum File:
case A, B, C, D, E, F, G, H
/**
* A rank (row) on the chess board, 18.
* Ordinal values 07 correspond to ranks 18.
*/
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, ah
* @param rank the row, 18
*/
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. */
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 rankOpt =
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)
@@ -0,0 +1,67 @@
package de.nowchess.api.game
import de.nowchess.api.board.{Color, Square}
/**
* Castling availability flags for one side.
*
* @param kingSide king-side castling still legally available
* @param queenSide queen-side castling still legally available
*/
final case class CastlingRights(kingSide: Boolean, queenSide: Boolean)
object CastlingRights:
val None: CastlingRights = CastlingRights(kingSide = false, queenSide = false)
val Both: CastlingRights = CastlingRights(kingSide = true, queenSide = true)
/** Outcome of a finished game. */
enum GameResult:
case WhiteWins
case BlackWins
case Draw
/** Lifecycle state of a game. */
enum GameStatus:
case NotStarted
case InProgress
case Finished(result: GameResult)
/**
* A FEN-compatible snapshot of board and game state.
*
* The board is represented as a FEN piece-placement string (rank 8 to rank 1,
* separated by '/'). All other fields mirror standard FEN fields.
*
* @param piecePlacement FEN piece-placement field, e.g.
* "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
* @param activeColor side to move
* @param castlingWhite castling rights for White
* @param castlingBlack castling rights for Black
* @param enPassantTarget square behind the double-pushed pawn, if any
* @param halfMoveClock plies since last capture or pawn advance (50-move rule)
* @param fullMoveNumber increments after Black's move, starts at 1
* @param status current lifecycle status of the game
*/
final case class GameState(
piecePlacement: String,
activeColor: Color,
castlingWhite: CastlingRights,
castlingBlack: CastlingRights,
enPassantTarget: Option[Square],
halfMoveClock: Int,
fullMoveNumber: Int,
status: GameStatus
)
object GameState:
/** Standard starting position. */
val initial: GameState = GameState(
piecePlacement = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR",
activeColor = Color.White,
castlingWhite = CastlingRights.Both,
castlingBlack = CastlingRights.Both,
enPassantTarget = None,
halfMoveClock = 0,
fullMoveNumber = 1,
status = GameStatus.InProgress
)
@@ -0,0 +1,33 @@
package de.nowchess.api.move
import de.nowchess.api.board.{PieceType, Square}
/** The piece a pawn may be promoted to (all non-pawn, non-king pieces). */
enum PromotionPiece:
case Knight, Bishop, Rook, Queen
/** Classifies special move semantics beyond a plain quiet move or capture. */
enum MoveType:
/** A normal move or capture with no special rule. */
case Normal
/** 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
*/
final case class Move(
from: Square,
to: Square,
moveType: MoveType = MoveType.Normal
)
@@ -0,0 +1,27 @@
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.
*/
opaque type PlayerId = String
object PlayerId:
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
*/
final case class PlayerInfo(
id: PlayerId,
displayName: String
)
@@ -0,0 +1,62 @@
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
*/
sealed trait ApiResponse[+A]
object ApiResponse:
/** A successful response carrying a payload. */
final case class Success[A](data: A) extends ApiResponse[A]
/** A failed response carrying one or more errors. */
final case class Failure(errors: List[ApiError]) extends ApiResponse[Nothing]
/** 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
*/
final case class ApiError(
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
*/
final case class Pagination(
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
*/
final case class PagedResponse[A](
items: List[A],
pagination: Pagination
)