WIP: feat: NCS-37 Quarkus integration #29

Closed
lq64 wants to merge 13 commits from feat/NCS-37 into main
75 changed files with 114991 additions and 123 deletions
+33 -24
View File
@@ -2,8 +2,8 @@
> **Stack:** raw-http | none | unknown | scala
> 0 routes | 0 models | 0 components | 35 lib files | 0 env vars | 0 middleware
> **Token savings:** this file is ~3.700 tokens. Without it, AI exploration would cost ~18.200 tokens. **Saves ~14.500 tokens per conversation.**
> 0 routes | 0 models | 0 components | 38 lib files | 0 env vars | 0 middleware
> **Token savings:** this file is ~0 tokens. Without it, AI exploration would cost ~0 tokens. **Saves ~0 tokens per conversation.**
---
@@ -54,7 +54,7 @@
- function withEnPassantSquare
- function withHalfMoveClock
- function withMove
- _...2 more_
- _...3 more_
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — class PlayerId, function apply
- `modules/api/src/main/scala/de/nowchess/api/response/ApiResponse.scala`
- class ApiResponse
@@ -93,6 +93,13 @@
- _...1 more_
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — class GameContextImport, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameFileService.scala`
- class GameFileService
- function saveGameToFile
- function loadGameFromFile
- class FileSystemGameService
- function saveGameToFile
- function loadGameFromFile
- `modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala`
- class FenExporter
- function boardToFen
@@ -114,6 +121,8 @@
- function parseBoard
- function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — function buildSquares
- `modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala` — class JsonExporter, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala` — class JsonParser, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala`
- class PgnExporter
- function exportGameContext
@@ -164,39 +173,39 @@
## Most Imported Files (change these carefully)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **28** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **21** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **19** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **14** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **13** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **10** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **38** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **26** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **25** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **21** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **14** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **13** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **13** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **9** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **8** files
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **8** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **4** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **4** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala` — imported by **7** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **5** files
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **4** files
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **4** files
- `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala` — imported by **4** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala` — imported by **3** files
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala` — imported by **3** files
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala` — imported by **2** files
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala` — imported by **2** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala` — imported by **2** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — imported by **2** files
- `modules/core/src/main/scala/de/nowchess/chess/controller/Parser.scala` — imported by **1** files
## Import Map (who imports what)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerBranchTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerTest.scala` +23 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala` +16 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala` +14 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala` +9 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineGameEndingTest.scala` +8 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineScenarioTest.scala`, `modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesStateTransitionsTest.scala` +5 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesStateTransitionsTest.scala`, `modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesTest.scala` +4 more
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerBranchTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerTest.scala` +33 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala` +21 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` +20 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala` +16 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala` +9 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineScenarioTest.scala`, `modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala` +8 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala`, `modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala` +8 more
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala``modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineScenarioTest.scala` +4 more
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala` +3 more
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala` +3 more
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala` +2 more
---
+21 -21
View File
@@ -2,36 +2,36 @@
## Most Imported Files (change these carefully)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **28** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **21** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **19** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **14** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **13** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **10** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **9** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala` — imported by **38** files
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala` — imported by **26** files
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala` — imported by **25** files
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala` — imported by **21** files
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — imported by **14** files
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala` — imported by **13** files
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala` — imported by **13** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala` — imported by **9** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **8** files
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala` — imported by **8** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **4** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **4** files
- `modules/api/src/main/scala/de/nowchess/api/board/CastlingRights.scala` — imported by **7** files
- `modules/api/src/main/scala/de/nowchess/api/game/DrawReason.scala` — imported by **7** files
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — imported by **5** files
- `modules/rule/src/main/scala/de/nowchess/rules/RuleSet.scala` — imported by **4** files
- `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` — imported by **4** files
- `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala` — imported by **4** files
- `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala` — imported by **3** files
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala` — imported by **3** files
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnParser.scala` — imported by **2** files
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala` — imported by **2** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala` — imported by **2** files
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — imported by **2** files
- `modules/core/src/main/scala/de/nowchess/chess/controller/Parser.scala` — imported by **1** files
## Import Map (who imports what)
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerBranchTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerTest.scala` +23 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala` +16 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala` +14 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala` +9 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineGameEndingTest.scala` +8 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineScenarioTest.scala`, `modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesStateTransitionsTest.scala` +5 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesStateTransitionsTest.scala`, `modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesTest.scala` +4 more
- `modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerBranchTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/command/CommandInvokerTest.scala` +33 more
- `modules/api/src/main/scala/de/nowchess/api/board/Square.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/move/Move.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/api/src/test/scala/de/nowchess/api/move/MoveTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/command/Command.scala` +21 more
- `modules/api/src/main/scala/de/nowchess/api/board/Color.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/main/scala/de/nowchess/api/game/GameResult.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` +20 more
- `modules/api/src/main/scala/de/nowchess/api/move/Move.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/board/BoardTest.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala` +16 more
- `modules/api/src/main/scala/de/nowchess/api/board/Board.scala``modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala`, `modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala` +9 more
- `modules/api/src/main/scala/de/nowchess/api/board/Piece.scala``modules/core/src/main/scala/de/nowchess/chess/command/Command.scala`, `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineScenarioTest.scala`, `modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala` +8 more
- `modules/api/src/main/scala/de/nowchess/api/board/PieceType.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala`, `modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala` +8 more
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala``modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadGameTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineNotationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineScenarioTest.scala` +4 more
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala` +3 more
- `modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/EngineTestHelpers.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEnginePromotionTest.scala`, `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala` +3 more
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala``modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala`, `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineIntegrationTest.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParser.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserCombinators.scala`, `modules/io/src/main/scala/de/nowchess/io/fen/FenParserFastParse.scala` +2 more
+10 -1
View File
@@ -45,7 +45,7 @@
- function withEnPassantSquare
- function withHalfMoveClock
- function withMove
- _...2 more_
- _...3 more_
- `modules/api/src/main/scala/de/nowchess/api/player/PlayerInfo.scala` — class PlayerId, function apply
- `modules/api/src/main/scala/de/nowchess/api/response/ApiResponse.scala`
- class ApiResponse
@@ -84,6 +84,13 @@
- _...1 more_
- `modules/io/src/main/scala/de/nowchess/io/GameContextExport.scala` — class GameContextExport, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameContextImport.scala` — class GameContextImport, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/GameFileService.scala`
- class GameFileService
- function saveGameToFile
- function loadGameFromFile
- class FileSystemGameService
- function saveGameToFile
- function loadGameFromFile
- `modules/io/src/main/scala/de/nowchess/io/fen/FenExporter.scala`
- class FenExporter
- function boardToFen
@@ -105,6 +112,8 @@
- function parseBoard
- function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/fen/FenParserSupport.scala` — function buildSquares
- `modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala` — class JsonExporter, function exportGameContext
- `modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala` — class JsonParser, function importGameContext
- `modules/io/src/main/scala/de/nowchess/io/pgn/PgnExporter.scala`
- class PgnExporter
- function exportGameContext
+133
View File
@@ -0,0 +1,133 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<AndroidXmlCodeStyleSettings>
<option name="USE_CUSTOM_SETTINGS" value="true" />
</AndroidXmlCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<ScalaCodeStyleSettings>
<option name="FORMATTER" value="1" />
</ScalaCodeStyleSettings>
<XML>
<option name="XML_KEEP_LINE_BREAKS" value="false" />
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
<option name="XML_SPACE_INSIDE_EMPTY_TAG" value="true" />
</XML>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+1 -1
View File
@@ -5,7 +5,7 @@
<option name="deprecationWarnings" value="true" />
<option name="uncheckedWarnings" value="true" />
</profile>
<profile name="Gradle 2" modules="NowChessSystems.modules.core.main,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.main,NowChessSystems.modules.io.scoverage,NowChessSystems.modules.io.test,NowChessSystems.modules.rule.main,NowChessSystems.modules.rule.scoverage,NowChessSystems.modules.rule.test,NowChessSystems.modules.ui.main,NowChessSystems.modules.ui.scoverage,NowChessSystems.modules.ui.test">
<profile name="Gradle 2" modules="NowChessSystems.modules.backcore.integrationTest,NowChessSystems.modules.backcore.main,NowChessSystems.modules.backcore.native-test,NowChessSystems.modules.backcore.quarkus-generated-sources,NowChessSystems.modules.backcore.quarkus-test-generated-sources,NowChessSystems.modules.backcore.scoverage,NowChessSystems.modules.backcore.test,NowChessSystems.modules.core.main,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.main,NowChessSystems.modules.io.scoverage,NowChessSystems.modules.io.test,NowChessSystems.modules.rule.main,NowChessSystems.modules.rule.scoverage,NowChessSystems.modules.rule.test,NowChessSystems.modules.ui.main,NowChessSystems.modules.ui.scoverage,NowChessSystems.modules.ui.test">
<option name="deprecationWarnings" value="true" />
<option name="uncheckedWarnings" value="true" />
<parameters>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ScalaProjectSettings">
<option name="scala3DisclaimerShown" value="true" />
</component>
</project>
Generated
-6
View File
@@ -1,11 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CommitMessageInspectionProfile">
<profile version="1.0">
<inspection_tool class="CommitFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="CommitNamingConvention" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
<component name="IssueNavigationConfiguration">
<option name="links">
<list>
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Draw offer
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/offer
body: none
auth: none
}
headers {
Accept: application/json
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Draw accept
type: http
seq: 2
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/accept
body: none
auth: none
}
headers {
Accept: application/json
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Draw decline
type: http
seq: 3
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/decline
body: none
auth: none
}
headers {
Accept: application/json
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Draw claim fifty move
type: http
seq: 4
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/draw/claim
body: none
auth: none
}
headers {
Accept: application/json
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Export FEN
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/board/game/{{gameId}}/export/fen
body: none
auth: none
}
headers {
Accept: text/plain
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Export PGN
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/board/game/{{gameId}}/export/pgn
body: none
auth: none
}
headers {
Accept: application/x-chess-pgn
}
+30
View File
@@ -0,0 +1,30 @@
meta {
name: Create game
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/board/game
body: json
auth: none
}
headers {
Accept: application/json
Content-Type: application/json
}
body:json {
{
"white": {
"id": "player1",
"displayName": "Alice"
},
"black": {
"id": "player2",
"displayName": "Bob"
}
}
}
+19
View File
@@ -0,0 +1,19 @@
meta {
name: Get game
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/board/game/{{gameId}}
body: none
auth: none
}
headers {
Accept: application/json
}
vars:pre-request {
gameId: DSooMdhj
}
+16
View File
@@ -0,0 +1,16 @@
meta {
name: Stream game (NDJSON)
type: http
seq: 3
}
get {
url: {{baseUrl}}/api/board/game/{{gameId}}/stream
body: none
auth: none
}
headers {
Accept: application/x-ndjson
}
+19
View File
@@ -0,0 +1,19 @@
meta {
name: Resign game
type: http
seq: 4
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/resign
body: none
auth: none
}
headers {
Accept: application/json
}
vars:pre-request {
gameId: DSooMdhj
}
+31
View File
@@ -0,0 +1,31 @@
meta {
name: Import FEN
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/board/game/import/fen
body: json
auth: none
}
headers {
Accept: application/json
Content-Type: application/json
}
body:json {
{
"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
"white": {
"id": "player1",
"displayName": "Alice"
},
"black": {
"id": "player2",
"displayName": "Bob"
}
}
}
+23
View File
@@ -0,0 +1,23 @@
meta {
name: Import PGN
type: http
seq: 2
}
post {
url: {{baseUrl}}/api/board/game/import/pgn
body: json
auth: none
}
headers {
Accept: application/json
Content-Type: application/json
}
body:json {
{
"pgn": "1. e4 e5 2. Nf3 Nc6 *"
}
}
+20
View File
@@ -0,0 +1,20 @@
meta {
name: Make move
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/move/{{uci}}
body: none
auth: none
}
headers {
Accept: application/json
}
vars:pre-request {
gameId: DSooMdhj
uci: b1c3
}
@@ -0,0 +1,23 @@
meta {
name: Get legal moves
type: http
seq: 2
}
get {
url: {{baseUrl}}/api/board/game/{{gameId}}/moves
body: none
auth: none
}
params:query {
square: {{square}}
}
headers {
Accept: application/json
}
vars:pre-request {
gameId: DSooMdhj
}
+19
View File
@@ -0,0 +1,19 @@
meta {
name: Undo move
type: http
seq: 3
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/undo
body: none
auth: none
}
headers {
Accept: application/json
}
vars:pre-request {
gameId: DSooMdhj
}
+19
View File
@@ -0,0 +1,19 @@
meta {
name: Redo move
type: http
seq: 4
}
post {
url: {{baseUrl}}/api/board/game/{{gameId}}/redo
body: none
auth: none
}
headers {
Accept: application/json
}
vars:pre-request {
gameId: DSooMdhj
}
+6
View File
@@ -0,0 +1,6 @@
{
"version": "1",
"name": "NowChessSystems",
"type": "collection"
}
+3
View File
@@ -0,0 +1,3 @@
vars {
baseUrl: http://localhost:8080
}
+8
View File
@@ -21,7 +21,15 @@ sonar {
if (report.exists()) report.absolutePath else null
}.joinToString(",")
val jacocoReports = subprojects.mapNotNull { subproject ->
val report = subproject.file("build/reports/jacoco/test/jacocoTestReport.xml")
if (report.exists()) report.absolutePath else null
}.joinToString(",")
property("sonar.scala.coverage.reportPaths", scoverageReports)
if (jacocoReports.isNotEmpty()) {
property("sonar.coverage.jacoco.xmlReportPaths", jacocoReports)
}
}
}
+5
View File
@@ -0,0 +1,5 @@
quarkusPluginId=io.quarkus
quarkusPluginVersion=3.32.4
quarkusPlatformGroupId=io.quarkus.platform
quarkusPlatformArtifactId=quarkus-bom
quarkusPlatformVersion=3.32.4
+5
View File
@@ -0,0 +1,5 @@
*
!build/*-runner
!build/*-runner.jar
!build/lib/*
!build/quarkus-app/*
+41
View File
@@ -0,0 +1,41 @@
# Gradle
.gradle/
build/
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.vscode
.factorypath
# OSX
.DS_Store
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
# Local environment
.env
# Plugin directory
/.quarkus/cli/plugins/
# TLS Certificates
.certs/
+106
View File
@@ -0,0 +1,106 @@
# backcore
This project uses Quarkus, the Supersonic Subatomic Java Framework.
If you want to learn more about Quarkus, please visit its website: <https://quarkus.io/>.
## Running the application in dev mode
You can run your application in dev mode that enables live coding using:
```shell script
./gradlew quarkusDev
```
> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at <http://localhost:8080/q/dev/>.
## Packaging and running the application
The application can be packaged using:
```shell script
./gradlew build
```
It produces the `quarkus-run.jar` file in the `build/quarkus-app/` directory.
Be aware that its not an _über-jar_ as the dependencies are copied into the `build/quarkus-app/lib/` directory.
The application is now runnable using `java -jar build/quarkus-app/quarkus-run.jar`.
If you want to build an _über-jar_, execute the following command:
```shell script
./gradlew build -Dquarkus.package.jar.type=uber-jar
```
The application, packaged as an _über-jar_, is now runnable using `java -jar build/*-runner.jar`.
## Creating a native executable
You can create a native executable using:
```shell script
./gradlew build -Dquarkus.native.enabled=true
```
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
```shell script
./gradlew build -Dquarkus.native.enabled=true -Dquarkus.native.container-build=true
```
You can then execute your native executable with: `./build/backcore-1.0-SNAPSHOT-runner`
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/gradle-tooling>.
## Related Guides
- REST ([guide](https://quarkus.io/guides/rest)): A Jakarta REST implementation utilizing build time processing and
Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on
it.
- Hibernate ORM ([guide](https://quarkus.io/guides/hibernate-orm)): Define your persistent model with Hibernate ORM and
Jakarta Persistence
- REST Client ([guide](https://quarkus.io/guides/rest-client)): Call REST services
- REST Jackson ([guide](https://quarkus.io/guides/rest#json-serialisation)): Jackson serialization support for Quarkus
REST. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it
- YAML Configuration ([guide](https://quarkus.io/guides/config-yaml)): Use YAML to configure your Quarkus application
- SmallRye Fault Tolerance ([guide](https://quarkus.io/guides/smallrye-fault-tolerance)): Build fault-tolerant network
services
- SmallRye JWT ([guide](https://quarkus.io/guides/security-jwt)): Secure your applications with JSON Web Token
- SmallRye Health ([guide](https://quarkus.io/guides/smallrye-health)): Monitor service health
- Micrometer metrics ([guide](https://quarkus.io/guides/micrometer)): Instrument the runtime and your application with
dimensional metrics using Micrometer.
## Provided Code
### YAML Config
Configure your application with YAML
[Related guide section...](https://quarkus.io/guides/config-reference#configuration-examples)
The Quarkus application configuration is located in `src/main/resources/application.yml`.
### Hibernate ORM
Create your first JPA entity
[Related guide section...](https://quarkus.io/guides/hibernate-orm)
### REST Client
Invoke different services through REST with JSON
[Related guide section...](https://quarkus.io/guides/rest-client)
### REST
Easily start your REST Web Services
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)
### SmallRye Health
Monitor your application's health using SmallRye Health
[Related guide section...](https://quarkus.io/guides/smallrye-health)
+135
View File
@@ -0,0 +1,135 @@
plugins {
id("scala")
id("org.scoverage") version "8.1"
id("io.quarkus")
id("jacoco")
}
@Suppress("UNCHECKED_CAST")
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
repositories {
mavenCentral()
mavenLocal()
}
scala {
scalaVersion = versions["SCALA3"]!!
}
scoverage {
scoverageVersion.set(versions["SCOVERAGE"]!!)
}
tasks.withType<ScalaCompile> {
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
}
val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project
dependencies {
implementation(project(":modules:api"))
implementation(project(":modules:core"))
implementation(project(":modules:io"))
implementation(project(":modules:rule"))
implementation(project(":modules:ui"))
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-rest")
implementation("io.quarkus:quarkus-rest-jackson")
implementation("io.quarkus:quarkus-config-yaml")
implementation("io.quarkus:quarkus-arc")
implementation("io.quarkus:quarkus-smallrye-openapi")
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
testImplementation("co.helmethair:scalatest-junit-runner:${versions["SCALATEST_JUNIT"]!!}")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.quarkus:quarkus-jacoco")
testImplementation("io.rest-assured:rest-assured")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
configurations.scoverage {
resolutionStrategy.eachDependency {
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
}
}
}
group = "de.nowchess"
version = "1.0-SNAPSHOT"
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
options.compilerArgs.add("-parameters")
}
tasks.withType<Jar>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
tasks.test {
useJUnitPlatform {
includeEngines("scalatest", "junit-jupiter")
}
testLogging {
events("passed", "skipped", "failed")
}
finalizedBy(tasks.named("jacocoTestReport"))
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
executionData.setFrom(
layout.buildDirectory.file("jacoco-quarkus.exec"),
layout.buildDirectory.file("jacoco/test.exec"),
)
sourceDirectories.setFrom(files("src/main/scala"))
classDirectories.setFrom(
files(layout.buildDirectory.dir("classes/scala/main")).asFileTree.matching {
exclude(
// App entrypoint (intentionally excluded)
"**/AppMain*.class", "**/AppMain\$*.class",
// DTO companion objects — only framework synthetics (writeReplace, fromProduct, unapply)
"**/dto/GameStateResponse\$.class",
"**/dto/PlayerInfoDto\$.class",
"**/dto/LegalMovesResponse\$.class",
"**/dto/ImportFenRequest\$.class",
"**/dto/CreateGameRequest\$.class",
"**/dto/OkResponse\$.class",
"**/dto/LegalMoveDto\$.class",
"**/dto/GameFullResponse\$.class",
"**/dto/ImportPgnRequest\$.class",
"**/dto/ApiErrorResponse\$.class",
// Private implementation detail — inaccessible from tests
"**/game/ServiceState.class", "**/game/ServiceState\$.class",
// GameResult: sealed trait companion + case object singletons (only synthetics)
"**/game/GameResult\$.class",
"**/game/GameResult\$AgreedDraw\$.class",
"**/game/GameResult\$FiftyMoveDraw\$.class",
// GameResult.Resign companion (writeReplace, fromProduct; instance class kept)
"**/game/GameResult\$Resign\$.class",
// Other companion objects with only framework synthetics
"**/game/GameId\$.class",
"**/game/GameSnapshot\$.class",
)
}
)
reports {
xml.required.set(true)
xml.outputLocation.set(
layout.buildDirectory.file("reports/jacoco/test/jacocoTestReport.xml")
)
html.required.set(false)
}
}
@@ -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" ]
@@ -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" ]
@@ -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 build -Dquarkus.native.enabled=true
#
# Then, build the image with:
#
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backcore .
#
# Then run the container using:
#
# 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.
# 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 build/*-runner /work/application
EXPOSE 8080
USER 1001
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
@@ -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"]
@@ -0,0 +1,6 @@
quarkus:
http:
port: 8080
jacoco:
data-file: ${user.dir}/build/jacoco-quarkus.exec
report: false
@@ -0,0 +1,17 @@
package de.nowchess.backcore
import de.nowchess.backcore.game.{GameEngineHolder, GameId}
import de.nowchess.ui.gui.ChessGUILauncher
import de.nowchess.ui.terminal.TerminalUI
import io.quarkus.runtime.annotations.QuarkusMain
import io.quarkus.runtime.{Quarkus, QuarkusApplication}
@QuarkusMain
class AppMain extends QuarkusApplication:
override def run(args: String*): Int =
val engine = GameEngineHolder.engine
println(s"REST API -> http://localhost:8080/api/board/game/${GameEngineHolder.gameId}")
ChessGUILauncher.launch(engine)
new TerminalUI(engine).start()
Quarkus.asyncExit()
0
@@ -0,0 +1,11 @@
package de.nowchess.backcore.config
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import io.quarkus.jackson.ObjectMapperCustomizer
import jakarta.inject.Singleton
@Singleton
class JacksonConfig extends ObjectMapperCustomizer:
def customize(mapper: ObjectMapper): Unit =
mapper.registerModule(DefaultScalaModule)
@@ -0,0 +1,57 @@
package de.nowchess.backcore.dto
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonInclude.Include
case class PlayerInfoDto(id: String, displayName: String)
case class GameStateResponse(
fen: String,
pgn: String,
turn: String,
status: String,
@JsonInclude(Include.NON_ABSENT) winner: Option[String],
moves: List[String],
undoAvailable: Boolean,
redoAvailable: Boolean,
)
case class GameFullResponse(
gameId: String,
white: PlayerInfoDto,
black: PlayerInfoDto,
state: GameStateResponse,
)
case class OkResponse(ok: Boolean = true)
@JsonInclude(Include.NON_ABSENT)
case class ApiErrorResponse(
code: String,
message: String,
field: Option[String] = None,
)
// Requests
case class CreateGameRequest(
white: Option[PlayerInfoDto] = None,
black: Option[PlayerInfoDto] = None,
)
case class ImportFenRequest(
fen: String = "",
white: Option[PlayerInfoDto] = None,
black: Option[PlayerInfoDto] = None,
)
case class ImportPgnRequest(pgn: String = "")
case class LegalMoveDto(
from: String,
to: String,
uci: String,
moveType: String,
@JsonInclude(Include.NON_ABSENT) promotion: Option[String] = None,
)
case class LegalMovesResponse(moves: List[LegalMoveDto])
@@ -0,0 +1,28 @@
package de.nowchess.backcore.game
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.chess.engine.GameEngine
import java.util.concurrent.atomic.AtomicReference
/** Singleton holder that bridges an externally-created GameEngine into the CDI context.
*
* All fields are `val` wrapping an AtomicReference. Set engine / white / black before starting Quarkus. In
* standalone-server or test mode the defaults are used; GameService is created lazily (on first request), so the
* values are already set by the time CDI constructs it.
*/
object GameEngineHolder:
private val engineRef = new AtomicReference[GameEngine](new GameEngine())
private val whiteRef = new AtomicReference[PlayerInfo](PlayerInfo(PlayerId("white"), "White"))
private val blackRef = new AtomicReference[PlayerInfo](PlayerInfo(PlayerId("black"), "Black"))
val gameId: String = GameId.generate()
def engine: GameEngine = engineRef.get()
def engine_=(e: GameEngine): Unit = engineRef.set(e)
def white: PlayerInfo = whiteRef.get()
def white_=(p: PlayerInfo): Unit = whiteRef.set(p)
def black: PlayerInfo = blackRef.get()
def black_=(p: PlayerInfo): Unit = blackRef.set(p)
@@ -0,0 +1,10 @@
package de.nowchess.backcore.game
import java.security.SecureRandom
object GameId:
private val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
private val random = SecureRandom()
def generate(): String =
(1 to 8).map(_ => chars(random.nextInt(chars.length))).mkString
@@ -0,0 +1,79 @@
package de.nowchess.backcore.game
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.Color
import de.nowchess.api.game.{DrawReason, GameResult as ApiGameResult}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.backcore.dto.*
import de.nowchess.io.fen.FenExporter
import de.nowchess.io.pgn.PgnExporter
import de.nowchess.rules.sets.DefaultRules
object GameMapper:
private val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
def toGameFullJson(snapshot: GameSnapshot): String =
mapper.writeValueAsString(toGameFull(snapshot))
def toGameFull(snapshot: GameSnapshot): GameFullResponse =
GameFullResponse(
gameId = snapshot.gameId,
white = toPlayerInfo(snapshot.white),
black = toPlayerInfo(snapshot.black),
state = toGameState(snapshot),
)
def toGameState(snapshot: GameSnapshot): GameStateResponse =
val (status, winner) = computeStatus(snapshot)
GameStateResponse(
fen = FenExporter.exportGameContext(snapshot.context),
pgn = buildPgn(snapshot.context.moves),
turn = if snapshot.context.turn == Color.White then "white" else "black",
status = status,
winner = winner,
moves = snapshot.context.moves.map(moveToUci),
undoAvailable = snapshot.canUndo,
redoAvailable = snapshot.canRedo,
)
private def toPlayerInfo(p: de.nowchess.api.player.PlayerInfo): PlayerInfoDto =
PlayerInfoDto(id = p.id.value, displayName = p.displayName)
private def colorStr(c: Color): String = if c == Color.White then "white" else "black"
private def computeStatus(snapshot: GameSnapshot): (String, Option[String]) =
snapshot.externalResult match
case Some(GameResult.Resign(winner)) =>
("resign", Some(colorStr(winner)))
case Some(GameResult.AgreedDraw) | Some(GameResult.FiftyMoveDraw) =>
("draw", None)
case _ =>
snapshot.context.result match
case Some(ApiGameResult.Win(winner)) => ("checkmate", Some(colorStr(winner)))
case Some(ApiGameResult.Draw(DrawReason.Stalemate)) => ("stalemate", None)
case Some(ApiGameResult.Draw(DrawReason.InsufficientMaterial)) => ("insufficientMaterial", None)
case Some(ApiGameResult.Draw(_)) => ("draw", None)
case None => computeLiveStatus(snapshot)
private def computeLiveStatus(snapshot: GameSnapshot): (String, Option[String]) =
val ctx = snapshot.context
if DefaultRules.isCheck(ctx) then ("check", None)
else if snapshot.drawOfferedBy.isDefined then ("drawOffered", None)
else if DefaultRules.isFiftyMoveRule(ctx) then ("fiftyMoveAvailable", None)
else ("started", None)
def moveToUci(move: Move): String =
val base = s"${move.from}${move.to}"
move.moveType match
case MoveType.Promotion(piece) =>
val suffix = piece match
case PromotionPiece.Queen => "q"
case PromotionPiece.Rook => "r"
case PromotionPiece.Bishop => "b"
case PromotionPiece.Knight => "n"
base + suffix
case _ => base
private def buildPgn(moves: List[Move]): String =
PgnExporter.exportGame(Map.empty, moves)
@@ -0,0 +1,9 @@
package de.nowchess.backcore.game
import de.nowchess.api.board.Color
sealed trait GameResult
object GameResult:
case class Resign(winner: Color) extends GameResult
case object AgreedDraw extends GameResult
case object FiftyMoveDraw extends GameResult
@@ -0,0 +1,175 @@
package de.nowchess.backcore.game
import de.nowchess.api.board.{Color, Square}
import de.nowchess.api.game.GameContext
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.backcore.dto.{CreateGameRequest, ImportFenRequest, PlayerInfoDto}
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import de.nowchess.rules.sets.DefaultRules
import jakarta.enterprise.context.ApplicationScoped
private case class ServiceState(
drawOfferedBy: Option[Color] = None,
externalResult: Option[GameResult] = None,
)
@ApplicationScoped
class GameService:
private val engine = GameEngineHolder.engine
@SuppressWarnings(Array("DisableSyntax.var"))
private var state: ServiceState = ServiceState()
def isKnownId(id: String): Boolean = id == GameEngineHolder.gameId
def getSnapshot: GameSnapshot = synchronized:
GameSnapshot(
gameId = GameEngineHolder.gameId,
white = GameEngineHolder.white,
black = GameEngineHolder.black,
context = engine.context,
drawOfferedBy = state.drawOfferedBy,
externalResult = state.externalResult,
canUndo = engine.canUndo,
canRedo = engine.canRedo,
)
def reset(req: CreateGameRequest): GameSnapshot = synchronized:
engine.reset()
state = ServiceState()
GameEngineHolder.white = toPlayerInfo(req.white, "white", "White")
GameEngineHolder.black = toPlayerInfo(req.black, "black", "Black")
getSnapshot
def applyMove(uci: String): Either[String, GameSnapshot] = synchronized:
if hasEnded then Left("Game is already over")
else
parseUci(uci) match
case None => Left(s"Invalid UCI notation: $uci")
case Some((from, to, promotion)) =>
val candidates = engine.ruleSet.legalMoves(engine.context)(from)
findMatchingMove(candidates, to, promotion) match
case None => Left(s"$uci is not a legal move")
case Some(move) =>
engine.processUserInput(s"$from$to")
promotion.foreach(engine.completePromotion)
Right(getSnapshot)
def legalMoves(square: Option[Square]): List[Move] = synchronized:
val ctx = engine.context
square match
case Some(sq) => engine.ruleSet.legalMoves(ctx)(sq)
case None => engine.ruleSet.allLegalMoves(ctx)
def undo(): Either[String, GameSnapshot] = synchronized:
if !engine.canUndo then Left("No moves to undo")
else
engine.undo()
state = state.copy(externalResult = None, drawOfferedBy = None)
Right(getSnapshot)
def redo(): Either[String, GameSnapshot] = synchronized:
if !engine.canRedo then Left("No moves to redo")
else
engine.redo()
Right(getSnapshot)
def resign(): Either[String, GameSnapshot] = synchronized:
if hasEnded then Left("Game is already over")
else
val winner = engine.context.turn.opposite
state = state.copy(externalResult = Some(GameResult.Resign(winner)))
Right(getSnapshot)
def drawAction(action: String): Either[String, GameSnapshot] = synchronized:
if hasEnded then Left("Game is already over")
else
action match
case "offer" =>
state = state.copy(drawOfferedBy = Some(engine.context.turn))
Right(getSnapshot)
case "accept" =>
state.drawOfferedBy match
case None => Left("No draw offer to accept")
case Some(offerer) if offerer == engine.context.turn =>
Left("Cannot accept your own draw offer")
case Some(_) =>
state = state.copy(externalResult = Some(GameResult.AgreedDraw), drawOfferedBy = None)
Right(getSnapshot)
case "decline" =>
state.drawOfferedBy match
case None => Left("No draw offer to decline")
case Some(_) =>
state = state.copy(drawOfferedBy = None)
Right(getSnapshot)
case "claim" =>
if DefaultRules.isFiftyMoveRule(engine.context) then
state = state.copy(externalResult = Some(GameResult.FiftyMoveDraw))
Right(getSnapshot)
else Left("Fifty-move rule has not been triggered")
case other => Left(s"Unknown draw action: $other")
def importFen(req: ImportFenRequest): Either[String, GameSnapshot] = synchronized:
FenParser.parseFen(req.fen) match
case Left(err) => Left(err)
case Right(ctx) =>
engine.loadPosition(ctx)
state = ServiceState()
GameEngineHolder.white = toPlayerInfo(req.white, "white", "White")
GameEngineHolder.black = toPlayerInfo(req.black, "black", "Black")
Right(getSnapshot)
def importPgn(pgn: String): Either[String, GameSnapshot] = synchronized:
engine.loadGame(PgnParser, pgn) match
case Left(err) => Left(err)
case Right(_) =>
state = ServiceState()
Right(getSnapshot)
def exportFen(): String = FenExporter.exportGameContext(engine.context)
def exportPgn(): String = PgnExporter.exportGameContext(engine.context)
private def hasEnded: Boolean =
state.externalResult.isDefined || engine.context.result.isDefined
private def toPlayerInfo(dto: Option[PlayerInfoDto], defaultId: String, defaultName: String): PlayerInfo =
dto.fold(PlayerInfo(PlayerId(defaultId), defaultName))(d => PlayerInfo(PlayerId(d.id), d.displayName))
private def parseUci(uci: String): Option[(Square, Square, Option[PromotionPiece])] =
if uci.length < 4 || uci.length > 5 then None
else
for
from <- Square.fromAlgebraic(uci.substring(0, 2))
to <- Square.fromAlgebraic(uci.substring(2, 4))
yield
val promotion = if uci.length == 5 then parsePromotionChar(uci.charAt(4)) else None
(from, to, promotion)
private def parsePromotionChar(c: Char): Option[PromotionPiece] =
c match
case 'q' => Some(PromotionPiece.Queen)
case 'r' => Some(PromotionPiece.Rook)
case 'b' => Some(PromotionPiece.Bishop)
case 'n' => Some(PromotionPiece.Knight)
case _ => None
private def findMatchingMove(
candidates: List[Move],
to: Square,
promotion: Option[PromotionPiece],
): Option[Move] =
candidates.filter(_.to == to) match
case Nil => None
case moves =>
promotion match
case Some(pp) => moves.find(_.moveType == MoveType.Promotion(pp))
case None =>
moves
.find(m =>
m.moveType match
case _: MoveType.Promotion => false
case _ => true,
)
.orElse(moves.headOption)
@@ -0,0 +1,16 @@
package de.nowchess.backcore.game
import de.nowchess.api.board.Color
import de.nowchess.api.game.GameContext
import de.nowchess.api.player.PlayerInfo
case class GameSnapshot(
gameId: String,
white: PlayerInfo,
black: PlayerInfo,
context: GameContext,
drawOfferedBy: Option[Color] = None,
externalResult: Option[GameResult] = None,
canUndo: Boolean = false,
canRedo: Boolean = false,
)
@@ -0,0 +1,90 @@
package de.nowchess.backcore.resource
import de.nowchess.backcore.dto.*
import de.nowchess.backcore.game.{GameMapper, GameService}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
@Path("/api/board/game")
@Produces(Array(MediaType.APPLICATION_JSON))
@ApplicationScoped
class GameResource @Inject() (service: GameService):
@POST
@Consumes(Array(MediaType.APPLICATION_JSON))
def createGame(req: CreateGameRequest): Response =
val snapshot = service.reset(Option(req).getOrElse(CreateGameRequest()))
Response.status(201).entity(GameMapper.toGameFull(snapshot)).build()
@GET
@Path("/{gameId}")
def getGame(@PathParam("gameId") gameId: String): Response =
if !service.isKnownId(gameId) then
Response
.status(404)
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
.build()
else Response.ok(GameMapper.toGameFull(service.getSnapshot)).build()
@GET
@Path("/{gameId}/stream")
@Produces(Array("application/x-ndjson"))
def streamGame(@PathParam("gameId") gameId: String): Response =
if !service.isKnownId(gameId) then
Response
.status(404)
.`type`(MediaType.APPLICATION_JSON)
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
.build()
else
val event = s"""{"type":"gameFull","game":${GameMapper.toGameFullJson(service.getSnapshot)}}"""
Response.ok(event + "\n").build()
@POST
@Path("/{gameId}/resign")
def resignGame(@PathParam("gameId") gameId: String): Response =
if !service.isKnownId(gameId) then
Response.status(404).entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found")).build()
else
service.resign() match
case Right(_) => Response.ok(OkResponse()).build()
case Left(err) => Response.status(400).entity(ApiErrorResponse("RESIGN_ERROR", err)).build()
@POST
@Path("/{gameId}/draw/{action}")
def drawAction(
@PathParam("gameId") gameId: String,
@PathParam("action") action: String,
): Response =
if !service.isKnownId(gameId) then
Response.status(404).entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found")).build()
else
service.drawAction(action) match
case Right(_) => Response.ok(OkResponse()).build()
case Left(err) => Response.status(400).entity(ApiErrorResponse("DRAW_ERROR", err)).build()
@GET
@Path("/{gameId}/export/fen")
@Produces(Array(MediaType.TEXT_PLAIN))
def exportFen(@PathParam("gameId") gameId: String): Response =
if !service.isKnownId(gameId) then
Response
.status(404)
.`type`(MediaType.APPLICATION_JSON)
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
.build()
else Response.ok(service.exportFen()).build()
@GET
@Path("/{gameId}/export/pgn")
@Produces(Array("application/x-chess-pgn"))
def exportPgn(@PathParam("gameId") gameId: String): Response =
if !service.isKnownId(gameId) then
Response
.status(404)
.`type`(MediaType.APPLICATION_JSON)
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
.build()
else Response.ok(service.exportPgn()).build()
@@ -0,0 +1,31 @@
package de.nowchess.backcore.resource
import de.nowchess.backcore.dto.{ApiErrorResponse, ImportFenRequest, ImportPgnRequest}
import de.nowchess.backcore.game.{GameMapper, GameService}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
@Path("/api/board/game/import")
@Produces(Array(MediaType.APPLICATION_JSON))
@Consumes(Array(MediaType.APPLICATION_JSON))
@ApplicationScoped
class ImportResource @Inject() (service: GameService):
@POST
@Path("/fen")
def importFen(req: ImportFenRequest): Response =
service.importFen(Option(req).getOrElse(ImportFenRequest())) match
case Right(snap) => Response.status(201).entity(GameMapper.toGameFull(snap)).build()
case Left(err) => Response.status(400).entity(ApiErrorResponse("INVALID_FEN", err)).build()
@POST
@Path("/pgn")
def importPgn(req: ImportPgnRequest): Response =
Option(req) match
case None => Response.status(400).entity(ApiErrorResponse("INVALID_PGN", "Request body is required")).build()
case Some(body) =>
service.importPgn(body.pgn) match
case Right(snap) => Response.status(201).entity(GameMapper.toGameFull(snap)).build()
case Left(err) => Response.status(400).entity(ApiErrorResponse("INVALID_PGN", err)).build()
@@ -0,0 +1,85 @@
package de.nowchess.backcore.resource
import de.nowchess.api.board.Square
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.backcore.dto.*
import de.nowchess.backcore.game.{GameMapper, GameService}
import jakarta.enterprise.context.ApplicationScoped
import jakarta.inject.Inject
import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
@Path("/api/board/game")
@Produces(Array(MediaType.APPLICATION_JSON))
@ApplicationScoped
class MoveResource @Inject() (service: GameService):
@POST
@Path("/{gameId}/move/{uci}")
def makeMove(
@PathParam("gameId") gameId: String,
@PathParam("uci") uci: String,
): Response =
if !service.isKnownId(gameId) then
Response.status(404).entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found")).build()
else
service.applyMove(uci) match
case Right(snap) => Response.ok(GameMapper.toGameState(snap)).build()
case Left(err) => Response.status(400).entity(ApiErrorResponse("INVALID_MOVE", err)).build()
@GET
@Path("/{gameId}/moves")
def getLegalMoves(
@PathParam("gameId") gameId: String,
@QueryParam("square") squareParam: String,
): Response =
if !service.isKnownId(gameId) then
Response.status(404).entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found")).build()
else
val square = Option(squareParam).flatMap(Square.fromAlgebraic)
val moves = service.legalMoves(square)
val dtos = moves.map(toLegalMoveDto)
Response.ok(LegalMovesResponse(dtos)).build()
@POST
@Path("/{gameId}/undo")
def undoMove(@PathParam("gameId") gameId: String): Response =
if !service.isKnownId(gameId) then
Response.status(404).entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found")).build()
else
service.undo() match
case Right(snap) => Response.ok(GameMapper.toGameState(snap)).build()
case Left(err) => Response.status(400).entity(ApiErrorResponse("UNDO_NOT_AVAILABLE", err)).build()
@POST
@Path("/{gameId}/redo")
def redoMove(@PathParam("gameId") gameId: String): Response =
if !service.isKnownId(gameId) then
Response.status(404).entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found")).build()
else
service.redo() match
case Right(snap) => Response.ok(GameMapper.toGameState(snap)).build()
case Left(err) => Response.status(400).entity(ApiErrorResponse("REDO_NOT_AVAILABLE", err)).build()
private def toLegalMoveDto(move: Move): LegalMoveDto =
val uci = GameMapper.moveToUci(move)
val (moveType, promotion) = move.moveType match
case MoveType.Normal(true) => ("capture", None)
case MoveType.Normal(false) => ("normal", None)
case MoveType.CastleKingside => ("castleKingside", None)
case MoveType.CastleQueenside => ("castleQueenside", None)
case MoveType.EnPassant => ("enPassant", None)
case MoveType.Promotion(pp) =>
val pName = pp match
case PromotionPiece.Queen => "queen"
case PromotionPiece.Rook => "rook"
case PromotionPiece.Bishop => "bishop"
case PromotionPiece.Knight => "knight"
("promotion", Some(pName))
LegalMoveDto(
from = move.from.toString,
to = move.to.toString,
uci = uci,
moveType = moveType,
promotion = promotion,
)
@@ -0,0 +1,14 @@
import io.quarkus.test.junit.QuarkusIntegrationTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Test
@QuarkusIntegrationTest
case class Test2(@Inject tester: Tester) {
@Test
def testAbc(): Unit = {
println("Hello World")
tester.test()
}
}
@@ -0,0 +1,11 @@
package de.nowchess.backcore
import io.quarkus.test.junit.QuarkusTest
import org.junit.jupiter.api.Test
@QuarkusTest
class BackcoreStartupTest:
@Test
def applicationStarts(): Unit =
// If we get here the Quarkus container started successfully
()
@@ -0,0 +1,226 @@
package de.nowchess.backcore.dto
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
/** Exercises the Scala-generated synthetic methods (equals, hashCode, copy, productElement,
* productElementName, toString, canEqual) on every DTO case class so that JaCoCo counts them as
* covered.
*/
class DtoCoverageTest:
@Test
def playerInfoDtoSynthetics(): Unit =
val a = PlayerInfoDto("id1", "Alice")
val b = PlayerInfoDto("id1", "Alice")
val c = PlayerInfoDto("id2", "Bob")
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("Alice"))
assertTrue(a.canEqual(b))
assertEquals("id1", a.productElement(0))
assertEquals("Alice", a.productElement(1))
assertEquals("id", a.productElementName(0))
assertEquals("displayName", a.productElementName(1))
assertEquals(a, a.copy())
assertEquals(PlayerInfoDto("x", "Alice"), a.copy(id = "x"))
assertEquals(PlayerInfoDto("id1", "X"), a.copy(displayName = "X"))
@Test
def okResponseSynthetics(): Unit =
val a = OkResponse()
val b = OkResponse()
val c = OkResponse(ok = false)
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("true"))
assertTrue(a.canEqual(b))
assertEquals(true, a.productElement(0))
assertEquals("ok", a.productElementName(0))
assertEquals(a, a.copy())
assertEquals(OkResponse(false), a.copy(ok = false))
@Test
def apiErrorResponseSynthetics(): Unit =
val a = ApiErrorResponse("CODE", "msg")
val b = ApiErrorResponse("CODE", "msg")
val c = ApiErrorResponse("OTHER", "msg")
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("CODE"))
assertTrue(a.canEqual(b))
assertEquals("CODE", a.productElement(0))
assertEquals("msg", a.productElement(1))
assertEquals(None, a.productElement(2))
assertEquals("code", a.productElementName(0))
assertEquals("message", a.productElementName(1))
assertEquals("field", a.productElementName(2))
assertEquals(a, a.copy())
assertEquals(ApiErrorResponse("X", "msg"), a.copy(code = "X"))
assertEquals(ApiErrorResponse("CODE", "X"), a.copy(message = "X"))
@Test
def createGameRequestSynthetics(): Unit =
val a = CreateGameRequest()
val b = CreateGameRequest()
val c = CreateGameRequest(white = Some(PlayerInfoDto("x", "X")))
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertNotNull(a.toString)
assertTrue(a.canEqual(b))
assertEquals(None, a.productElement(0))
assertEquals(None, a.productElement(1))
assertEquals("white", a.productElementName(0))
assertEquals("black", a.productElementName(1))
assertEquals(a, a.copy())
assertEquals(CreateGameRequest(black = None), a.copy(black = None))
@Test
def importFenRequestSynthetics(): Unit =
val a = ImportFenRequest(fen = "fen1")
val b = ImportFenRequest(fen = "fen1")
val c = ImportFenRequest(fen = "fen2")
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("fen1"))
assertTrue(a.canEqual(b))
assertEquals("fen1", a.productElement(0))
assertEquals(None, a.productElement(1))
assertEquals(None, a.productElement(2))
assertEquals("fen", a.productElementName(0))
assertEquals("white", a.productElementName(1))
assertEquals("black", a.productElementName(2))
assertEquals(a, a.copy())
assertEquals(ImportFenRequest(fen = "x"), a.copy(fen = "x"))
assertEquals(ImportFenRequest(fen = "fen1", white = None), a.copy(white = None))
@Test
def importPgnRequestSynthetics(): Unit =
val a = ImportPgnRequest(pgn = "1. e4 *")
val b = ImportPgnRequest(pgn = "1. e4 *")
val c = ImportPgnRequest(pgn = "other")
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("e4"))
assertTrue(a.canEqual(b))
assertEquals("1. e4 *", a.productElement(0))
assertEquals("pgn", a.productElementName(0))
assertEquals(a, a.copy())
assertEquals(ImportPgnRequest(pgn = "x"), a.copy(pgn = "x"))
@Test
def legalMoveDtoSynthetics(): Unit =
val a = LegalMoveDto("e2", "e4", "e2e4", "normal")
val b = LegalMoveDto("e2", "e4", "e2e4", "normal")
val c = LegalMoveDto("d2", "d4", "d2d4", "normal")
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("e2"))
assertTrue(a.canEqual(b))
assertEquals("e2", a.productElement(0))
assertEquals("e4", a.productElement(1))
assertEquals("e2e4", a.productElement(2))
assertEquals("normal", a.productElement(3))
assertEquals(None, a.productElement(4))
assertEquals("from", a.productElementName(0))
assertEquals("to", a.productElementName(1))
assertEquals("uci", a.productElementName(2))
assertEquals("moveType", a.productElementName(3))
assertEquals("promotion", a.productElementName(4))
assertEquals(a, a.copy())
assertEquals(LegalMoveDto("x", "e4", "xe4", "normal"), a.copy(from = "x", uci = "xe4"))
assertEquals(LegalMoveDto("e2", "x", "e2x", "normal"), a.copy(to = "x", uci = "e2x"))
@Test
def legalMovesResponseSynthetics(): Unit =
val a = LegalMovesResponse(List.empty)
val b = LegalMovesResponse(List.empty)
val c = LegalMovesResponse(List(LegalMoveDto("a1", "a2", "a1a2", "normal")))
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertNotNull(a.toString)
assertTrue(a.canEqual(b))
assertEquals(List.empty, a.productElement(0))
assertEquals("moves", a.productElementName(0))
assertEquals(a, a.copy())
assertEquals(LegalMovesResponse(List.empty), a.copy(moves = List.empty))
@Test
def gameStateResponseSynthetics(): Unit =
val a = GameStateResponse("fen", "pgn", "white", "started", None, List.empty, false, false)
val b = GameStateResponse("fen", "pgn", "white", "started", None, List.empty, false, false)
val c = GameStateResponse("fen2", "pgn", "white", "started", None, List.empty, false, false)
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("fen"))
assertTrue(a.canEqual(b))
assertEquals("fen", a.productElement(0))
assertEquals("pgn", a.productElement(1))
assertEquals("white", a.productElement(2))
assertEquals("started", a.productElement(3))
assertEquals(None, a.productElement(4))
assertEquals(List.empty, a.productElement(5))
assertEquals(false, a.productElement(6))
assertEquals(false, a.productElement(7))
assertEquals("fen", a.productElementName(0))
assertEquals("pgn", a.productElementName(1))
assertEquals("turn", a.productElementName(2))
assertEquals("status", a.productElementName(3))
assertEquals("winner", a.productElementName(4))
assertEquals("moves", a.productElementName(5))
assertEquals("undoAvailable", a.productElementName(6))
assertEquals("redoAvailable", a.productElementName(7))
assertEquals(a, a.copy())
assertEquals(GameStateResponse("x", "pgn", "white", "started", None, List.empty, false, false), a.copy(fen = "x"))
@Test
def gameFullResponseSynthetics(): Unit =
val p = PlayerInfoDto("id", "Name")
val state = GameStateResponse("fen", "pgn", "white", "started", None, List.empty, false, false)
val a = GameFullResponse("gid", p, p, state)
val b = GameFullResponse("gid", p, p, state)
val c = GameFullResponse("other", p, p, state)
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("gid"))
assertTrue(a.canEqual(b))
assertEquals("gid", a.productElement(0))
assertEquals(p, a.productElement(1))
assertEquals(p, a.productElement(2))
assertEquals(state, a.productElement(3))
assertEquals("gameId", a.productElementName(0))
assertEquals("white", a.productElementName(1))
assertEquals("black", a.productElementName(2))
assertEquals("state", a.productElementName(3))
assertEquals(a, a.copy())
assertEquals(GameFullResponse("x", p, p, state), a.copy(gameId = "x"))
@@ -0,0 +1,91 @@
package de.nowchess.backcore.game
import de.nowchess.api.board.Color
import de.nowchess.api.game.GameContext
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.io.{ByteArrayOutputStream, ObjectOutputStream}
/** Exercises Scala-generated synthetic methods on domain case classes and serializes singleton
* objects (Scala objects implement Serializable; writeReplace is called on serialization).
*/
class GameDomainCoverageTest:
private val white = PlayerInfo(PlayerId("white"), "White")
private val black = PlayerInfo(PlayerId("black"), "Black")
private def freshSnap(canUndo: Boolean = false, canRedo: Boolean = false): GameSnapshot =
GameSnapshot(
gameId = "g1",
white = white,
black = black,
context = GameContext.initial,
canUndo = canUndo,
canRedo = canRedo,
)
@Test
def gameResultResignSynthetics(): Unit =
val a = GameResult.Resign(Color.White)
val b = GameResult.Resign(Color.White)
val c = GameResult.Resign(Color.Black)
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.toString.contains("White"))
assertTrue(a.canEqual(b))
assertEquals(Color.White, a.productElement(0))
assertEquals("winner", a.productElementName(0))
assertEquals(a, a.copy())
assertEquals(GameResult.Resign(Color.Black), a.copy(winner = Color.Black))
@Test
def gameSnapshotSynthetics(): Unit =
val a = freshSnap()
val b = freshSnap()
val c = freshSnap(canUndo = true)
assertEquals(a, b)
assertNotEquals(a, c)
assertFalse(a.equals(null))
assertFalse(a.equals("other"))
assertEquals(a.hashCode, b.hashCode)
assertTrue(a.canEqual(b))
assertEquals("g1", a.productElement(0))
assertEquals(white, a.productElement(1))
assertEquals(black, a.productElement(2))
assertEquals(GameContext.initial, a.productElement(3))
assertEquals(None, a.productElement(4))
assertEquals(None, a.productElement(5))
assertEquals(false, a.productElement(6))
assertEquals(false, a.productElement(7))
assertEquals("gameId", a.productElementName(0))
assertEquals("white", a.productElementName(1))
assertEquals("black", a.productElementName(2))
assertEquals("context", a.productElementName(3))
assertEquals("drawOfferedBy", a.productElementName(4))
assertEquals("externalResult", a.productElementName(5))
assertEquals("canUndo", a.productElementName(6))
assertEquals("canRedo", a.productElementName(7))
assertEquals(a, a.copy())
assertEquals(freshSnap(canUndo = true), a.copy(canUndo = true))
assertEquals(freshSnap(canRedo = true), a.copy(canRedo = true))
@Test
def gameMapperSingletonIsSerializable(): Unit =
val bos = new ByteArrayOutputStream()
val oos = new ObjectOutputStream(bos)
oos.writeObject(GameMapper)
oos.close()
assertTrue(bos.size() > 0)
@Test
def gameEngineHolderSingletonIsSerializable(): Unit =
val bos = new ByteArrayOutputStream()
val oos = new ObjectOutputStream(bos)
oos.writeObject(GameEngineHolder)
oos.close()
assertTrue(bos.size() > 0)
@@ -0,0 +1,190 @@
package de.nowchess.backcore.game
import de.nowchess.api.board.{Color, File, Rank, Square}
import de.nowchess.api.game.{DrawReason, GameContext, GameResult as ApiGameResult}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.player.{PlayerId, PlayerInfo}
import de.nowchess.io.fen.FenParser
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
class GameMapperTest:
private val white = PlayerInfo(PlayerId("white"), "White")
private val black = PlayerInfo(PlayerId("black"), "Black")
private def snap(
ctx: GameContext = GameContext.initial,
externalResult: Option[GameResult] = None,
drawOfferedBy: Option[Color] = None,
canUndo: Boolean = false,
canRedo: Boolean = false,
): GameSnapshot =
GameSnapshot(
gameId = "testId1",
white = white,
black = black,
context = ctx,
drawOfferedBy = drawOfferedBy,
externalResult = externalResult,
canUndo = canUndo,
canRedo = canRedo,
)
@Test
def resignWhiteReturnsResignWithWhiteWinner(): Unit =
val state = GameMapper.toGameState(snap(externalResult = Some(GameResult.Resign(Color.White))))
assertEquals("resign", state.status)
assertEquals(Some("white"), state.winner)
@Test
def resignBlackReturnsResignWithBlackWinner(): Unit =
val state = GameMapper.toGameState(snap(externalResult = Some(GameResult.Resign(Color.Black))))
assertEquals("resign", state.status)
assertEquals(Some("black"), state.winner)
@Test
def agreedDrawReturnsDrawNoWinner(): Unit =
val state = GameMapper.toGameState(snap(externalResult = Some(GameResult.AgreedDraw)))
assertEquals("draw", state.status)
assertEquals(None, state.winner)
@Test
def fiftyMoveDrawReturnsDrawNoWinner(): Unit =
val state = GameMapper.toGameState(snap(externalResult = Some(GameResult.FiftyMoveDraw)))
assertEquals("draw", state.status)
assertEquals(None, state.winner)
@Test
def contextWinWhiteReturnsCheckmate(): Unit =
val ctx = GameContext.initial.withResult(Some(ApiGameResult.Win(Color.White)))
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("checkmate", state.status)
assertEquals(Some("white"), state.winner)
@Test
def contextWinBlackReturnsCheckmate(): Unit =
val ctx = GameContext.initial.withResult(Some(ApiGameResult.Win(Color.Black)))
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("checkmate", state.status)
assertEquals(Some("black"), state.winner)
@Test
def contextDrawStalemateReturnsStalemate(): Unit =
val ctx = GameContext.initial.withResult(Some(ApiGameResult.Draw(DrawReason.Stalemate)))
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("stalemate", state.status)
assertEquals(None, state.winner)
@Test
def contextDrawInsufficientMaterialReturnsInsufficientMaterial(): Unit =
val ctx = GameContext.initial.withResult(Some(ApiGameResult.Draw(DrawReason.InsufficientMaterial)))
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("insufficientMaterial", state.status)
assertEquals(None, state.winner)
@Test
def contextDrawFiftyMoveRuleReturnsDraw(): Unit =
val ctx = GameContext.initial.withResult(Some(ApiGameResult.Draw(DrawReason.FiftyMoveRule)))
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("draw", state.status)
assertEquals(None, state.winner)
@Test
def contextDrawAgreementReturnsDraw(): Unit =
val ctx = GameContext.initial.withResult(Some(ApiGameResult.Draw(DrawReason.Agreement)))
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("draw", state.status)
assertEquals(None, state.winner)
@Test
def liveCheckPositionReturnsCheckStatus(): Unit =
// White king on a1, black rook on h1 — white is in check
val ctx = FenParser.parseFen("k7/8/8/8/8/8/8/K6r w - - 0 1")
.getOrElse(fail("Invalid FEN"))
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("check", state.status)
@Test
def liveDrawOfferedBySetReturnsDrawOffered(): Unit =
val state = GameMapper.toGameState(snap(drawOfferedBy = Some(Color.White)))
assertEquals("drawOffered", state.status)
@Test
def liveFiftyMoveClockGe100ReturnsFiftyMoveAvailable(): Unit =
val ctx = GameContext.initial.withHalfMoveClock(100)
val state = GameMapper.toGameState(snap(ctx = ctx))
assertEquals("fiftyMoveAvailable", state.status)
@Test
def liveNormalPositionReturnsStarted(): Unit =
val state = GameMapper.toGameState(snap())
assertEquals("started", state.status)
assertEquals(None, state.winner)
@Test
def canUndoReflectedInToGameState(): Unit =
val state = GameMapper.toGameState(snap(canUndo = true, canRedo = false))
assertTrue(state.undoAvailable)
assertFalse(state.redoAvailable)
@Test
def canRedoReflectedInToGameState(): Unit =
val state = GameMapper.toGameState(snap(canUndo = false, canRedo = true))
assertFalse(state.undoAvailable)
assertTrue(state.redoAvailable)
@Test
def moveToUciNormalQuiet(): Unit =
val sq = Square(File.E, Rank.R2)
val sq2 = Square(File.E, Rank.R4)
val move = Move(sq, sq2, MoveType.Normal(false))
assertEquals("e2e4", GameMapper.moveToUci(move))
@Test
def moveToUciNormalCapture(): Unit =
val move = Move(Square(File.D, Rank.R4), Square(File.E, Rank.R5), MoveType.Normal(true))
assertEquals("d4e5", GameMapper.moveToUci(move))
@Test
def moveToUciCastleKingside(): Unit =
val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
assertEquals("e1g1", GameMapper.moveToUci(move))
@Test
def moveToUciCastleQueenside(): Unit =
val move = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside)
assertEquals("e1c1", GameMapper.moveToUci(move))
@Test
def moveToUciEnPassant(): Unit =
val move = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant)
assertEquals("e5d6", GameMapper.moveToUci(move))
@Test
def moveToUciPromotionQueen(): Unit =
val move = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Queen))
assertEquals("e7e8q", GameMapper.moveToUci(move))
@Test
def moveToUciPromotionRook(): Unit =
val move = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Rook))
assertEquals("e7e8r", GameMapper.moveToUci(move))
@Test
def moveToUciPromotionBishop(): Unit =
val move = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Bishop))
assertEquals("e7e8b", GameMapper.moveToUci(move))
@Test
def moveToUciPromotionKnight(): Unit =
val move = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R8), MoveType.Promotion(PromotionPiece.Knight))
assertEquals("e7e8n", GameMapper.moveToUci(move))
@Test
def toGameFullPopulatesAllFields(): Unit =
val full = GameMapper.toGameFull(snap())
assertEquals("testId1", full.gameId)
assertEquals("white", full.white.id)
assertEquals("black", full.black.id)
assertNotNull(full.state)
@@ -0,0 +1,313 @@
package de.nowchess.backcore.game
import de.nowchess.api.board.{Color, File, Rank, Square}
import de.nowchess.backcore.dto.{CreateGameRequest, ImportFenRequest, PlayerInfoDto}
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.{BeforeEach, Test}
class GameServiceTest:
private val promotionFen = "8/4P3/8/8/8/8/8/4K2k w - - 0 1"
private def freshService(): GameService =
val svc = new GameService()
svc.reset(CreateGameRequest())
svc
@Test
def resetClearsStateAndReturnsSnapshot(): Unit =
val svc = freshService()
svc.resign()
val snap = svc.reset(CreateGameRequest())
assertEquals(GameEngineHolder.gameId, snap.gameId)
assertEquals(None, snap.externalResult)
assertEquals(None, snap.drawOfferedBy)
@Test
def resetWithPlayersUpdatesHolderInfo(): Unit =
val svc = freshService()
val req = CreateGameRequest(
white = Some(PlayerInfoDto("p1", "Alice")),
black = Some(PlayerInfoDto("p2", "Bob")),
)
val snap = svc.reset(req)
assertEquals("Alice", snap.white.displayName)
assertEquals("Bob", snap.black.displayName)
@Test
def applyMoveInvalidUciReturnsLeft(): Unit =
val svc = freshService()
val result = svc.applyMove("zzzz")
assertTrue(result.isLeft)
@Test
def applyMoveIllegalMoveReturnsLeft(): Unit =
val svc = freshService()
val result = svc.applyMove("e2e5")
assertTrue(result.isLeft)
@Test
def applyMoveValidMoveReturnsRightWithUpdatedContext(): Unit =
val svc = freshService()
val result = svc.applyMove("e2e4")
assertTrue(result.isRight)
val snap = result.getOrElse(fail("Expected Right"))
assertEquals(Color.Black, snap.context.turn)
@Test
def applyMoveWhenGameOverReturnsLeft(): Unit =
val svc = freshService()
svc.resign()
val result = svc.applyMove("e2e4")
assertTrue(result.isLeft)
assertTrue(result.left.getOrElse("").contains("Game is already over"))
@Test
def legalMovesNoneReturnsNonEmptyListAtStart(): Unit =
val svc = freshService()
val moves = svc.legalMoves(None)
assertFalse(moves.isEmpty)
@Test
def legalMovesForE2ReturnsE2e3AndE2e4(): Unit =
val svc = freshService()
val e2 = Square(File.E, Rank.R2)
val moves = svc.legalMoves(Some(e2))
val ucis = moves.map(m => s"${m.from}${m.to}")
assertTrue(ucis.contains("e2e3"), s"Expected e2e3 in $ucis")
assertTrue(ucis.contains("e2e4"), s"Expected e2e4 in $ucis")
@Test
def undoWithNoHistoryReturnsLeft(): Unit =
val svc = freshService()
val result = svc.undo()
assertTrue(result.isLeft)
@Test
def undoAfterMoveReverts(): Unit =
val svc = freshService()
svc.applyMove("e2e4")
val result = svc.undo()
assertTrue(result.isRight)
val snap = result.getOrElse(fail("Expected Right"))
assertEquals(Color.White, snap.context.turn)
@Test
def redoWithNoRedoStackReturnsLeft(): Unit =
val svc = freshService()
val result = svc.redo()
assertTrue(result.isLeft)
@Test
def redoAfterUndoReturnsRight(): Unit =
val svc = freshService()
svc.applyMove("e2e4")
svc.undo()
val result = svc.redo()
assertTrue(result.isRight)
val snap = result.getOrElse(fail("Expected Right"))
assertEquals(Color.Black, snap.context.turn)
@Test
def resignNormalReturnsRight(): Unit =
val svc = freshService()
val result = svc.resign()
assertTrue(result.isRight)
val snap = result.getOrElse(fail("Expected Right"))
assertTrue(snap.externalResult.isDefined)
snap.externalResult match
case Some(GameResult.Resign(_)) => ()
case other => fail(s"Expected Resign but got $other")
@Test
def resignWhenAlreadyResignedReturnsLeft(): Unit =
val svc = freshService()
svc.resign()
val result = svc.resign()
assertTrue(result.isLeft)
@Test
def drawActionOfferSetsDrawOfferedBy(): Unit =
val svc = freshService()
val result = svc.drawAction("offer")
assertTrue(result.isRight)
val snap = result.getOrElse(fail("Expected Right"))
assertTrue(snap.drawOfferedBy.isDefined)
@Test
def drawActionAcceptWithNoOfferReturnsLeft(): Unit =
val svc = freshService()
val result = svc.drawAction("accept")
assertTrue(result.isLeft)
@Test
def drawActionAcceptOwnOfferReturnsLeft(): Unit =
val svc = freshService()
svc.drawAction("offer")
val result = svc.drawAction("accept")
assertTrue(result.isLeft)
assertTrue(result.left.getOrElse("").contains("Cannot accept your own draw offer"))
@Test
def drawActionAcceptOpponentAcceptsReturnsAgreedDraw(): Unit =
val svc = freshService()
svc.drawAction("offer")
svc.applyMove("e2e4")
val result = svc.drawAction("accept")
assertTrue(result.isRight)
val snap = result.getOrElse(fail("Expected Right"))
assertEquals(Some(GameResult.AgreedDraw), snap.externalResult)
@Test
def drawActionDeclineWithNoOfferReturnsLeft(): Unit =
val svc = freshService()
val result = svc.drawAction("decline")
assertTrue(result.isLeft)
@Test
def drawActionDeclineAfterOfferClearsOffer(): Unit =
val svc = freshService()
svc.drawAction("offer")
val result = svc.drawAction("decline")
assertTrue(result.isRight)
val snap = result.getOrElse(fail("Expected Right"))
assertEquals(None, snap.drawOfferedBy)
@Test
def drawActionClaimWhenFiftyMoveNotTriggeredReturnsLeft(): Unit =
val svc = freshService()
val result = svc.drawAction("claim")
assertTrue(result.isLeft)
@Test
def drawActionUnknownReturnsLeft(): Unit =
val svc = freshService()
val result = svc.drawAction("unknown")
assertTrue(result.isLeft)
@Test
def importFenValidReturnsRight(): Unit =
val svc = freshService()
val result = svc.importFen(ImportFenRequest(fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"))
assertTrue(result.isRight)
@Test
def importFenInvalidReturnsLeft(): Unit =
val svc = freshService()
val result = svc.importFen(ImportFenRequest(fen = "not-a-fen"))
assertTrue(result.isLeft)
@Test
def importPgnValidReturnsRight(): Unit =
val svc = freshService()
val result = svc.importPgn("1. e4 e5 2. Nf3 Nc6 *")
assertTrue(result.isRight)
@Test
def importPgnInvalidReturnsLeft(): Unit =
val svc = freshService()
val result = svc.importPgn("1. z9 *")
assertTrue(result.isLeft)
@Test
def exportFenReturnsNonEmptyString(): Unit =
val svc = freshService()
val fen = svc.exportFen()
assertFalse(fen.isEmpty)
@Test
def exportPgnReturnsString(): Unit =
val svc = freshService()
val pgn = svc.exportPgn()
assertNotNull(pgn)
@Test
def isKnownIdTrueForHolderGameId(): Unit =
val svc = freshService()
assertTrue(svc.isKnownId(GameEngineHolder.gameId))
@Test
def isKnownIdFalseForOtherId(): Unit =
val svc = freshService()
assertFalse(svc.isKnownId("XXXXXXXX"))
@Test
def applyMovePromotionQueenProducesQueen(): Unit =
val svc = freshService()
svc.importFen(ImportFenRequest(fen = promotionFen))
val result = svc.applyMove("e7e8q")
assertTrue(result.isRight, s"Expected Right but got $result")
val snap = result.getOrElse(fail("Expected Right"))
val e8 = Square(File.E, Rank.R8)
snap.context.board.pieceAt(e8) match
case Some(piece) =>
assertEquals(de.nowchess.api.board.PieceType.Queen, piece.pieceType)
case None => fail("Expected queen on e8")
@Test
def applyMovePromotionRookProducesRook(): Unit =
val svc = freshService()
svc.importFen(ImportFenRequest(fen = promotionFen))
val result = svc.applyMove("e7e8r")
assertTrue(result.isRight, s"Expected Right but got $result")
val snap = result.getOrElse(fail("Expected Right"))
val e8 = Square(File.E, Rank.R8)
snap.context.board.pieceAt(e8) match
case Some(piece) => assertEquals(de.nowchess.api.board.PieceType.Rook, piece.pieceType)
case None => fail("Expected rook on e8")
@Test
def applyMovePromotionBishopProducesBishop(): Unit =
val svc = freshService()
svc.importFen(ImportFenRequest(fen = promotionFen))
val result = svc.applyMove("e7e8b")
assertTrue(result.isRight, s"Expected Right but got $result")
val snap = result.getOrElse(fail("Expected Right"))
val e8 = Square(File.E, Rank.R8)
snap.context.board.pieceAt(e8) match
case Some(piece) => assertEquals(de.nowchess.api.board.PieceType.Bishop, piece.pieceType)
case None => fail("Expected bishop on e8")
@Test
def applyMovePromotionKnightProducesKnight(): Unit =
val svc = freshService()
svc.importFen(ImportFenRequest(fen = promotionFen))
val result = svc.applyMove("e7e8n")
assertTrue(result.isRight, s"Expected Right but got $result")
val snap = result.getOrElse(fail("Expected Right"))
val e8 = Square(File.E, Rank.R8)
snap.context.board.pieceAt(e8) match
case Some(piece) => assertEquals(de.nowchess.api.board.PieceType.Knight, piece.pieceType)
case None => fail("Expected knight on e8")
@Test
def applyMoveWithoutPromotionCharFallsBackToFirstPromotion(): Unit =
val svc = freshService()
svc.importFen(ImportFenRequest(fen = promotionFen))
val result = svc.applyMove("e7e8")
assertTrue(result.isRight, s"Expected Right but got $result")
@Test
def applyMoveWithInvalidPromotionCharFallsBackToFirstPromotion(): Unit =
// 'x' → parsePromotionChar wildcard branch → None → orElse(headOption)
val svc = freshService()
svc.importFen(ImportFenRequest(fen = promotionFen))
val result = svc.applyMove("e7e8x")
assertTrue(result.isRight, s"Expected Right but got $result")
@Test
def drawActionClaimWhenFiftyMoveTriggeredReturnsRight(): Unit =
val svc = freshService()
svc.importFen(ImportFenRequest(fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 100 51"))
val result = svc.drawAction("claim")
assertTrue(result.isRight, s"Expected Right but got $result")
val snap = result.getOrElse(fail("Expected Right"))
assertEquals(Some(GameResult.FiftyMoveDraw), snap.externalResult)
@Test
def engineSetterPropagatesNewEngine(): Unit =
val original = GameEngineHolder.engine
val fresh = new de.nowchess.chess.engine.GameEngine()
GameEngineHolder.engine = fresh
assertEquals(fresh, GameEngineHolder.engine)
GameEngineHolder.engine = original
@@ -0,0 +1,112 @@
package de.nowchess.backcore.resource
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import org.hamcrest.Matchers.{equalTo, matchesPattern, notNullValue}
import org.junit.jupiter.api.Test
@QuarkusTest
class GameResourceTest:
@Test
def createGameReturns201WithGameId(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.body("gameId", matchesPattern("[A-Za-z0-9]{8}"))
.body("state.fen", notNullValue())
.body("state.turn", equalTo("white"))
.body("state.status", equalTo("started"))
@Test
def createGameWithPlayersReturns201(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("""{"white":{"id":"p1","displayName":"Alice"},"black":{"id":"p2","displayName":"Bob"}}""")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.body("white.id", equalTo("p1"))
.body("black.displayName", equalTo("Bob"))
@Test
def getGameReturns200ForExistingGame(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId")
.`then`()
.statusCode(200)
.body("gameId", equalTo(gameId))
@Test
def getGameReturns404ForUnknownId(): Unit =
RestAssured
.`given`()
.when()
.get("/api/board/game/XXXXXXXX")
.`then`()
.statusCode(404)
@Test
def createGameWithNullBodyReturns201(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("null")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
@Test
def exportPgnOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.get("/api/board/game/XXXXXXXX/export/pgn")
.`then`()
.statusCode(404)
@Test
def resignWhenAlreadyResignedReturns400(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/resign")
.`then`()
.statusCode(200)
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/resign")
.`then`()
.statusCode(400)
@@ -0,0 +1,199 @@
package de.nowchess.backcore.resource
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import org.hamcrest.Matchers.{containsString, equalTo, matchesPattern, notNullValue}
import org.junit.jupiter.api.Test
@QuarkusTest
class ImportExportTest:
private val startFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
// ─── Import FEN ────────────────────────────────────────────────
@Test
def importFenReturns201WithCorrectPosition(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body(s"""{"fen":"$startFen"}""")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(201)
.body("gameId", matchesPattern("[A-Za-z0-9]{8}"))
.body("state.fen", equalTo(startFen))
.body("state.turn", equalTo("white"))
@Test
def importFenWithCustomPositionWorks(): Unit =
val fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
RestAssured
.`given`()
.contentType("application/json")
.body(s"""{"fen":"$fen"}""")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(201)
.body("state.fen", equalTo(fen))
.body("state.turn", equalTo("black"))
@Test
def importFenWithInvalidFenReturns400(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("""{"fen":"not-a-fen"}""")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(400)
@Test
def importFenWithNullBodyReturns400(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("null")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(400)
// ─── Import PGN ────────────────────────────────────────────────
@Test
def importPgnReturns201(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("""{"pgn":"1. e4 e5 2. Nf3 Nc6 *"}""")
.when()
.post("/api/board/game/import/pgn")
.`then`()
.statusCode(201)
.body("gameId", matchesPattern("[A-Za-z0-9]{8}"))
.body("state.turn", equalTo("white"))
@Test
def importPgnWithInvalidPgnReturns400(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("""{"pgn":"1. z9 *"}""")
.when()
.post("/api/board/game/import/pgn")
.`then`()
.statusCode(400)
@Test
def importPgnWithNullBodyReturns400(): Unit =
RestAssured
.`given`()
.contentType("application/json")
.body("null")
.when()
.post("/api/board/game/import/pgn")
.`then`()
.statusCode(400)
// ─── Export FEN ────────────────────────────────────────────────
@Test
def exportFenReturnsStartingFen(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/export/fen")
.`then`()
.statusCode(200)
.body(equalTo(startFen))
@Test
def exportFenOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.get("/api/board/game/XXXXXXXX/export/fen")
.`then`()
.statusCode(404)
// ─── Export PGN ────────────────────────────────────────────────
@Test
def exportPgnReturnsText(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/move/e2e4")
.`then`()
.statusCode(200)
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/export/pgn")
.`then`()
.statusCode(200)
.body(containsString("e4"))
// ─── Stream ────────────────────────────────────────────────────
@Test
def streamReturnsNdjsonSnapshot(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
val body = RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/stream")
.`then`()
.statusCode(200)
.contentType("application/x-ndjson")
.extract()
.body()
.asString()
assert(body.trim.startsWith("""{"type":"gameFull""""), s"Expected gameFull event, got: $body")
@Test
def streamOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.get("/api/board/game/XXXXXXXX/stream")
.`then`()
.statusCode(404)
@@ -0,0 +1,164 @@
package de.nowchess.backcore.resource
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import org.hamcrest.Matchers.{containsString, empty, equalTo, hasItem, hasItems, not, notNullValue}
import org.junit.jupiter.api.Test
@QuarkusTest
class MoveResourceTest:
private def createGame(): String =
RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
@Test
def makeMoveReturns200WithUpdatedFen(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/move/e2e4")
.`then`()
.statusCode(200)
.body("fen", containsString("4P3")) // e4 pawn present in FEN
.body("turn", equalTo("black"))
.body("moves", hasItem("e2e4"))
@Test
def makeMoveOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.post("/api/board/game/XXXXXXXX/move/e2e4")
.`then`()
.statusCode(404)
@Test
def illegalMoveReturns400(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/move/e2e5") // illegal — pawns can't jump 3 squares
.`then`()
.statusCode(400)
@Test
def getLegalMovesReturnsNonEmptyList(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/moves")
.`then`()
.statusCode(200)
.body("moves", not(empty()))
@Test
def getLegalMovesFilteredBySquareReturnsCorrectMoves(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/moves?square=e2")
.`then`()
.statusCode(200)
.body("moves.uci", hasItems("e2e3", "e2e4"))
@Test
def getLegalMovesOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.get("/api/board/game/XXXXXXXX/moves")
.`then`()
.statusCode(404)
@Test
def getLegalMovesIncludesCaptureType(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("""{"fen":"rnbqkbnr/ppp1pppp/8/3p4/4P3/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 2"}""")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/moves?square=e4")
.`then`()
.statusCode(200)
.body("moves.moveType", hasItem("capture"))
@Test
def getLegalMovesIncludesCastlingTypes(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("""{"fen":"4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1"}""")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/moves?square=e1")
.`then`()
.statusCode(200)
.body("moves.moveType", hasItems("castleKingside", "castleQueenside"))
@Test
def getLegalMovesIncludesEnPassantType(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("""{"fen":"rnbqkbnr/ppp1p1pp/8/3pPp2/8/8/PPPP1PPP/RNBQKBNR w KQkq f6 0 3"}""")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/moves?square=e5")
.`then`()
.statusCode(200)
.body("moves.moveType", hasItem("enPassant"))
@Test
def getLegalMovesIncludesAllPromotionTypes(): Unit =
val gameId = RestAssured
.`given`()
.contentType("application/json")
.body("""{"fen":"8/4P3/8/8/8/8/8/4K2k w - - 0 1"}""")
.when()
.post("/api/board/game/import/fen")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId/moves?square=e7")
.`then`()
.statusCode(200)
.body("moves.promotion", hasItems("rook", "bishop", "knight"))
@@ -0,0 +1,168 @@
package de.nowchess.backcore.resource
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import org.hamcrest.Matchers.{equalTo, notNullValue}
import org.junit.jupiter.api.Test
@QuarkusTest
class ResignDrawTest:
private def createGame(): String =
RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
// ─── Resign ────────────────────────────────────────────────────
@Test
def resignReturns200(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/resign")
.`then`()
.statusCode(200)
.body("ok", equalTo(true))
@Test
def afterResignGameShowsResignStatusAndWinner(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/resign")
.`then`()
.statusCode(200)
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId")
.`then`()
.statusCode(200)
.body("state.status", equalTo("resign"))
.body("state.winner", notNullValue())
@Test
def resignOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.post("/api/board/game/XXXXXXXX/resign")
.`then`()
.statusCode(404)
// ─── Draw ──────────────────────────────────────────────────────
@Test
def offerDrawSetsDrawOfferedStatus(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/draw/offer")
.`then`()
.statusCode(200)
.body("ok", equalTo(true))
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId")
.`then`()
.statusCode(200)
.body("state.status", equalTo("drawOffered"))
@Test
def acceptDrawAfterOfferSetsDrawStatus(): Unit =
val gameId = createGame()
// White offers
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/draw/offer")
.`then`()
.statusCode(200)
// Black moves so it's black's turn... actually the API doesn't enforce turn-based draw accept.
// White offered, so black (opponent) accepts — but since there's no auth, we just call accept.
// The GameStore checks drawOfferedBy != turn to allow accept.
// White offered on white's turn, so black needs to accept — but current turn is still white.
// We need to make a move first to switch turns.
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/move/e2e4")
.`then`()
.statusCode(200)
// Now it's black's turn and white offered the draw — black accepts
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/draw/accept")
.`then`()
.statusCode(200)
.body("ok", equalTo(true))
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId")
.`then`()
.statusCode(200)
.body("state.status", equalTo("draw"))
@Test
def declineDrawClearsOffer(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/draw/offer")
.`then`()
.statusCode(200)
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/draw/decline")
.`then`()
.statusCode(200)
.body("ok", equalTo(true))
RestAssured
.`given`()
.when()
.get(s"/api/board/game/$gameId")
.`then`()
.statusCode(200)
.body("state.status", equalTo("started"))
@Test
def acceptWithoutOfferReturns400(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/draw/accept")
.`then`()
.statusCode(400)
@Test
def drawOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.post("/api/board/game/XXXXXXXX/draw/offer")
.`then`()
.statusCode(404)
@@ -0,0 +1,106 @@
package de.nowchess.backcore.resource
import io.quarkus.test.junit.QuarkusTest
import io.restassured.RestAssured
import org.hamcrest.Matchers.{containsString, equalTo}
import org.junit.jupiter.api.Test
@QuarkusTest
class UndoRedoTest:
private val initialFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
private def createGame(): String =
RestAssured
.`given`()
.contentType("application/json")
.body("{}")
.when()
.post("/api/board/game")
.`then`()
.statusCode(201)
.extract()
.path[String]("gameId")
@Test
def undoAfterMoveRestoresOriginalPosition(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/move/e2e4")
.`then`()
.statusCode(200)
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/undo")
.`then`()
.statusCode(200)
.body("fen", equalTo(initialFen))
.body("undoAvailable", equalTo(false))
@Test
def redoAfterUndoRestoresMovedPosition(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/move/e2e4")
.`then`()
.statusCode(200)
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/undo")
.`then`()
.statusCode(200)
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/redo")
.`then`()
.statusCode(200)
.body("fen", containsString("4P3"))
.body("turn", equalTo("black"))
@Test
def undoWithNoHistoryReturns400(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/undo")
.`then`()
.statusCode(400)
@Test
def redoWithNoRedoStackReturns400(): Unit =
val gameId = createGame()
RestAssured
.`given`()
.when()
.post(s"/api/board/game/$gameId/redo")
.`then`()
.statusCode(400)
@Test
def undoMoveOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.post("/api/board/game/XXXXXXXX/undo")
.`then`()
.statusCode(404)
@Test
def redoMoveOnUnknownGameReturns404(): Unit =
RestAssured
.`given`()
.when()
.post("/api/board/game/XXXXXXXX/redo")
.`then`()
.statusCode(404)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -18,8 +18,8 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
initialShouldFailOnUndo: Boolean = false,
initialShouldFailOnExecute: Boolean = false,
) extends Command:
val shouldFailOnUndo = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnUndo)
val shouldFailOnExecute = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnExecute)
val shouldFailOnUndo = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnUndo)
val shouldFailOnExecute = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnExecute)
override def execute(): Boolean = !shouldFailOnExecute.get()
override def undo(): Boolean = !shouldFailOnUndo.get()
override def description: String = "Conditional fail"
@@ -69,7 +69,9 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
engine.context shouldBe target
engine.commandHistory shouldBe empty
events.lastOption.exists { case _: de.nowchess.chess.observer.BoardResetEvent => true; case _ => false } shouldBe true
events.lastOption.exists {
case _: de.nowchess.chess.observer.BoardResetEvent => true; case _ => false
} shouldBe true
test("redo event includes captured piece description when replaying a capture"):
val engine = new GameEngine()
@@ -70,58 +70,66 @@ class FenParserFastParseTest extends AnyFunSuite with Matchers:
FenParserFastParse.parseBoard("8pp/8/8/8/8/8/8/8") shouldBe None
test("parseFen handles all individual castling rights"):
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w K - 0 1").fold(_ => fail(), ctx =>
ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe false
ctx.castlingRights.blackKingSide shouldBe false
ctx.castlingRights.blackQueenSide shouldBe false
)
FenParserFastParse
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w K - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe false
ctx.castlingRights.blackKingSide shouldBe false
ctx.castlingRights.blackQueenSide shouldBe false,
)
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Q - 0 1").fold(_ => fail(), ctx =>
ctx.castlingRights.whiteQueenSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false
)
FenParserFastParse
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Q - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteQueenSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false,
)
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w k - 0 1").fold(_ => fail(), ctx =>
ctx.castlingRights.blackKingSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false
)
FenParserFastParse
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w k - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.blackKingSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false,
)
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w q - 0 1").fold(_ => fail(), ctx =>
ctx.castlingRights.blackQueenSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false
)
FenParserFastParse
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w q - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.blackQueenSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false,
)
test("parseFen parses all en passant squares"):
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - a3 0 1").fold(_ => fail(), ctx =>
ctx.enPassantSquare shouldBe Some(Square(File.A, Rank.R3))
)
FenParserFastParse
.parseFen("8/8/8/8/8/8/8/8 w - a3 0 1")
.fold(_ => fail(), ctx => ctx.enPassantSquare shouldBe Some(Square(File.A, Rank.R3)))
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - h6 0 1").fold(_ => fail(), ctx =>
ctx.enPassantSquare shouldBe Some(Square(File.H, Rank.R6))
)
FenParserFastParse
.parseFen("8/8/8/8/8/8/8/8 w - h6 0 1")
.fold(_ => fail(), ctx => ctx.enPassantSquare shouldBe Some(Square(File.H, Rank.R6)))
test("parseFen parses different halfMove and fullMove clocks"):
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 5 10").fold(_ => fail(), ctx =>
ctx.halfMoveClock shouldBe 5
)
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 5 10").fold(_ => fail(), ctx => ctx.halfMoveClock shouldBe 5)
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 100").fold(_ => fail(), ctx =>
ctx.halfMoveClock shouldBe 0
)
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 100").fold(_ => fail(), ctx => ctx.halfMoveClock shouldBe 0)
test("parseBoard parses boards with mixed empty and piece tokens"):
val mixed = "8/1p1p1p1p/8/1P1P1P1P/8/8/8/8"
FenParserFastParse.parseBoard(mixed) should not be empty
test("parseFen handles turn transitions"):
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 1").fold(_ => fail(), ctx =>
ctx.turn shouldBe Color.White
)
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 1").fold(_ => fail(), ctx => ctx.turn shouldBe Color.White)
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 b - - 0 1").fold(_ => fail(), ctx =>
ctx.turn shouldBe Color.Black
)
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 b - - 0 1").fold(_ => fail(), ctx => ctx.turn shouldBe Color.Black)
test("parseFen rejects invalid piece characters"):
FenParserFastParse.parseFen("8x/8/8/8/8/8/8/8 w - - 0 1").isLeft shouldBe true
@@ -133,7 +141,7 @@ class FenParserFastParseTest extends AnyFunSuite with Matchers:
test("parseBoard tests all piece types in various positions"):
// Test each piece type: pawn, rook, knight, bishop, queen, king (both colors)
val allPieces = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
val parsed = FenParserFastParse.parseBoard(allPieces)
val parsed = FenParserFastParse.parseBoard(allPieces)
parsed.map(_.pieces.size) shouldBe Some(32)
parsed.map(_.pieceAt(Square(File.A, Rank.R8))) shouldBe Some(Some(Piece.BlackRook))
parsed.map(_.pieceAt(Square(File.B, Rank.R8))) shouldBe Some(Some(Piece.BlackKnight))
@@ -150,25 +158,33 @@ class FenParserFastParseTest extends AnyFunSuite with Matchers:
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 b - - 0 1").fold(_ => fail(), _.turn shouldBe Color.Black)
test("parseFen tests all castling combinations"):
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w KQkq - 0 1").fold(_ => fail(), ctx =>
ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe true
ctx.castlingRights.blackKingSide shouldBe true
ctx.castlingRights.blackQueenSide shouldBe true
)
FenParserFastParse
.parseFen("8/8/8/8/8/8/8/8 w KQkq - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe true
ctx.castlingRights.blackKingSide shouldBe true
ctx.castlingRights.blackQueenSide shouldBe true,
)
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w Kq - 0 1").fold(_ => fail(), ctx =>
ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe false
ctx.castlingRights.blackKingSide shouldBe false
ctx.castlingRights.blackQueenSide shouldBe true
)
FenParserFastParse
.parseFen("8/8/8/8/8/8/8/8 w Kq - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe false
ctx.castlingRights.blackKingSide shouldBe false
ctx.castlingRights.blackQueenSide shouldBe true,
)
test("parseFen tests all en passant files"):
for file <- Seq("a", "b", "c", "d", "e", "f", "g", "h") do
FenParserFastParse.parseFen(s"8/8/8/8/8/8/8/8 w - ${file}3 0 1").fold(_ => fail(), ctx =>
ctx.enPassantSquare should not be empty
)
FenParserFastParse
.parseFen(s"8/8/8/8/8/8/8/8 w - ${file}3 0 1")
.fold(_ => fail(), ctx => ctx.enPassantSquare should not be empty)
test("parseBoard with mixed pieces and empty squares"):
FenParserFastParse.parseBoard("r1bqkb1r/pppppppp/2n2n2/8/8/2N2N2/PPPPPPPP/R1BQKB1R") should not be empty
@@ -29,10 +29,12 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
test("pawn can capture diagonally"):
// FEN: white pawn e4, black pawn d5
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.allLegalMoves(context)
val captures = moves.filter(m => m.from == Square(File.E, Rank.R4) && (m.moveType match { case _: MoveType.Normal => true; case _ => false }))
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.allLegalMoves(context)
val captures = moves.filter(m =>
m.from == Square(File.E, Rank.R4) && (m.moveType match { case _: MoveType.Normal => true; case _ => false }),
)
captures.exists(m => m.to == Square(File.D, Rank.R5)) shouldBe true
test("pawn cannot move backward"):
+8
View File
@@ -90,6 +90,14 @@ dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
configurations.scoverage {
resolutionStrategy.eachDependency {
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
}
}
}
tasks.test {
useJUnitPlatform {
includeEngines("scalatest")
@@ -1,20 +1,15 @@
package de.nowchess.ui
import de.nowchess.chess.engine.GameEngine
import de.nowchess.ui.terminal.TerminalUI
import de.nowchess.ui.gui.ChessGUILauncher
import de.nowchess.ui.terminal.TerminalUI
/** Application entry point - starts both GUI and Terminal UI for the chess game. Both views subscribe to the same
* GameEngine via Observer pattern.
*/
object Main:
def main(args: Array[String]): Unit =
// Create the core game engine (single source of truth)
val engine = new GameEngine()
// Launch ScalaFX GUI in separate thread
ChessGUILauncher.launch(engine)
// Create and start the terminal UI (blocks on main thread)
val tui = new TerminalUI(engine)
tui.start()
@@ -266,7 +266,8 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
case Some(piece) =>
Seq(bgRect) ++ PieceSprites.loadPieceImage(piece, squareSize * 0.8).toSeq
case None =>
Seq(bgRect)): Seq[scalafx.scene.Node]
Seq(bgRect)
): Seq[scalafx.scene.Node]
}
def showMessage(msg: String): Unit =
@@ -30,9 +30,9 @@ class ChessGUIApp extends JFXApplication:
stage.scene = new Scene {
root = boardView
// Load CSS if available
try {
try
Option(getClass.getResource("/styles.css")).foreach(url => stylesheets.add(url.toExternalForm))
} catch {
catch {
case _: Exception => // CSS is optional
}
}
+16 -1
View File
@@ -1,8 +1,23 @@
rootProject.name = "NowChessSystems"
pluginManagement {
val quarkusPluginVersion: String by settings
val quarkusPluginId: String by settings
repositories {
mavenCentral()
gradlePluginPortal()
mavenLocal()
}
plugins {
id(quarkusPluginId) version quarkusPluginVersion
}
}
include(
"modules:core",
"modules:api",
"modules:io",
"modules:rule",
"modules:ui",
)
"modules:backcore"
)