From 96d4f995fa47225399da2a2a73c44d00eb54f7c0 Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 21 Apr 2026 21:37:43 +0200 Subject: [PATCH 1/6] feat: refactor IO module to API structure and add FEN export functionality --- .../nowchess/api/dto/ImportFenRequest.scala | 3 + .../nowchess/api/dto/ImportPgnRequest.scala | 3 + .../de/nowchess/api/io/FenExporter.scala | 51 +++++++++++++ .../nowchess/api/io/GameContextExport.scala | 6 ++ .../nowchess/api/io/GameContextImport.scala | 6 ++ modules/core/build.gradle.kts | 3 +- .../chess/client/IoServiceClient.scala | 2 +- .../nowchess/chess/config/JacksonConfig.scala | 1 - .../chess/config/SquareKeyDeserializer.scala | 8 ++ .../chess/config/SquareKeySerializer.scala | 9 +++ .../de/nowchess/chess/engine/GameEngine.scala | 2 +- .../chess/resource/GameResource.scala | 5 +- .../engine/GameEngineIntegrationTest.scala | 2 +- .../chess/engine/GameEngineLoadGameTest.scala | 6 +- .../GameResourceIntegrationTest.scala | 2 +- .../de/nowchess/io/GameContextExport.scala | 6 +- .../de/nowchess/io/GameContextImport.scala | 6 +- .../de/nowchess/io/fen/FenExporter.scala | 56 ++------------ .../de/nowchess/io/pgn/PgnExporter.scala | 75 +------------------ .../de/nowchess/rules/pgn/PgnExporter.scala | 58 ++++++++++++++ 20 files changed, 166 insertions(+), 144 deletions(-) create mode 100644 modules/api/src/main/scala/de/nowchess/api/dto/ImportFenRequest.scala create mode 100644 modules/api/src/main/scala/de/nowchess/api/dto/ImportPgnRequest.scala create mode 100644 modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala create mode 100644 modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala create mode 100644 modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala create mode 100644 modules/core/src/main/scala/de/nowchess/chess/config/SquareKeyDeserializer.scala create mode 100644 modules/core/src/main/scala/de/nowchess/chess/config/SquareKeySerializer.scala create mode 100644 modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala diff --git a/modules/api/src/main/scala/de/nowchess/api/dto/ImportFenRequest.scala b/modules/api/src/main/scala/de/nowchess/api/dto/ImportFenRequest.scala new file mode 100644 index 0000000..9a30b62 --- /dev/null +++ b/modules/api/src/main/scala/de/nowchess/api/dto/ImportFenRequest.scala @@ -0,0 +1,3 @@ +package de.nowchess.api.dto + +case class ImportFenRequest(fen: String) diff --git a/modules/api/src/main/scala/de/nowchess/api/dto/ImportPgnRequest.scala b/modules/api/src/main/scala/de/nowchess/api/dto/ImportPgnRequest.scala new file mode 100644 index 0000000..ef3613a --- /dev/null +++ b/modules/api/src/main/scala/de/nowchess/api/dto/ImportPgnRequest.scala @@ -0,0 +1,3 @@ +package de.nowchess.api.dto + +case class ImportPgnRequest(pgn: String) diff --git a/modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala b/modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala new file mode 100644 index 0000000..3fd4eb9 --- /dev/null +++ b/modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala @@ -0,0 +1,51 @@ +package de.nowchess.api.io + +import de.nowchess.api.board.* +import de.nowchess.api.game.GameContext + +object FenExporter extends GameContextExport: + + def boardToFen(board: Board): String = + Rank.values.reverse + .map(rank => buildRankString(board, rank)) + .mkString("/") + + def gameContextToFen(context: GameContext): String = + val piecePlacement = boardToFen(context.board) + val activeColor = if context.turn == Color.White then "w" else "b" + val castling = castlingString(context.castlingRights) + val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-") + val fullMoveNumber = 1 + (context.moves.length / 2) + s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber" + + def exportGameContext(context: GameContext): String = gameContextToFen(context) + + private def buildRankString(board: Board, rank: Rank): String = + val rankSquares = File.values.map(file => Square(file, rank)) + val (result, emptyCount) = rankSquares.foldLeft(("", 0)): + case ((acc, empty), square) => + board.pieceAt(square) match + case Some(piece) => + val flushed = if empty > 0 then acc + empty.toString else acc + (flushed + pieceToFenChar(piece), 0) + case None => + (acc, empty + 1) + if emptyCount > 0 then result + emptyCount.toString else result + + private def castlingString(rights: CastlingRights): String = + val wk = if rights.whiteKingSide then "K" else "" + val wq = if rights.whiteQueenSide then "Q" else "" + val bk = if rights.blackKingSide then "k" else "" + val bq = if rights.blackQueenSide then "q" else "" + val result = s"$wk$wq$bk$bq" + if result.isEmpty then "-" else result + + private def pieceToFenChar(piece: Piece): Char = + val base = piece.pieceType match + case PieceType.Pawn => 'p' + case PieceType.Knight => 'n' + case PieceType.Bishop => 'b' + case PieceType.Rook => 'r' + case PieceType.Queen => 'q' + case PieceType.King => 'k' + if piece.color == Color.White then base.toUpper else base diff --git a/modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala b/modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala new file mode 100644 index 0000000..a261742 --- /dev/null +++ b/modules/api/src/main/scala/de/nowchess/api/io/GameContextExport.scala @@ -0,0 +1,6 @@ +package de.nowchess.api.io + +import de.nowchess.api.game.GameContext + +trait GameContextExport: + def exportGameContext(context: GameContext): String diff --git a/modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala b/modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala new file mode 100644 index 0000000..22c149e --- /dev/null +++ b/modules/api/src/main/scala/de/nowchess/api/io/GameContextImport.scala @@ -0,0 +1,6 @@ +package de.nowchess.api.io + +import de.nowchess.api.game.GameContext + +trait GameContextImport: + def importGameContext(input: String): Either[String, GameContext] diff --git a/modules/core/build.gradle.kts b/modules/core/build.gradle.kts index d572500..d057a24 100644 --- a/modules/core/build.gradle.kts +++ b/modules/core/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { } implementation(project(":modules:api")) - implementation(project(":modules:io")) implementation(project(":modules:rule")) implementation(project(":modules:bot")) @@ -69,6 +68,8 @@ dependencies { implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}") + testImplementation(project(":modules:io")) + testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}") diff --git a/modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala b/modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala index dac4c3d..bc41b1b 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/client/IoServiceClient.scala @@ -1,7 +1,7 @@ package de.nowchess.chess.client +import de.nowchess.api.dto.{ImportFenRequest, ImportPgnRequest} import de.nowchess.api.game.GameContext -import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest} import jakarta.ws.rs.* import jakarta.ws.rs.core.MediaType import org.eclipse.microprofile.rest.client.inject.RegisterRestClient diff --git a/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala b/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala index 816cb0b..1393880 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/config/JacksonConfig.scala @@ -5,7 +5,6 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.scala.DefaultScalaModule import de.nowchess.api.board.Square -import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer} import io.quarkus.jackson.ObjectMapperCustomizer import jakarta.inject.Singleton diff --git a/modules/core/src/main/scala/de/nowchess/chess/config/SquareKeyDeserializer.scala b/modules/core/src/main/scala/de/nowchess/chess/config/SquareKeyDeserializer.scala new file mode 100644 index 0000000..35e9604 --- /dev/null +++ b/modules/core/src/main/scala/de/nowchess/chess/config/SquareKeyDeserializer.scala @@ -0,0 +1,8 @@ +package de.nowchess.chess.config + +import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer} +import de.nowchess.api.board.Square + +class SquareKeyDeserializer extends KeyDeserializer: + override def deserializeKey(key: String, ctx: DeserializationContext): AnyRef = + Square.fromAlgebraic(key).orNull diff --git a/modules/core/src/main/scala/de/nowchess/chess/config/SquareKeySerializer.scala b/modules/core/src/main/scala/de/nowchess/chess/config/SquareKeySerializer.scala new file mode 100644 index 0000000..c0ca0d0 --- /dev/null +++ b/modules/core/src/main/scala/de/nowchess/chess/config/SquareKeySerializer.scala @@ -0,0 +1,9 @@ +package de.nowchess.chess.config + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider} +import de.nowchess.api.board.Square + +class SquareKeySerializer extends JsonSerializer[Square]: + override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit = + gen.writeFieldName(value.toString) diff --git a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala index 43f099b..0759048 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala @@ -7,7 +7,7 @@ import de.nowchess.api.player.{PlayerId, PlayerInfo} import de.nowchess.chess.controller.Parser import de.nowchess.chess.observer.* import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult} -import de.nowchess.io.{GameContextExport, GameContextImport} +import de.nowchess.api.io.{GameContextExport, GameContextImport} import de.nowchess.rules.RuleSet import de.nowchess.rules.sets.DefaultRules diff --git a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala index ece9752..283c6a8 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.nowchess.api.board.Square import de.nowchess.api.dto.* import de.nowchess.api.game.{DrawReason, GameContext, GameResult} +import de.nowchess.api.io.FenExporter import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.player.{PlayerId, PlayerInfo} import de.nowchess.chess.client.IoServiceClient @@ -12,9 +13,7 @@ import de.nowchess.chess.engine.GameEngine import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException} import de.nowchess.chess.observer.* import de.nowchess.chess.registry.{GameEntry, GameRegistry} -import de.nowchess.io.fen.FenExporter -import de.nowchess.io.pgn.PgnExporter -import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest} +import de.nowchess.rules.pgn.PgnExporter import io.smallrye.mutiny.Multi import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject diff --git a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala index 371f2dd..e07336b 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala @@ -4,7 +4,7 @@ import de.nowchess.api.board.{Board, Color, File, PieceType, Rank, Square} import de.nowchess.api.game.GameContext import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.chess.observer.{GameEvent, InvalidMoveEvent, InvalidMoveReason, MoveRedoneEvent, Observer} -import de.nowchess.io.GameContextImport +import de.nowchess.api.io.GameContextImport import de.nowchess.rules.RuleSet import de.nowchess.rules.sets.DefaultRules import org.scalatest.funsuite.AnyFunSuite diff --git a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala index d926147..437006d 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala @@ -4,10 +4,10 @@ import scala.collection.mutable import de.nowchess.api.board.{Board, Color} import de.nowchess.api.game.GameContext import de.nowchess.chess.observer.{GameEvent, Observer, PgnLoadedEvent} -import de.nowchess.io.pgn.PgnParser +import de.nowchess.api.io.FenExporter import de.nowchess.io.fen.FenParser -import de.nowchess.io.pgn.PgnExporter -import de.nowchess.io.fen.FenExporter +import de.nowchess.io.pgn.PgnParser +import de.nowchess.rules.pgn.PgnExporter import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers diff --git a/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala b/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala index 1ee458e..3d27898 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala @@ -4,7 +4,7 @@ import de.nowchess.api.dto.* import de.nowchess.api.game.GameContext import de.nowchess.chess.client.IoServiceClient import de.nowchess.chess.exception.BadRequestException -import de.nowchess.io.fen.FenExporter +import de.nowchess.api.io.FenExporter import de.nowchess.io.pgn.PgnParser import io.quarkus.test.InjectMock import io.quarkus.test.junit.QuarkusTest diff --git a/modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala b/modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala index 3968e71..8cc51ef 100644 --- a/modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala +++ b/modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala @@ -1,7 +1,3 @@ package de.nowchess.io -import de.nowchess.api.game.GameContext - -trait GameContextExport: - - def exportGameContext(context: GameContext): String +type GameContextExport = de.nowchess.api.io.GameContextExport diff --git a/modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala b/modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala index c56850f..e2c60f6 100644 --- a/modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala +++ b/modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala @@ -1,7 +1,3 @@ package de.nowchess.io -import de.nowchess.api.game.GameContext - -trait GameContextImport: - - def importGameContext(input: String): Either[String, GameContext] +type GameContextImport = de.nowchess.api.io.GameContextImport diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala index 8ba9303..e71df91 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala @@ -1,57 +1,11 @@ package de.nowchess.io.fen -import de.nowchess.api.board.* +import de.nowchess.api.board.Board import de.nowchess.api.game.GameContext +import de.nowchess.api.io.{FenExporter => ApiFenExporter} import de.nowchess.io.GameContextExport object FenExporter extends GameContextExport: - - /** Convert a Board to FEN piece-placement string (rank 8 to rank 1, separated by '/'). */ - def boardToFen(board: Board): String = - Rank.values.reverse - .map(rank => buildRankString(board, rank)) - .mkString("/") - - /** Build the FEN representation for a single rank. */ - private def buildRankString(board: Board, rank: Rank): String = - val rankSquares = File.values.map(file => Square(file, rank)) - val (result, emptyCount) = rankSquares.foldLeft(("", 0)): - case ((acc, empty), square) => - board.pieceAt(square) match - case Some(piece) => - val flushed = if empty > 0 then acc + empty.toString else acc - (flushed + pieceToFenChar(piece), 0) - case None => - (acc, empty + 1) - if emptyCount > 0 then result + emptyCount.toString else result - - /** Convert a GameContext to a complete FEN string. */ - def gameContextToFen(context: GameContext): String = - val piecePlacement = boardToFen(context.board) - val activeColor = if context.turn == Color.White then "w" else "b" - val castling = castlingString(context.castlingRights) - val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-") - val fullMoveNumber = 1 + (context.moves.length / 2) - s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber" - - def exportGameContext(context: GameContext): String = gameContextToFen(context) - - /** Convert castling rights to FEN notation. */ - private def castlingString(rights: CastlingRights): String = - val wk = if rights.whiteKingSide then "K" else "" - val wq = if rights.whiteQueenSide then "Q" else "" - val bk = if rights.blackKingSide then "k" else "" - val bq = if rights.blackQueenSide then "q" else "" - val result = s"$wk$wq$bk$bq" - if result.isEmpty then "-" else result - - /** Convert a Piece to its FEN character (uppercase = White, lowercase = Black). */ - private def pieceToFenChar(piece: Piece): Char = - val base = piece.pieceType match - case PieceType.Pawn => 'p' - case PieceType.Knight => 'n' - case PieceType.Bishop => 'b' - case PieceType.Rook => 'r' - case PieceType.Queen => 'q' - case PieceType.King => 'k' - if piece.color == Color.White then base.toUpper else base + def boardToFen(board: Board): String = ApiFenExporter.boardToFen(board) + def gameContextToFen(context: GameContext): String = ApiFenExporter.gameContextToFen(context) + def exportGameContext(context: GameContext): String = ApiFenExporter.exportGameContext(context) diff --git a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala index afb697a..5e002f6 100644 --- a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala @@ -1,77 +1,10 @@ package de.nowchess.io.pgn -import de.nowchess.api.board.* -import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.game.GameContext +import de.nowchess.api.move.Move import de.nowchess.io.GameContextExport -import de.nowchess.rules.sets.DefaultRules +import de.nowchess.rules.pgn.{PgnExporter => RulesPgnExporter} object PgnExporter extends GameContextExport: - - /** Export a GameContext to PGN format. */ - def exportGameContext(context: GameContext): String = - val headers = Map( - "Event" -> "?", - "White" -> "?", - "Black" -> "?", - "Result" -> "*", - ) - - exportGame(headers, context.moves) - - /** Export a game with headers and moves to PGN format. */ - def exportGame(headers: Map[String, String], moves: List[Move]): String = - val headerLines = headers - .map { case (key, value) => - s"""[$key "$value"]""" - } - .mkString("\n") - - val moveText = - if moves.isEmpty then "" - else - val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) - val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } - - val groupedMoves = sanMoves.zipWithIndex.groupBy(_._2 / 2) - val moveLines = for (moveNumber, movePairs) <- groupedMoves.toList.sortBy(_._1) yield - val moveNum = moveNumber + 1 - val whiteMoveStr = movePairs.find(_._2 % 2 == 0).map(_._1).getOrElse("") - val blackMoveStr = movePairs.find(_._2 % 2 == 1).map(_._1).getOrElse("") - if blackMoveStr.isEmpty then s"$moveNum. $whiteMoveStr" - else s"$moveNum. $whiteMoveStr $blackMoveStr" - - val termination = headers.getOrElse("Result", "*") - moveLines.mkString(" ") + s" $termination" - - if headerLines.isEmpty then moveText - else if moveText.isEmpty then headerLines - else s"$headerLines\n\n$moveText" - - /** Convert a Move to Standard Algebraic Notation using the board state before the move. */ - private def moveToAlgebraic(move: Move, boardBefore: Board): String = - move.moveType match - case MoveType.CastleKingside => "O-O" - case MoveType.CastleQueenside => "O-O-O" - case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}" - case MoveType.Promotion(pp) => - val promSuffix = pp match - case PromotionPiece.Queen => "=Q" - case PromotionPiece.Rook => "=R" - case PromotionPiece.Bishop => "=B" - case PromotionPiece.Knight => "=N" - val isCapture = boardBefore.pieceAt(move.to).isDefined - if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$promSuffix" - else s"${move.to}$promSuffix" - case MoveType.Normal(isCapture) => - val dest = move.to.toString - val capStr = if isCapture then "x" else "" - boardBefore.pieceAt(move.from).map(_.pieceType).getOrElse(PieceType.Pawn) match - case PieceType.Pawn => - if isCapture then s"${move.from.file.toString.toLowerCase}x$dest" - else dest - case PieceType.Knight => s"N$capStr$dest" - case PieceType.Bishop => s"B$capStr$dest" - case PieceType.Rook => s"R$capStr$dest" - case PieceType.Queen => s"Q$capStr$dest" - case PieceType.King => s"K$capStr$dest" + def exportGameContext(context: GameContext): String = RulesPgnExporter.exportGameContext(context) + def exportGame(headers: Map[String, String], moves: List[Move]): String = RulesPgnExporter.exportGame(headers, moves) diff --git a/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala b/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala new file mode 100644 index 0000000..34b2973 --- /dev/null +++ b/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala @@ -0,0 +1,58 @@ +package de.nowchess.rules.pgn + +import de.nowchess.api.board.* +import de.nowchess.api.game.GameContext +import de.nowchess.api.io.GameContextExport +import de.nowchess.api.move.{Move, MoveType, PromotionPiece} +import de.nowchess.rules.sets.DefaultRules + +object PgnExporter extends GameContextExport: + + def exportGameContext(context: GameContext): String = + exportGame( + Map("Event" -> "?", "White" -> "?", "Black" -> "?", "Result" -> "*"), + context.moves, + ) + + def exportGame(headers: Map[String, String], moves: List[Move]): String = + val headerLines = headers.map { case (k, v) => s"""[$k "$v"]""" }.mkString("\n") + val moveText = if moves.isEmpty then "" else buildMoveText(headers, moves) + if headerLines.isEmpty then moveText + else if moveText.isEmpty then headerLines + else s"$headerLines\n\n$moveText" + + private def buildMoveText(headers: Map[String, String], moves: List[Move]): String = + val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) + val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } + val grouped = sanMoves.zipWithIndex.groupBy(_._2 / 2) + val moveLines = grouped.toList.sortBy(_._1).map { case (n, pairs) => + val w = pairs.find(_._2 % 2 == 0).map(_._1).getOrElse("") + val b = pairs.find(_._2 % 2 == 1).map(_._1).getOrElse("") + if b.isEmpty then s"${n + 1}. $w" else s"${n + 1}. $w $b" + } + moveLines.mkString(" ") + s" ${headers.getOrElse("Result", "*")}" + + private def moveToAlgebraic(move: Move, boardBefore: Board): String = + move.moveType match + case MoveType.CastleKingside => "O-O" + case MoveType.CastleQueenside => "O-O-O" + case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}" + case MoveType.Promotion(pp) => + val suffix = pp match + case PromotionPiece.Queen => "=Q" + case PromotionPiece.Rook => "=R" + case PromotionPiece.Bishop => "=B" + case PromotionPiece.Knight => "=N" + val isCapture = boardBefore.pieceAt(move.to).isDefined + if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$suffix" + else s"${move.to}$suffix" + case MoveType.Normal(isCapture) => + val dest = move.to.toString + val capStr = if isCapture then "x" else "" + boardBefore.pieceAt(move.from).map(_.pieceType).getOrElse(PieceType.Pawn) match + case PieceType.Pawn => if isCapture then s"${move.from.file.toString.toLowerCase}x$dest" else dest + case PieceType.Knight => s"N$capStr$dest" + case PieceType.Bishop => s"B$capStr$dest" + case PieceType.Rook => s"R$capStr$dest" + case PieceType.Queen => s"Q$capStr$dest" + case PieceType.King => s"K$capStr$dest" -- 2.52.0 From c4c245434b6a4ff064c7ed11282194ef966352cb Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 21 Apr 2026 21:51:38 +0200 Subject: [PATCH 2/6] refactor: update import statements for FenExporter and PgnExporter for clarity --- .../src/main/scala/de/nowchess/io/fen/FenExporter.scala | 8 ++++---- .../src/main/scala/de/nowchess/io/pgn/PgnExporter.scala | 4 ++-- .../main/scala/de/nowchess/rules/pgn/PgnExporter.scala | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala index e71df91..9fa37e0 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala @@ -2,10 +2,10 @@ package de.nowchess.io.fen import de.nowchess.api.board.Board import de.nowchess.api.game.GameContext -import de.nowchess.api.io.{FenExporter => ApiFenExporter} +import de.nowchess.api.io.FenExporter as ApiFenExporter import de.nowchess.io.GameContextExport object FenExporter extends GameContextExport: - def boardToFen(board: Board): String = ApiFenExporter.boardToFen(board) - def gameContextToFen(context: GameContext): String = ApiFenExporter.gameContextToFen(context) - def exportGameContext(context: GameContext): String = ApiFenExporter.exportGameContext(context) + def boardToFen(board: Board): String = ApiFenExporter.boardToFen(board) + def gameContextToFen(context: GameContext): String = ApiFenExporter.gameContextToFen(context) + def exportGameContext(context: GameContext): String = ApiFenExporter.exportGameContext(context) diff --git a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala index 5e002f6..c7312a8 100644 --- a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala @@ -3,8 +3,8 @@ package de.nowchess.io.pgn import de.nowchess.api.game.GameContext import de.nowchess.api.move.Move import de.nowchess.io.GameContextExport -import de.nowchess.rules.pgn.{PgnExporter => RulesPgnExporter} +import de.nowchess.rules.pgn.PgnExporter as RulesPgnExporter object PgnExporter extends GameContextExport: - def exportGameContext(context: GameContext): String = RulesPgnExporter.exportGameContext(context) + def exportGameContext(context: GameContext): String = RulesPgnExporter.exportGameContext(context) def exportGame(headers: Map[String, String], moves: List[Move]): String = RulesPgnExporter.exportGame(headers, moves) diff --git a/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala b/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala index 34b2973..5657cb3 100644 --- a/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala +++ b/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala @@ -22,9 +22,9 @@ object PgnExporter extends GameContextExport: else s"$headerLines\n\n$moveText" private def buildMoveText(headers: Map[String, String], moves: List[Move]): String = - val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) - val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } - val grouped = sanMoves.zipWithIndex.groupBy(_._2 / 2) + val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) + val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } + val grouped = sanMoves.zipWithIndex.groupBy(_._2 / 2) val moveLines = grouped.toList.sortBy(_._1).map { case (n, pairs) => val w = pairs.find(_._2 % 2 == 0).map(_._1).getOrElse("") val b = pairs.find(_._2 % 2 == 1).map(_._1).getOrElse("") @@ -38,7 +38,7 @@ object PgnExporter extends GameContextExport: case MoveType.CastleQueenside => "O-O-O" case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}" case MoveType.Promotion(pp) => - val suffix = pp match + val suffix = pp match case PromotionPiece.Queen => "=Q" case PromotionPiece.Rook => "=R" case PromotionPiece.Bishop => "=B" -- 2.52.0 From 55b4328916948ecfaac00df45c994bc293e54b61 Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 21 Apr 2026 21:59:06 +0200 Subject: [PATCH 3/6] feat: enhance FenExporter with detailed FEN string generation and refactor PgnExporter for improved game context export --- .../de/nowchess/api/io/FenExporter.scala | 51 ---------------- .../chess/resource/GameResource.scala | 14 +---- .../chess/engine/GameEngineLoadGameTest.scala | 11 ++-- .../GameResourceIntegrationTest.scala | 2 +- .../de/nowchess/io/fen/FenExporter.scala | 51 ++++++++++++++-- .../de/nowchess/io/pgn/PgnExporter.scala | 58 +++++++++++++++++-- .../de/nowchess/rules/pgn/PgnExporter.scala | 58 ------------------- 7 files changed, 106 insertions(+), 139 deletions(-) delete mode 100644 modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala delete mode 100644 modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala diff --git a/modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala b/modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala deleted file mode 100644 index 3fd4eb9..0000000 --- a/modules/api/src/main/scala/de/nowchess/api/io/FenExporter.scala +++ /dev/null @@ -1,51 +0,0 @@ -package de.nowchess.api.io - -import de.nowchess.api.board.* -import de.nowchess.api.game.GameContext - -object FenExporter extends GameContextExport: - - def boardToFen(board: Board): String = - Rank.values.reverse - .map(rank => buildRankString(board, rank)) - .mkString("/") - - def gameContextToFen(context: GameContext): String = - val piecePlacement = boardToFen(context.board) - val activeColor = if context.turn == Color.White then "w" else "b" - val castling = castlingString(context.castlingRights) - val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-") - val fullMoveNumber = 1 + (context.moves.length / 2) - s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber" - - def exportGameContext(context: GameContext): String = gameContextToFen(context) - - private def buildRankString(board: Board, rank: Rank): String = - val rankSquares = File.values.map(file => Square(file, rank)) - val (result, emptyCount) = rankSquares.foldLeft(("", 0)): - case ((acc, empty), square) => - board.pieceAt(square) match - case Some(piece) => - val flushed = if empty > 0 then acc + empty.toString else acc - (flushed + pieceToFenChar(piece), 0) - case None => - (acc, empty + 1) - if emptyCount > 0 then result + emptyCount.toString else result - - private def castlingString(rights: CastlingRights): String = - val wk = if rights.whiteKingSide then "K" else "" - val wq = if rights.whiteQueenSide then "Q" else "" - val bk = if rights.blackKingSide then "k" else "" - val bq = if rights.blackQueenSide then "q" else "" - val result = s"$wk$wq$bk$bq" - if result.isEmpty then "-" else result - - private def pieceToFenChar(piece: Piece): Char = - val base = piece.pieceType match - case PieceType.Pawn => 'p' - case PieceType.Knight => 'n' - case PieceType.Bishop => 'b' - case PieceType.Rook => 'r' - case PieceType.Queen => 'q' - case PieceType.King => 'k' - if piece.color == Color.White then base.toUpper else base diff --git a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala index 283c6a8..e5dbf94 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/resource/GameResource.scala @@ -4,7 +4,6 @@ import com.fasterxml.jackson.databind.ObjectMapper import de.nowchess.api.board.Square import de.nowchess.api.dto.* import de.nowchess.api.game.{DrawReason, GameContext, GameResult} -import de.nowchess.api.io.FenExporter import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.player.{PlayerId, PlayerInfo} import de.nowchess.chess.client.IoServiceClient @@ -13,7 +12,6 @@ import de.nowchess.chess.engine.GameEngine import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException} import de.nowchess.chess.observer.* import de.nowchess.chess.registry.{GameEntry, GameRegistry} -import de.nowchess.rules.pgn.PgnExporter import io.smallrye.mutiny.Multi import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject @@ -88,16 +86,8 @@ class GameResource: private def toGameStateDto(entry: GameEntry): GameStateDto = val ctx = entry.engine.context GameStateDto( - fen = FenExporter.exportGameContext(ctx), - pgn = PgnExporter.exportGame( - Map( - "Event" -> "NowChess game", - "White" -> entry.white.displayName, - "Black" -> entry.black.displayName, - "Result" -> "*", - ), - ctx.moves, - ), + fen = ioClient.exportFen(ctx), + pgn = ioClient.exportPgn(ctx), turn = ctx.turn.label.toLowerCase, status = statusOf(entry), winner = ctx.result.collect { case GameResult.Win(c) => c.label.toLowerCase }, diff --git a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala index 437006d..91ac2b1 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala @@ -1,16 +1,13 @@ package de.nowchess.chess.engine -import scala.collection.mutable -import de.nowchess.api.board.{Board, Color} -import de.nowchess.api.game.GameContext -import de.nowchess.chess.observer.{GameEvent, Observer, PgnLoadedEvent} -import de.nowchess.api.io.FenExporter +import de.nowchess.chess.observer.{GameEvent, Observer} import de.nowchess.io.fen.FenParser -import de.nowchess.io.pgn.PgnParser -import de.nowchess.rules.pgn.PgnExporter +import de.nowchess.io.pgn.{PgnExporter, PgnParser} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import scala.collection.mutable + class GameEngineLoadGameTest extends AnyFunSuite with Matchers: test("loadGame with PgnParser: loads valid PGN and enables undo/redo"): diff --git a/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala b/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala index 3d27898..1ee458e 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/resource/GameResourceIntegrationTest.scala @@ -4,7 +4,7 @@ import de.nowchess.api.dto.* import de.nowchess.api.game.GameContext import de.nowchess.chess.client.IoServiceClient import de.nowchess.chess.exception.BadRequestException -import de.nowchess.api.io.FenExporter +import de.nowchess.io.fen.FenExporter import de.nowchess.io.pgn.PgnParser import io.quarkus.test.InjectMock import io.quarkus.test.junit.QuarkusTest diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala index 9fa37e0..9e90afa 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala @@ -1,11 +1,52 @@ package de.nowchess.io.fen -import de.nowchess.api.board.Board +import de.nowchess.api.board.* import de.nowchess.api.game.GameContext -import de.nowchess.api.io.FenExporter as ApiFenExporter import de.nowchess.io.GameContextExport object FenExporter extends GameContextExport: - def boardToFen(board: Board): String = ApiFenExporter.boardToFen(board) - def gameContextToFen(context: GameContext): String = ApiFenExporter.gameContextToFen(context) - def exportGameContext(context: GameContext): String = ApiFenExporter.exportGameContext(context) + + def boardToFen(board: Board): String = + Rank.values.reverse + .map(rank => buildRankString(board, rank)) + .mkString("/") + + def gameContextToFen(context: GameContext): String = + val piecePlacement = boardToFen(context.board) + val activeColor = if context.turn == Color.White then "w" else "b" + val castling = castlingString(context.castlingRights) + val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-") + val fullMoveNumber = 1 + (context.moves.length / 2) + s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber" + + def exportGameContext(context: GameContext): String = gameContextToFen(context) + + private def buildRankString(board: Board, rank: Rank): String = + val rankSquares = File.values.map(file => Square(file, rank)) + val (result, emptyCount) = rankSquares.foldLeft(("", 0)): + case ((acc, empty), square) => + board.pieceAt(square) match + case Some(piece) => + val flushed = if empty > 0 then acc + empty.toString else acc + (flushed + pieceToFenChar(piece), 0) + case None => + (acc, empty + 1) + if emptyCount > 0 then result + emptyCount.toString else result + + private def castlingString(rights: CastlingRights): String = + val wk = if rights.whiteKingSide then "K" else "" + val wq = if rights.whiteQueenSide then "Q" else "" + val bk = if rights.blackKingSide then "k" else "" + val bq = if rights.blackQueenSide then "q" else "" + val result = s"$wk$wq$bk$bq" + if result.isEmpty then "-" else result + + private def pieceToFenChar(piece: Piece): Char = + val base = piece.pieceType match + case PieceType.Pawn => 'p' + case PieceType.Knight => 'n' + case PieceType.Bishop => 'b' + case PieceType.Rook => 'r' + case PieceType.Queen => 'q' + case PieceType.King => 'k' + if piece.color == Color.White then base.toUpper else base diff --git a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala index c7312a8..c95efa3 100644 --- a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala @@ -1,10 +1,58 @@ package de.nowchess.io.pgn +import de.nowchess.api.board.{Board, PieceType} import de.nowchess.api.game.GameContext -import de.nowchess.api.move.Move -import de.nowchess.io.GameContextExport -import de.nowchess.rules.pgn.PgnExporter as RulesPgnExporter +import de.nowchess.api.io.GameContextExport +import de.nowchess.api.move.{Move, MoveType, PromotionPiece} +import de.nowchess.rules.sets.DefaultRules object PgnExporter extends GameContextExport: - def exportGameContext(context: GameContext): String = RulesPgnExporter.exportGameContext(context) - def exportGame(headers: Map[String, String], moves: List[Move]): String = RulesPgnExporter.exportGame(headers, moves) + + def exportGameContext(context: GameContext): String = + exportGame( + Map("Event" -> "?", "White" -> "?", "Black" -> "?", "Result" -> "*"), + context.moves, + ) + + def exportGame(headers: Map[String, String], moves: List[Move]): String = + val headerLines = headers.map { case (k, v) => s"""[$k "$v"]""" }.mkString("\n") + val moveText = if moves.isEmpty then "" else buildMoveText(headers, moves) + if headerLines.isEmpty then moveText + else if moveText.isEmpty then headerLines + else s"$headerLines\n\n$moveText" + + private def buildMoveText(headers: Map[String, String], moves: List[Move]): String = + val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) + val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } + val grouped = sanMoves.zipWithIndex.groupBy(_._2 / 2) + val moveLines = grouped.toList.sortBy(_._1).map { case (n, pairs) => + val w = pairs.find(_._2 % 2 == 0).map(_._1).getOrElse("") + val b = pairs.find(_._2 % 2 == 1).map(_._1).getOrElse("") + if b.isEmpty then s"${n + 1}. $w" else s"${n + 1}. $w $b" + } + moveLines.mkString(" ") + s" ${headers.getOrElse("Result", "*")}" + + private def moveToAlgebraic(move: Move, boardBefore: Board): String = + move.moveType match + case MoveType.CastleKingside => "O-O" + case MoveType.CastleQueenside => "O-O-O" + case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}" + case MoveType.Promotion(pp) => + val suffix = pp match + case PromotionPiece.Queen => "=Q" + case PromotionPiece.Rook => "=R" + case PromotionPiece.Bishop => "=B" + case PromotionPiece.Knight => "=N" + val isCapture = boardBefore.pieceAt(move.to).isDefined + if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$suffix" + else s"${move.to}$suffix" + case MoveType.Normal(isCapture) => + val dest = move.to.toString + val capStr = if isCapture then "x" else "" + boardBefore.pieceAt(move.from).map(_.pieceType).getOrElse(PieceType.Pawn) match + case PieceType.Pawn => if isCapture then s"${move.from.file.toString.toLowerCase}x$dest" else dest + case PieceType.Knight => s"N$capStr$dest" + case PieceType.Bishop => s"B$capStr$dest" + case PieceType.Rook => s"R$capStr$dest" + case PieceType.Queen => s"Q$capStr$dest" + case PieceType.King => s"K$capStr$dest" diff --git a/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala b/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala deleted file mode 100644 index 5657cb3..0000000 --- a/modules/rule/src/main/scala/de/nowchess/rules/pgn/PgnExporter.scala +++ /dev/null @@ -1,58 +0,0 @@ -package de.nowchess.rules.pgn - -import de.nowchess.api.board.* -import de.nowchess.api.game.GameContext -import de.nowchess.api.io.GameContextExport -import de.nowchess.api.move.{Move, MoveType, PromotionPiece} -import de.nowchess.rules.sets.DefaultRules - -object PgnExporter extends GameContextExport: - - def exportGameContext(context: GameContext): String = - exportGame( - Map("Event" -> "?", "White" -> "?", "Black" -> "?", "Result" -> "*"), - context.moves, - ) - - def exportGame(headers: Map[String, String], moves: List[Move]): String = - val headerLines = headers.map { case (k, v) => s"""[$k "$v"]""" }.mkString("\n") - val moveText = if moves.isEmpty then "" else buildMoveText(headers, moves) - if headerLines.isEmpty then moveText - else if moveText.isEmpty then headerLines - else s"$headerLines\n\n$moveText" - - private def buildMoveText(headers: Map[String, String], moves: List[Move]): String = - val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) - val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } - val grouped = sanMoves.zipWithIndex.groupBy(_._2 / 2) - val moveLines = grouped.toList.sortBy(_._1).map { case (n, pairs) => - val w = pairs.find(_._2 % 2 == 0).map(_._1).getOrElse("") - val b = pairs.find(_._2 % 2 == 1).map(_._1).getOrElse("") - if b.isEmpty then s"${n + 1}. $w" else s"${n + 1}. $w $b" - } - moveLines.mkString(" ") + s" ${headers.getOrElse("Result", "*")}" - - private def moveToAlgebraic(move: Move, boardBefore: Board): String = - move.moveType match - case MoveType.CastleKingside => "O-O" - case MoveType.CastleQueenside => "O-O-O" - case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}" - case MoveType.Promotion(pp) => - val suffix = pp match - case PromotionPiece.Queen => "=Q" - case PromotionPiece.Rook => "=R" - case PromotionPiece.Bishop => "=B" - case PromotionPiece.Knight => "=N" - val isCapture = boardBefore.pieceAt(move.to).isDefined - if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$suffix" - else s"${move.to}$suffix" - case MoveType.Normal(isCapture) => - val dest = move.to.toString - val capStr = if isCapture then "x" else "" - boardBefore.pieceAt(move.from).map(_.pieceType).getOrElse(PieceType.Pawn) match - case PieceType.Pawn => if isCapture then s"${move.from.file.toString.toLowerCase}x$dest" else dest - case PieceType.Knight => s"N$capStr$dest" - case PieceType.Bishop => s"B$capStr$dest" - case PieceType.Rook => s"R$capStr$dest" - case PieceType.Queen => s"Q$capStr$dest" - case PieceType.King => s"K$capStr$dest" -- 2.52.0 From edbf63ebc591a7bf7f5dfcf5b01f89633dcc2a03 Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 21 Apr 2026 22:02:48 +0200 Subject: [PATCH 4/6] refactor: update import statements to use api.io for GameContextExport and GameContextImport --- .../de/nowchess/io/GameContextExport.scala | 3 - .../de/nowchess/io/GameContextImport.scala | 3 - .../de/nowchess/io/GameFileService.scala | 2 + .../de/nowchess/io/fen/FenExporter.scala | 27 ++++---- .../scala/de/nowchess/io/fen/FenParser.scala | 2 +- .../io/fen/FenParserCombinators.scala | 3 +- .../nowchess/io/fen/FenParserFastParse.scala | 2 +- .../de/nowchess/io/json/JsonExporter.scala | 3 +- .../de/nowchess/io/json/JsonParser.scala | 3 +- .../de/nowchess/io/pgn/PgnExporter.scala | 63 ++++++++++++------- .../scala/de/nowchess/io/pgn/PgnParser.scala | 2 +- 11 files changed, 68 insertions(+), 45 deletions(-) delete mode 100644 modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala delete mode 100644 modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala diff --git a/modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala b/modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala deleted file mode 100644 index 8cc51ef..0000000 --- a/modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.nowchess.io - -type GameContextExport = de.nowchess.api.io.GameContextExport diff --git a/modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala b/modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala deleted file mode 100644 index e2c60f6..0000000 --- a/modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala +++ /dev/null @@ -1,3 +0,0 @@ -package de.nowchess.io - -type GameContextImport = de.nowchess.api.io.GameContextImport diff --git a/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala b/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala index f84d4fb..9bbfc44 100644 --- a/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala +++ b/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala @@ -1,6 +1,8 @@ package de.nowchess.io import de.nowchess.api.game.GameContext +import de.nowchess.api.io.{GameContextExport, GameContextImport} + import java.nio.file.{Files, Path} import java.nio.charset.StandardCharsets import scala.util.Try diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala index 9e90afa..377459b 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala @@ -2,25 +2,17 @@ package de.nowchess.io.fen import de.nowchess.api.board.* import de.nowchess.api.game.GameContext -import de.nowchess.io.GameContextExport +import de.nowchess.api.io.GameContextExport object FenExporter extends GameContextExport: + /** Convert a Board to FEN piece-placement string (rank 8 to rank 1, separated by '/'). */ def boardToFen(board: Board): String = Rank.values.reverse .map(rank => buildRankString(board, rank)) .mkString("/") - def gameContextToFen(context: GameContext): String = - val piecePlacement = boardToFen(context.board) - val activeColor = if context.turn == Color.White then "w" else "b" - val castling = castlingString(context.castlingRights) - val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-") - val fullMoveNumber = 1 + (context.moves.length / 2) - s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber" - - def exportGameContext(context: GameContext): String = gameContextToFen(context) - + /** Build the FEN representation for a single rank. */ private def buildRankString(board: Board, rank: Rank): String = val rankSquares = File.values.map(file => Square(file, rank)) val (result, emptyCount) = rankSquares.foldLeft(("", 0)): @@ -33,6 +25,18 @@ object FenExporter extends GameContextExport: (acc, empty + 1) if emptyCount > 0 then result + emptyCount.toString else result + /** Convert a GameContext to a complete FEN string. */ + def gameContextToFen(context: GameContext): String = + val piecePlacement = boardToFen(context.board) + val activeColor = if context.turn == Color.White then "w" else "b" + val castling = castlingString(context.castlingRights) + val enPassant = context.enPassantSquare.map(_.toString).getOrElse("-") + val fullMoveNumber = 1 + (context.moves.length / 2) + s"$piecePlacement $activeColor $castling $enPassant ${context.halfMoveClock} $fullMoveNumber" + + def exportGameContext(context: GameContext): String = gameContextToFen(context) + + /** Convert castling rights to FEN notation. */ private def castlingString(rights: CastlingRights): String = val wk = if rights.whiteKingSide then "K" else "" val wq = if rights.whiteQueenSide then "Q" else "" @@ -41,6 +45,7 @@ object FenExporter extends GameContextExport: val result = s"$wk$wq$bk$bq" if result.isEmpty then "-" else result + /** Convert a Piece to its FEN character (uppercase = White, lowercase = Black). */ private def pieceToFenChar(piece: Piece): Char = val base = piece.pieceType match case PieceType.Pawn => 'p' diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala index 1f206ff..179ae95 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala @@ -2,7 +2,7 @@ package de.nowchess.io.fen import de.nowchess.api.board.* import de.nowchess.api.game.GameContext -import de.nowchess.io.GameContextImport +import de.nowchess.api.io.GameContextImport object FenParser extends GameContextImport: diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala index f51b46f..11c4416 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala @@ -2,9 +2,10 @@ package de.nowchess.io.fen import de.nowchess.api.board.* import de.nowchess.api.game.GameContext -import de.nowchess.io.GameContextImport + import scala.util.parsing.combinator.RegexParsers import FenParserSupport.* +import de.nowchess.api.io.GameContextImport object FenParserCombinators extends RegexParsers with GameContextImport: diff --git a/modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala b/modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala index 4a8ffca..5ed7aec 100644 --- a/modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala +++ b/modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala @@ -4,8 +4,8 @@ import fastparse.* import fastparse.NoWhitespace.* import de.nowchess.api.board.* import de.nowchess.api.game.GameContext -import de.nowchess.io.GameContextImport import FenParserSupport.* +import de.nowchess.api.io.GameContextImport object FenParserFastParse extends GameContextImport: diff --git a/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala b/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala index 44ba4cc..e33be19 100644 --- a/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala @@ -6,8 +6,9 @@ import com.fasterxml.jackson.module.scala.DefaultScalaModule import de.nowchess.api.board.* import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.game.GameContext -import de.nowchess.io.GameContextExport +import de.nowchess.api.io.GameContextExport import de.nowchess.io.pgn.PgnExporter + import java.time.{LocalDate, ZoneId, ZonedDateTime} /** Exports a GameContext to a comprehensive JSON format using Jackson. diff --git a/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala b/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala index ca283a6..1827434 100644 --- a/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala +++ b/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala @@ -5,7 +5,8 @@ import com.fasterxml.jackson.module.scala.DefaultScalaModule import de.nowchess.api.board.* import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.game.GameContext -import de.nowchess.io.GameContextImport +import de.nowchess.api.io.GameContextImport + import scala.util.Try /** Imports a GameContext from JSON format using Jackson. diff --git a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala index c95efa3..4a7b832 100644 --- a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala @@ -1,56 +1,75 @@ package de.nowchess.io.pgn -import de.nowchess.api.board.{Board, PieceType} +import de.nowchess.api.board.* +import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.game.GameContext import de.nowchess.api.io.GameContextExport -import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.rules.sets.DefaultRules object PgnExporter extends GameContextExport: + /** Export a GameContext to PGN format. */ def exportGameContext(context: GameContext): String = - exportGame( - Map("Event" -> "?", "White" -> "?", "Black" -> "?", "Result" -> "*"), - context.moves, + val headers = Map( + "Event" -> "?", + "White" -> "?", + "Black" -> "?", + "Result" -> "*", ) + exportGame(headers, context.moves) + + /** Export a game with headers and moves to PGN format. */ def exportGame(headers: Map[String, String], moves: List[Move]): String = - val headerLines = headers.map { case (k, v) => s"""[$k "$v"]""" }.mkString("\n") - val moveText = if moves.isEmpty then "" else buildMoveText(headers, moves) + val headerLines = headers + .map { case (key, value) => + s"""[$key "$value"]""" + } + .mkString("\n") + + val moveText = + if moves.isEmpty then "" + else + val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) + val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } + + val groupedMoves = sanMoves.zipWithIndex.groupBy(_._2 / 2) + val moveLines = for (moveNumber, movePairs) <- groupedMoves.toList.sortBy(_._1) yield + val moveNum = moveNumber + 1 + val whiteMoveStr = movePairs.find(_._2 % 2 == 0).map(_._1).getOrElse("") + val blackMoveStr = movePairs.find(_._2 % 2 == 1).map(_._1).getOrElse("") + if blackMoveStr.isEmpty then s"$moveNum. $whiteMoveStr" + else s"$moveNum. $whiteMoveStr $blackMoveStr" + + val termination = headers.getOrElse("Result", "*") + moveLines.mkString(" ") + s" $termination" + if headerLines.isEmpty then moveText else if moveText.isEmpty then headerLines else s"$headerLines\n\n$moveText" - private def buildMoveText(headers: Map[String, String], moves: List[Move]): String = - val contexts = moves.scanLeft(GameContext.initial)((ctx, move) => DefaultRules.applyMove(ctx)(move)) - val sanMoves = moves.zip(contexts).map { case (move, ctx) => moveToAlgebraic(move, ctx.board) } - val grouped = sanMoves.zipWithIndex.groupBy(_._2 / 2) - val moveLines = grouped.toList.sortBy(_._1).map { case (n, pairs) => - val w = pairs.find(_._2 % 2 == 0).map(_._1).getOrElse("") - val b = pairs.find(_._2 % 2 == 1).map(_._1).getOrElse("") - if b.isEmpty then s"${n + 1}. $w" else s"${n + 1}. $w $b" - } - moveLines.mkString(" ") + s" ${headers.getOrElse("Result", "*")}" - + /** Convert a Move to Standard Algebraic Notation using the board state before the move. */ private def moveToAlgebraic(move: Move, boardBefore: Board): String = move.moveType match case MoveType.CastleKingside => "O-O" case MoveType.CastleQueenside => "O-O-O" case MoveType.EnPassant => s"${move.from.file.toString.toLowerCase}x${move.to}" case MoveType.Promotion(pp) => - val suffix = pp match + val promSuffix = pp match case PromotionPiece.Queen => "=Q" case PromotionPiece.Rook => "=R" case PromotionPiece.Bishop => "=B" case PromotionPiece.Knight => "=N" val isCapture = boardBefore.pieceAt(move.to).isDefined - if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$suffix" - else s"${move.to}$suffix" + if isCapture then s"${move.from.file.toString.toLowerCase}x${move.to}$promSuffix" + else s"${move.to}$promSuffix" case MoveType.Normal(isCapture) => val dest = move.to.toString val capStr = if isCapture then "x" else "" boardBefore.pieceAt(move.from).map(_.pieceType).getOrElse(PieceType.Pawn) match - case PieceType.Pawn => if isCapture then s"${move.from.file.toString.toLowerCase}x$dest" else dest + case PieceType.Pawn => + if isCapture then s"${move.from.file.toString.toLowerCase}x$dest" + else dest case PieceType.Knight => s"N$capStr$dest" case PieceType.Bishop => s"B$capStr$dest" case PieceType.Rook => s"R$capStr$dest" diff --git a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala index 561e2da..27a81cd 100644 --- a/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala +++ b/modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala @@ -3,7 +3,7 @@ package de.nowchess.io.pgn import de.nowchess.api.board.* import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.game.GameContext -import de.nowchess.io.GameContextImport +import de.nowchess.api.io.GameContextImport import de.nowchess.rules.sets.DefaultRules /** A parsed PGN game containing headers and the resolved move list. */ -- 2.52.0 From 777db6ace2929eb1adda1995511ea0e307f43b93 Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 21 Apr 2026 22:08:09 +0200 Subject: [PATCH 5/6] refactor: update import statements to use api.io for GameContextExport and GameContextImport --- .../test/scala/de/nowchess/io/GameFileServiceSuite.scala | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala b/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala index 62d2f40..32056ca 100644 --- a/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala @@ -1,13 +1,14 @@ package de.nowchess.io -import de.nowchess.api.game.GameContext import de.nowchess.api.board.{File, Rank, Square} +import de.nowchess.api.game.GameContext +import de.nowchess.api.io.GameContextExport import de.nowchess.api.move.Move import de.nowchess.io.json.{JsonExporter, JsonParser} -import java.nio.file.{Files, Paths} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -import scala.util.Using + +import java.nio.file.{Files, Paths} class GameFileServiceSuite extends AnyFunSuite with Matchers: -- 2.52.0 From a380cff628385f918e35ba24b4f57c4350565280 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 22 Apr 2026 08:36:01 +0200 Subject: [PATCH 6/6] feat: add Dockerfiles for building Quarkus applications in various modes (native, JVM, legacy-jar, micro) --- .github/workflows/native-image.yml | 87 +++++++++++++++ .../core/src/main/docker/Dockerfile.native | 6 +- modules/io/src/main/docker/Dockerfile.jvm | 100 ++++++++++++++++++ .../io/src/main/docker/Dockerfile.legacy-jar | 96 +++++++++++++++++ modules/io/src/main/docker/Dockerfile.native | 29 +++++ .../src/main/docker/Dockerfile.native-micro | 32 ++++++ 6 files changed, 347 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/native-image.yml create mode 100644 modules/io/src/main/docker/Dockerfile.jvm create mode 100644 modules/io/src/main/docker/Dockerfile.legacy-jar create mode 100644 modules/io/src/main/docker/Dockerfile.native create mode 100644 modules/io/src/main/docker/Dockerfile.native-micro diff --git a/.github/workflows/native-image.yml b/.github/workflows/native-image.yml new file mode 100644 index 0000000..bf27e82 --- /dev/null +++ b/.github/workflows/native-image.yml @@ -0,0 +1,87 @@ +name: Build & Push Native Image + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + check-actor: + runs-on: ubuntu-latest + outputs: + allowed: ${{ steps.check.outputs.allowed }} + steps: + - id: check + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" || "${{ github.actor }}" == "TeamCity" ]]; then + echo "allowed=true" >> "$GITHUB_OUTPUT" + else + echo "allowed=false" >> "$GITHUB_OUTPUT" + fi + + build-and-push: + needs: check-actor + if: needs.check-actor.outputs.allowed == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + strategy: + matrix: + module: + - core + - io + + steps: + - uses: actions/checkout@v4 + + - name: Set up GraalVM + uses: graalvm/setup-graalvm@v1 + with: + java-version: '21' + distribution: 'graalvm-community' + native-image-job-reports: 'true' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }}- + + - name: Build native binary + run: ./gradlew :modules:${{ matrix.module }}:build -Dquarkus.native.enabled=true --no-daemon + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/now-chess/now-chess-systems/${{ matrix.module }} + tags: | + type=sha,prefix=,format=short + type=raw,value=latest + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: modules/${{ matrix.module }}/src/main/docker/Dockerfile.native + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=${{ matrix.module }} + cache-to: type=gha,mode=max,scope=${{ matrix.module }} diff --git a/modules/core/src/main/docker/Dockerfile.native b/modules/core/src/main/docker/Dockerfile.native index 57defbf..c5e5c1d 100644 --- a/modules/core/src/main/docker/Dockerfile.native +++ b/modules/core/src/main/docker/Dockerfile.native @@ -3,7 +3,7 @@ # # Before building the container image run: # -# ./gradlew build -Dquarkus.native.enabled=true +# ./gradlew :modules:core:build -Dquarkus.native.enabled=true # # Then, build the image with: # @@ -13,7 +13,7 @@ # # docker run -i --rm -p 8080:8080 quarkus/backcore # -# The ` registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9. +# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9. # To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`. ### FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7 @@ -21,7 +21,7 @@ WORKDIR /work/ RUN chown 1001 /work \ && chmod "g+rwX" /work \ && chown 1001:root /work -COPY --chown=1001:root --chmod=0755 build/*-runner /work/application +COPY --chown=1001:root --chmod=0755 modules/core/build/*-runner /work/application EXPOSE 8080 USER 1001 diff --git a/modules/io/src/main/docker/Dockerfile.jvm b/modules/io/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..c3c09fc --- /dev/null +++ b/modules/io/src/main/docker/Dockerfile.jvm @@ -0,0 +1,100 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/backcore-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backcore-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/backcore-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override +# the default JVM options, use `JAVA_OPTS_APPEND` to append options +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +# You can find more information about the UBI base runtime images and their configuration here: +# https://rh-openjdk.github.io/redhat-openjdk-containers/ +### +FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24 + +ENV LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 build/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 build/quarkus-app/*.jar /deployments/ +COPY --chown=185 build/quarkus-app/app/ /deployments/app/ +COPY --chown=185 build/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] + diff --git a/modules/io/src/main/docker/Dockerfile.legacy-jar b/modules/io/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..8c89666 --- /dev/null +++ b/modules/io/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,96 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.package.jar.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/backcore-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backcore-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005. +# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005 +# when running the container +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/backcore-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override +# the default JVM options, use `JAVA_OPTS_APPEND` to append options +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +# You can find more information about the UBI base runtime images and their configuration here: +# https://rh-openjdk.github.io/redhat-openjdk-containers/ +### +FROM registry.access.redhat.com/ubi9/openjdk-21-runtime:1.24 + +ENV LANGUAGE='en_US:en' + + +COPY build/lib/* /deployments/lib/ +COPY build/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + +ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ] diff --git a/modules/io/src/main/docker/Dockerfile.native b/modules/io/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..a6d6afd --- /dev/null +++ b/modules/io/src/main/docker/Dockerfile.native @@ -0,0 +1,29 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./gradlew :modules:io:build -Dquarkus.native.enabled=true +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/backio . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backio +# +# The `registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9. +# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`. +### +FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root --chmod=0755 modules/io/build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/modules/io/src/main/docker/Dockerfile.native-micro b/modules/io/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..9408243 --- /dev/null +++ b/modules/io/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,32 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./gradlew build -Dquarkus.native.enabled=true +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/backcore . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/backcore +# +# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9. +# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`. +### +FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root --chmod=0755 build/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"] -- 2.52.0