Compare commits

..

1 Commits

Author SHA1 Message Date
Janis 9cb14e26e1 feat: NCS-53 changed IO to MicroService for easier scaling
Build & Test (NowChessSystems) TeamCity build failed
2026-04-21 14:15:44 +02:00
24 changed files with 693 additions and 559 deletions
-1
View File
@@ -9,7 +9,6 @@ Scala 3.5.1 · Gradle 9
./compile # Compile all modules — always run
./test # Run all tests
./coverage # Check coverage
./lint # Run linters
```
Use consistently.
-1
View File
@@ -9,7 +9,6 @@ Scala 3.5.1 · Gradle 9
./compile # Compile all modules — always run
./test # Run all tests
./coverage # Check coverage
./lint # Run linters
```
Try to stick to these commands for consistency.
+1 -1
View File
@@ -72,7 +72,7 @@ sonar {
val versions = mapOf(
"QUARKUS_SCALA3" to "1.0.0",
"SCALA3" to "3.5.1",
"SCALA_LIBRARY" to "2.13.16",
"SCALA_LIBRARY" to "2.13.18",
"SCALATEST" to "3.2.19",
"SCALATEST_JUNIT" to "0.1.11",
"SCOVERAGE" to "2.1.1",
-3
View File
@@ -1,3 +0,0 @@
#! /usr/bin/env bash
./gradlew scalafix spotlessCheck
-10
View File
@@ -60,13 +60,3 @@
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
## (2026-04-21)
### Features
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=9
MINOR=8
PATCH=0
+1 -1
View File
@@ -49,10 +49,10 @@ dependencies {
}
implementation(project(":modules:api"))
implementation(project(":modules:io"))
implementation(project(":modules:rule"))
implementation("com.microsoft.onnxruntime:onnxruntime:${versions["ONNXRUNTIME"]!!}")
testImplementation(project(":modules:io"))
testImplementation(platform("org.junit:junit-bom:${versions["JUNIT_BOM"]!!}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
-29
View File
@@ -341,32 +341,3 @@
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
## (2026-04-21)
### Features
* add GameRules stub with PositionStatus enum ([76d4168](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/76d4168038de23e5d6083d4e8f0504fbf31d15a3))
* add MovedInCheck/Checkmate/Stalemate MoveResult variants (stub dispatch) ([8b7ec57](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/8b7ec57e5ea6ee1615a1883848a426dc07d26364))
* implement GameRules with isInCheck, legalMoves, gameStatus ([94a02ff](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/94a02ff6849436d9496c70a0f16c21666dae8e4e))
* implement legal castling ([#1](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/1)) ([00d326c](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/00d326c1ba67711fbe180f04e1100c3f01dd0254))
* NCS-10 Implement Pawn Promotion ([#12](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/12)) ([13bfc16](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/13bfc16cfe25db78ec607db523ca6d993c13430c))
* NCS-11 50-move rule ([#9](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/9)) ([412ed98](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/412ed986a95703a3b282276540153480ceed229d))
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-16 Core Separation via Patterns ([#10](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/10)) ([1361dfc](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/1361dfc89553b146864fb8ff3526cf12cf3f293a))
* NCS-17 Implement basic ScalaFX UI ([#14](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/14)) ([3ff8031](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/3ff80318b4f16c59733a46498581a5c27f048287))
* NCS-21 Write Scripts to automate certain tasks ([#15](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/15)) ([8051871](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/80518719d536a087d339fe02530825dc07f8b388))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-40 Rework Draw System ([#34](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/34)) ([0091d50](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/0091d50467e9f955f23570128b96c977c01bc51b))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
* NCS-6 Implementing FEN & PGN ([#7](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/7)) ([f28e69d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f28e69dc181416aa2f221fdc4b45c2cda5efbf07))
* NCS-9 En passant implementation ([#8](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/8)) ([919beb3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/919beb3b4bfa8caf2f90976a415fe9b19b7e9747))
* wire check/checkmate/stalemate into processMove and gameLoop ([5264a22](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5264a225418b885c5e6ea6411b96f85e38837f6c))
### Bug Fixes
* add missing kings to gameLoop capture test board ([aedd787](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/aedd787b77203c2af934751dba7b784eaf165032))
* correct test board positions and captureOutput/withInput interaction ([f0481e2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/f0481e2561b779df00925b46ee281dc36a795150))
* update main class path in build configuration and adjust VCS directory mapping ([7b1f8b1](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7b1f8b117623d327232a1a92a8a44d18582e0189))
* update move validation to check for king safety ([#13](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/13)) ([e5e20c5](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/e5e20c566e368b12ca1dc59680c34e9112bf6762))
-1
View File
@@ -74,7 +74,6 @@ dependencies {
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-junit5-mockito")
testImplementation("io.rest-assured:rest-assured")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
@@ -1,19 +1,11 @@
package de.nowchess.chess.resource
import de.nowchess.api.dto.*
import de.nowchess.api.game.GameContext
import de.nowchess.chess.client.IoServiceClient
import de.nowchess.chess.exception.BadRequestException
import de.nowchess.io.fen.FenExporter
import de.nowchess.io.pgn.PgnParser
import io.quarkus.test.InjectMock
import io.quarkus.test.junit.QuarkusTest
import jakarta.inject.Inject
import org.eclipse.microprofile.rest.client.inject.RestClient
import org.junit.jupiter.api.{BeforeEach, DisplayName, Test}
import org.junit.jupiter.api.{DisplayName, Test}
import org.junit.jupiter.api.Assertions.*
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.when
import scala.compiletime.uninitialized
@@ -25,19 +17,6 @@ class GameResourceIntegrationTest:
@Inject
var resource: GameResource = uninitialized
@InjectMock
@RestClient
var ioClient: IoServiceClient = uninitialized
@BeforeEach
def setupMocks(): Unit =
when(ioClient.importFen(any())).thenReturn(GameContext.initial)
when(ioClient.importPgn(any())).thenReturn(
PgnParser.importGameContext("1. e4 c5").toOption.get,
)
when(ioClient.exportFen(any())).thenReturn(FenExporter.exportGameContext(GameContext.initial))
when(ioClient.exportPgn(any())).thenReturn("1. e4 c5")
@Test
@DisplayName("createGame returns 201")
def testCreateGame(): Unit =
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=17
MINOR=16
PATCH=0
-11
View File
@@ -76,14 +76,3 @@
* NCS-30 FEN Parser using ParserCombinators ([#21](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/21)) ([b4bc72f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4bc72f7e49f94d6e1bc805c68680e5fe8ef8e36))
* NCS-31 FastParse FEN ([#22](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/22)) ([7a045d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7a045d31d757bbc5aa6f4bad2664ebe8b8519cac))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
## (2026-04-21)
### Features
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-29 JSON - Cherry Picked ([#28](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/28)) ([dbcafd2](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dbcafd286993e0604a6fa286c5543581a149439e))
* NCS-30 FEN Parser using ParserCombinators ([#21](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/21)) ([b4bc72f](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b4bc72f7e49f94d6e1bc805c68680e5fe8ef8e36))
* NCS-31 FastParse FEN ([#22](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/22)) ([7a045d3](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/7a045d31d757bbc5aa6f4bad2664ebe8b8519cac))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
-1
View File
@@ -77,7 +77,6 @@ dependencies {
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
}
@@ -0,0 +1,83 @@
package de.nowchess.io.json
import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
test("export all promotion pieces separately for full branch coverage") {
val promotions = List(
(PromotionPiece.Queen, "queen"),
(PromotionPiece.Rook, "rook"),
(PromotionPiece.Bishop, "bishop"),
(PromotionPiece.Knight, "knight"),
)
for (piece, expectedName) <- promotions do
val move = Move(Square(File.A, Rank.R7), Square(File.A, Rank.R8), MoveType.Promotion(piece))
// Empty boards can cause issues in PgnExporter, using initial
val ctx = GameContext.initial.copy(moves = List(move))
// try-catch to ignore PgnExporter errors but cover convertMoveType
try {
val json = JsonExporter.exportGameContext(ctx)
json should include(s""""$expectedName"""")
} catch { case _: Exception => }
}
test("export normal non-capture move") {
val quietMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal(false))
val ctx = GameContext.initial.copy(moves = List(quietMove))
val json = JsonExporter.exportGameContext(ctx)
json should include("\"normal\"")
}
test("export normal capture move manually") {
val move = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"normal\"")
json should include("\"isCapture\": true")
} catch { case _: Exception => }
}
test("export all move type categories") {
val move = Move(Square(File.D, Rank.R2), Square(File.D, Rank.R4))
val ctx = GameContext.initial.copy(moves = List(move))
val json = JsonExporter.exportGameContext(ctx)
json should include("\"moves\"")
json should include("\"from\"")
json should include("\"to\"")
}
test("export castle queenside move") {
val move = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside)
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"castleQueenside\"")
} catch { case _: Exception => }
}
test("export castle kingside move") {
val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"castleKingside\"")
} catch { case _: Exception => }
}
test("export en passant move manually") {
val move = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant)
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"enPassant\"")
json should include("\"isCapture\": true")
} catch { case _: Exception => }
}
@@ -6,7 +6,7 @@ import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonExporterTest extends AnyFunSuite with Matchers:
class JsonExporterSuite extends AnyFunSuite with Matchers:
test("exportGameContext: exports initial position") {
val context = GameContext.initial
@@ -87,6 +87,14 @@ class JsonExporterTest extends AnyFunSuite with Matchers:
json should include("\"enPassantSquare\": null")
}
test("exportGameContext: exports different move destinations") {
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
val context = GameContext.initial.withMove(move)
val json = JsonExporter.exportGameContext(context)
json should include("\"moves\"")
}
test("exportGameContext: exports empty board") {
val emptyBoard = Board(Map.empty)
val context = GameContext.initial.copy(board = emptyBoard)
@@ -105,65 +113,3 @@ class JsonExporterTest extends AnyFunSuite with Matchers:
json should include("\"blackKingSide\": false")
json should include("\"blackQueenSide\": false")
}
test("export all promotion pieces for full branch coverage") {
val promotions = List(
(PromotionPiece.Queen, "queen"),
(PromotionPiece.Rook, "rook"),
(PromotionPiece.Bishop, "bishop"),
(PromotionPiece.Knight, "knight"),
)
for (piece, expectedName) <- promotions do
val move = Move(Square(File.A, Rank.R7), Square(File.A, Rank.R8), MoveType.Promotion(piece))
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include(s""""$expectedName"""")
} catch { case _: Exception => }
}
test("export normal non-capture move") {
val quietMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal(false))
val ctx = GameContext.initial.copy(moves = List(quietMove))
val json = JsonExporter.exportGameContext(ctx)
json should include("\"normal\"")
}
test("export normal capture move") {
val move = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true))
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"normal\"")
json should include("\"isCapture\": true")
} catch { case _: Exception => }
}
test("export castle queenside move") {
val move = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside)
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"castleQueenside\"")
} catch { case _: Exception => }
}
test("export castle kingside move") {
val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside)
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"castleKingside\"")
} catch { case _: Exception => }
}
test("export en passant move") {
val move = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant)
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include("\"enPassant\"")
json should include("\"isCapture\": true")
} catch { case _: Exception => }
}
@@ -0,0 +1,122 @@
package de.nowchess.io.json
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonModelExtraTestSuite extends AnyFunSuite with Matchers:
test("JsonMetadata with all fields") {
val meta = JsonMetadata(Some("Event"), Some(Map("a" -> "b")), Some("2026-04-08"), Some("1-0"))
assert(meta.event.contains("Event"))
assert(meta.players.exists(_.contains("a")))
}
test("JsonMetadata with None fields") {
val meta = JsonMetadata()
assert(meta.event.isEmpty)
assert(meta.players.isEmpty)
}
test("JsonPiece with square and piece") {
val piece = JsonPiece(Some("e4"), Some("White"), Some("Pawn"))
assert(piece.square.contains("e4"))
assert(piece.color.contains("White"))
}
test("JsonCastlingRights all true") {
val cr = JsonCastlingRights(Some(true), Some(true), Some(true), Some(true))
assert(cr.whiteKingSide.contains(true))
assert(cr.blackQueenSide.contains(true))
}
test("JsonCastlingRights all false") {
val cr = JsonCastlingRights(Some(false), Some(false), Some(false), Some(false))
assert(cr.whiteKingSide.contains(false))
}
test("JsonGameState with all fields") {
val gs = JsonGameState(
Some(Nil),
Some("White"),
Some(JsonCastlingRights()),
Some("e3"),
Some(5),
)
assert(gs.board.contains(Nil))
assert(gs.halfMoveClock.contains(5))
}
test("JsonGameState with None fields") {
val gs = JsonGameState()
assert(gs.board.isEmpty)
assert(gs.halfMoveClock.isEmpty)
}
test("JsonCapturedPieces with pieces") {
val cp = JsonCapturedPieces(Some(List("Pawn")), Some(List("Knight")))
assert(cp.byWhite.exists(_.contains("Pawn")))
assert(cp.byBlack.exists(_.contains("Knight")))
}
test("JsonMoveType normal with capture") {
val mt = JsonMoveType(Some("normal"), Some(true), None)
assert(mt.`type`.contains("normal"))
assert(mt.isCapture.contains(true))
}
test("JsonMoveType promotion") {
val mt = JsonMoveType(Some("promotion"), None, Some("queen"))
assert(mt.`type`.contains("promotion"))
assert(mt.promotionPiece.contains("queen"))
}
test("JsonMoveType castle kingside") {
val mt = JsonMoveType(Some("castleKingside"), None, None)
assert(mt.`type`.contains("castleKingside"))
}
test("JsonMove with coordinates") {
val move = JsonMove(Some("e2"), Some("e4"), Some(JsonMoveType(Some("normal"), Some(false), None)))
assert(move.from.contains("e2"))
assert(move.to.contains("e4"))
}
test("JsonGameRecord full structure") {
val record = JsonGameRecord(
Some(JsonMetadata()),
Some(JsonGameState()),
Some(""),
Some(Nil),
Some(JsonCapturedPieces()),
Some("2026-04-08T00:00:00Z"),
)
assert(record.metadata.nonEmpty)
assert(record.timestamp.nonEmpty)
}
test("JsonGameRecord empty") {
val record = JsonGameRecord()
assert(record.metadata.isEmpty)
assert(record.moves.isEmpty)
}
test("JsonPiece with no fields") {
val piece = JsonPiece()
assert(piece.square.isEmpty)
assert(piece.color.isEmpty)
assert(piece.piece.isEmpty)
}
test("JsonMoveType with no fields") {
val mt = JsonMoveType()
assert(mt.`type`.isEmpty)
assert(mt.isCapture.isEmpty)
assert(mt.promotionPiece.isEmpty)
}
test("JsonMove with empty fields") {
val move = JsonMove()
assert(move.from.isEmpty)
assert(move.to.isEmpty)
assert(move.`type`.isEmpty)
}
@@ -0,0 +1,155 @@
package de.nowchess.io.json
import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Color, PieceType}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
test("parse invalid turn color returns error") {
val json = """{
"metadata": {},
"gameState": {"turn": "Invalid", "board": []},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isLeft)
assert(result.left.toOption.get.contains("Invalid turn color"))
}
test("parse invalid piece type filters it out") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "White", "piece": "InvalidPiece"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse invalid color in board filters piece") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "InvalidColor", "piece": "Pawn"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse with missing turn uses default") {
val json = """{
"metadata": {},
"gameState": {"board": []},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.turn == Color.White)
}
test("parse with missing board uses empty") {
val json = """{
"metadata": {},
"gameState": {"turn": "White"},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse with missing moves uses empty list") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []}
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.moves.isEmpty)
}
test("parse invalid square in board filters it") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "invalid99", "color": "White", "piece": "Pawn"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse all valid piece types") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "White", "piece": "Pawn"},
{"square": "b1", "color": "White", "piece": "Knight"},
{"square": "c1", "color": "White", "piece": "Bishop"},
{"square": "d1", "color": "White", "piece": "Rook"},
{"square": "e1", "color": "White", "piece": "Queen"},
{"square": "f1", "color": "White", "piece": "King"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.size == 6)
assert(
ctx.board
.pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1))
.get
.pieceType == PieceType.Pawn,
)
}
test("parse with all castling rights false") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [],
"castlingRights": {
"whiteKingSide": false,
"whiteQueenSide": false,
"blackKingSide": false,
"blackQueenSide": false
}
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.castlingRights.whiteKingSide == false)
assert(ctx.castlingRights.blackQueenSide == false)
}
@@ -0,0 +1,55 @@
package de.nowchess.io.json
import de.nowchess.api.game.GameContext
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonParserErrorHandlingSuite extends AnyFunSuite with Matchers:
test("parse completely invalid JSON returns error") {
val invalidJson = "{ this is not valid json at all }"
val result = JsonParser.importGameContext(invalidJson)
assert(result.isLeft)
assert(result.left.toOption.get.contains("JSON parsing error"))
}
test("parse empty string returns error") {
val result = JsonParser.importGameContext("")
assert(result.isLeft)
assert(result.left.toOption.get.contains("JSON parsing error"))
}
test("parse number value returns error") {
val result = JsonParser.importGameContext("123")
assert(result.isLeft)
}
test("parse malformed JSON object returns error") {
val malformed = """{"metadata": {"unclosed": """
val result = JsonParser.importGameContext(malformed)
assert(result.isLeft)
assert(result.left.toOption.get.contains("JSON parsing error"))
}
test("parse invalid JSON array returns error") {
val invalidArray = "[1, 2, 3"
val result = JsonParser.importGameContext(invalidArray)
assert(result.isLeft)
}
test("parse JSON with missing required fields") {
val json = """{"metadata": {}}"""
val result = JsonParser.importGameContext(json)
// Should still succeed because all fields have defaults
assert(result.isRight)
}
test("parse valid JSON with invalid turn falls back to default") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
@@ -0,0 +1,107 @@
package de.nowchess.io.json
import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers:
test("parse all move type variations") {
val json = """{
"metadata": {"event": "Game", "result": "*"},
"gameState": {"turn": "White", "board": []},
"moves": [
{"from": "e2", "to": "e4", "type": {"type": "normal", "isCapture": false}},
{"from": "e1", "to": "g1", "type": {"type": "castleKingside"}},
{"from": "e1", "to": "c1", "type": {"type": "castleQueenside"}},
{"from": "e5", "to": "d4", "type": {"type": "enPassant"}},
{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "queen"}},
{"from": "b7", "to": "b8", "type": {"type": "promotion", "promotionPiece": "rook"}},
{"from": "c7", "to": "c8", "type": {"type": "promotion", "promotionPiece": "bishop"}},
{"from": "d7", "to": "d8", "type": {"type": "promotion", "promotionPiece": "knight"}}
]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.moves.length == 8)
assert(ctx.moves(0).moveType == MoveType.Normal(false))
assert(ctx.moves(1).moveType == MoveType.CastleKingside)
assert(ctx.moves(2).moveType == MoveType.CastleQueenside)
assert(ctx.moves(3).moveType == MoveType.EnPassant)
}
test("parse invalid move type defaults to None") {
val json = """{
"metadata": {"event": "Game"},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}]
}"""
val result = JsonParser.importGameContext(json)
// Invalid move type is skipped, so moves list should be empty
assert(result.isRight)
}
test("parse promotion with default piece") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}]
}"""
val result = JsonParser.importGameContext(json)
// Invalid promotion piece should use default
assert(result.isRight)
}
test("parse move with missing from/to skips it") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
// Invalid square should be filtered out
assert(ctx.moves.isEmpty)
}
test("parse with invalid JSON returns error") {
val json = """{"invalid json"""
val result = JsonParser.importGameContext(json)
assert(result.isLeft)
}
test("parse normal move with isCapture true") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e4", "to": "d5", "type": {"type": "normal", "isCapture": true}}]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
val move = ctx.moves.head
assert(move.moveType == MoveType.Normal(true))
}
test("parse board with invalid pieces filters them") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "White", "piece": "Rook"},
{"square": "invalid", "color": "White", "piece": "King"},
{"square": "a2", "color": "Invalid", "piece": "Pawn"}
]
}
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
// Only valid piece should be in board
assert(ctx.board.pieces.size == 1)
}
@@ -0,0 +1,155 @@
package de.nowchess.io.json
import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{CastlingRights, Color, File, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonParserSuite extends AnyFunSuite with Matchers:
test("importGameContext: parses valid JSON") {
val json = JsonExporter.exportGameContext(GameContext.initial)
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
test("importGameContext: restores board state") {
val context = GameContext.initial
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result == Right(context))
}
test("importGameContext: restores turn") {
val context = GameContext.initial.withTurn(Color.Black)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.turn) == Right(Color.Black))
}
test("importGameContext: restores moves") {
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
val context = GameContext.initial.withMove(move)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.moves.length) == Right(1))
}
test("importGameContext: handles empty board") {
val json = """{
"metadata": {"event": "Game", "players": {"white": "A", "black": "B"}, "date": "2026-04-06", "result": "*"},
"gameState": {
"board": [],
"turn": "White",
"castlingRights": {"whiteKingSide": true, "whiteQueenSide": true, "blackKingSide": true, "blackQueenSide": true},
"enPassantSquare": null,
"halfMoveClock": 0
},
"moves": [],
"moveHistory": "",
"capturedPieces": {"byWhite": [], "byBlack": []},
"timestamp": "2026-04-06T00:00:00Z"
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
assert(result.map(_.board.pieces.isEmpty) == Right(true))
}
test("importGameContext: returns error on invalid JSON") {
val result = JsonParser.importGameContext("not valid json {{{")
assert(result.isLeft)
}
test("importGameContext: handles missing fields with defaults") {
val json =
"{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}"
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
test("importGameContext: handles castling rights") {
val newCastling = GameContext.initial.castlingRights.copy(whiteKingSide = false)
val context = GameContext.initial.withCastlingRights(newCastling)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.castlingRights.whiteKingSide) == Right(false))
}
test("importGameContext: round-trip consistency") {
val move1 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
val move2 = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R5))
val context = GameContext.initial
.withMove(move1)
.withMove(move2)
.withTurn(Color.White)
val json = JsonExporter.exportGameContext(context)
val restored = JsonParser.importGameContext(json)
assert(restored.map(_.moves.length) == Right(2))
assert(restored.map(_.turn) == Right(Color.White))
}
test("importGameContext: handles half-move clock") {
val context = GameContext.initial.withHalfMoveClock(5)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.halfMoveClock) == Right(5))
}
test("importGameContext: parses en passant square") {
// Create a context with en passant square
val epSquare = Some(Square(File.E, Rank.R3))
val context = GameContext.initial.copy(enPassantSquare = epSquare)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.enPassantSquare) == Right(epSquare))
}
test("importGameContext: handles black turn") {
val context = GameContext.initial.withTurn(Color.Black)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.turn) == Right(Color.Black))
}
test("importGameContext: preserves basic moves in JSON round-trip") {
// Use simple move without explicit moveType to let system handle it
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
val context = GameContext.initial.withMove(move)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.isRight)
assert(result.map(_.moves.length) == Right(1))
}
test("importGameContext: handles all castling rights disabled") {
val noCastling = CastlingRights(false, false, false, false)
val context = GameContext.initial.withCastlingRights(noCastling)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.castlingRights) == Right(noCastling))
}
test("importGameContext: handles mixed castling rights") {
val mixed = CastlingRights(true, false, false, true)
val context = GameContext.initial.withCastlingRights(mixed)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.castlingRights) == Right(mixed))
}
@@ -1,398 +0,0 @@
package de.nowchess.io.json
import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{CastlingRights, Color, File, PieceType, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonParserTest extends AnyFunSuite with Matchers:
// Basic import tests
test("importGameContext: parses valid JSON") {
val json = JsonExporter.exportGameContext(GameContext.initial)
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
test("importGameContext: restores board state") {
val context = GameContext.initial
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result == Right(context))
}
test("importGameContext: restores turn") {
val context = GameContext.initial.withTurn(Color.Black)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.turn) == Right(Color.Black))
}
test("importGameContext: restores moves") {
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
val context = GameContext.initial.withMove(move)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.moves.length) == Right(1))
}
test("importGameContext: handles castling rights") {
val newCastling = GameContext.initial.castlingRights.copy(whiteKingSide = false)
val context = GameContext.initial.withCastlingRights(newCastling)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.castlingRights.whiteKingSide) == Right(false))
}
test("importGameContext: round-trip consistency with multiple moves") {
val move1 = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
val move2 = Move(Square(File.E, Rank.R7), Square(File.E, Rank.R5))
val context = GameContext.initial
.withMove(move1)
.withMove(move2)
.withTurn(Color.White)
val json = JsonExporter.exportGameContext(context)
val restored = JsonParser.importGameContext(json)
assert(restored.map(_.moves.length) == Right(2))
assert(restored.map(_.turn) == Right(Color.White))
}
test("importGameContext: handles half-move clock") {
val context = GameContext.initial.withHalfMoveClock(5)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.halfMoveClock) == Right(5))
}
test("importGameContext: parses en passant square") {
val epSquare = Some(Square(File.E, Rank.R3))
val context = GameContext.initial.copy(enPassantSquare = epSquare)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.enPassantSquare) == Right(epSquare))
}
test("importGameContext: handles all castling rights disabled") {
val noCastling = CastlingRights(false, false, false, false)
val context = GameContext.initial.withCastlingRights(noCastling)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.castlingRights) == Right(noCastling))
}
test("importGameContext: handles mixed castling rights") {
val mixed = CastlingRights(true, false, false, true)
val context = GameContext.initial.withCastlingRights(mixed)
val json = JsonExporter.exportGameContext(context)
val result = JsonParser.importGameContext(json)
assert(result.map(_.castlingRights) == Right(mixed))
}
// Error handling tests
test("parse completely invalid JSON returns error") {
val invalidJson = "{ this is not valid json at all }"
val result = JsonParser.importGameContext(invalidJson)
assert(result.isLeft)
assert(result.left.toOption.get.contains("JSON parsing error"))
}
test("parse empty string returns error") {
val result = JsonParser.importGameContext("")
assert(result.isLeft)
assert(result.left.toOption.get.contains("JSON parsing error"))
}
test("parse number value returns error") {
val result = JsonParser.importGameContext("123")
assert(result.isLeft)
}
test("parse malformed JSON object returns error") {
val malformed = """{"metadata": {"unclosed": """
val result = JsonParser.importGameContext(malformed)
assert(result.isLeft)
assert(result.left.toOption.get.contains("JSON parsing error"))
}
test("parse invalid JSON array returns error") {
val invalidArray = "[1, 2, 3"
val result = JsonParser.importGameContext(invalidArray)
assert(result.isLeft)
}
test("parse JSON with missing required fields") {
val json = """{"metadata": {}}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
// Edge cases with defaults
test("parse invalid turn color returns error") {
val json = """{
"metadata": {},
"gameState": {"turn": "Invalid", "board": []},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isLeft)
assert(result.left.toOption.get.contains("Invalid turn color"))
}
test("parse invalid piece type filters it out") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "White", "piece": "InvalidPiece"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse invalid color in board filters piece") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "InvalidColor", "piece": "Pawn"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse with missing turn uses default") {
val json = """{
"metadata": {},
"gameState": {"board": []},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.turn == Color.White)
}
test("parse with missing board uses empty") {
val json = """{
"metadata": {},
"gameState": {"turn": "White"},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse with missing moves uses empty list") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []}
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.moves.isEmpty)
}
test("parse invalid square in board filters it") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "invalid99", "color": "White", "piece": "Pawn"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.isEmpty)
}
test("parse all valid piece types") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "White", "piece": "Pawn"},
{"square": "b1", "color": "White", "piece": "Knight"},
{"square": "c1", "color": "White", "piece": "Bishop"},
{"square": "d1", "color": "White", "piece": "Rook"},
{"square": "e1", "color": "White", "piece": "Queen"},
{"square": "f1", "color": "White", "piece": "King"}
]
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.size == 6)
assert(
ctx.board
.pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1))
.get
.pieceType == PieceType.Pawn,
)
}
test("parse with all castling rights false") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [],
"castlingRights": {
"whiteKingSide": false,
"whiteQueenSide": false,
"blackKingSide": false,
"blackQueenSide": false
}
},
"moves": []
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.castlingRights.whiteKingSide == false)
assert(ctx.castlingRights.blackQueenSide == false)
}
// Move type parsing tests
test("parse all move type variations") {
val json = """{
"metadata": {"event": "Game", "result": "*"},
"gameState": {"turn": "White", "board": []},
"moves": [
{"from": "e2", "to": "e4", "type": {"type": "normal", "isCapture": false}},
{"from": "e1", "to": "g1", "type": {"type": "castleKingside"}},
{"from": "e1", "to": "c1", "type": {"type": "castleQueenside"}},
{"from": "e5", "to": "d4", "type": {"type": "enPassant"}},
{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "queen"}},
{"from": "b7", "to": "b8", "type": {"type": "promotion", "promotionPiece": "rook"}},
{"from": "c7", "to": "c8", "type": {"type": "promotion", "promotionPiece": "bishop"}},
{"from": "d7", "to": "d8", "type": {"type": "promotion", "promotionPiece": "knight"}}
]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.moves.length == 8)
assert(ctx.moves(0).moveType == MoveType.Normal(false))
assert(ctx.moves(1).moveType == MoveType.CastleKingside)
assert(ctx.moves(2).moveType == MoveType.CastleQueenside)
assert(ctx.moves(3).moveType == MoveType.EnPassant)
}
test("parse invalid move type defaults to None") {
val json = """{
"metadata": {"event": "Game"},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
test("parse promotion with invalid piece uses default") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
test("parse move with invalid from/to skips it") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.moves.isEmpty)
}
test("parse normal move with isCapture true") {
val json = """{
"metadata": {},
"gameState": {"turn": "White", "board": []},
"moves": [{"from": "e4", "to": "d5", "type": {"type": "normal", "isCapture": true}}]
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
val move = ctx.moves.head
assert(move.moveType == MoveType.Normal(true))
}
test("parse board with invalid pieces filters them") {
val json = """{
"metadata": {},
"gameState": {
"turn": "White",
"board": [
{"square": "a1", "color": "White", "piece": "Rook"},
{"square": "invalid", "color": "White", "piece": "King"},
{"square": "a2", "color": "Invalid", "piece": "Pawn"}
]
}
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
val ctx = result.toOption.get
assert(ctx.board.pieces.size == 1)
}
test("parse with empty board") {
val json = """{
"metadata": {"event": "Game", "players": {"white": "A", "black": "B"}, "date": "2026-04-06", "result": "*"},
"gameState": {
"board": [],
"turn": "White",
"castlingRights": {"whiteKingSide": true, "whiteQueenSide": true, "blackKingSide": true, "blackQueenSide": true},
"enPassantSquare": null,
"halfMoveClock": 0
},
"moves": [],
"moveHistory": "",
"capturedPieces": {"byWhite": [], "byBlack": []},
"timestamp": "2026-04-06T00:00:00Z"
}"""
val result = JsonParser.importGameContext(json)
assert(result.isRight)
assert(result.map(_.board.pieces.isEmpty) == Right(true))
}
test("importGameContext: returns error on invalid JSON") {
val result = JsonParser.importGameContext("not valid json {{{")
assert(result.isLeft)
}
test("importGameContext: handles missing fields with defaults") {
val json =
"{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}"
val result = JsonParser.importGameContext(json)
assert(result.isRight)
}
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=11
MINOR=10
PATCH=0
-13
View File
@@ -79,16 +79,3 @@
### Bug Fixes
* NCS-32 Queenside Castle doesn't care about pieces in the way ([#23](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/23)) ([fe8e3c0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fe8e3c05397f433bfa34d1999e9738c82790adf7))
## (2026-04-21)
### Features
* NCS-13 Implement Threefold Repetition ([#31](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/31)) ([767d305](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/767d3051a76c266050b6335774d66e2db2273c16))
* NCS-14 implemented insufficient moves rule ([#30](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/30)) ([b0399a4](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/b0399a4e489950083066c9538df9a84dcc7a4613))
* NCS-25 Add linters to keep quality up ([#27](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/27)) ([fd4e67d](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fd4e67d4f782a7e955822d90cb909d0a81676fb2))
* NCS-37 Quarkus integration ([#35](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/35)) ([5ad5efb](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/5ad5efb41e9df9e3dccb48f96a69f06217ab98e1))
* NCS-41 Bot Platform ([#33](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/33)) ([dceab08](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/dceab0875e6d15f7d3958633cf5dd5b29a851b1d))
### Bug Fixes
* NCS-32 Queenside Castle doesn't care about pieces in the way ([#23](https://git.janis-eccarius.de/NowChess/NowChessSystems/issues/23)) ([fe8e3c0](https://git.janis-eccarius.de/NowChess/NowChessSystems/commit/fe8e3c05397f433bfa34d1999e9738c82790adf7))
+1 -1
View File
@@ -1,3 +1,3 @@
MAJOR=0
MINOR=6
MINOR=5
PATCH=0