Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fc97bde02 | |||
| 2d75b2e80e | |||
| f088c4e9ff | |||
| 8a1cf909d4 | |||
| 33e785d22a | |||
| d16cec176b | |||
| 8744bee2dd |
Generated
+1
-1
@@ -5,7 +5,7 @@
|
||||
<option name="deprecationWarnings" value="true" />
|
||||
<option name="uncheckedWarnings" value="true" />
|
||||
</profile>
|
||||
<profile name="Gradle 2" modules="NowChessSystems.modules.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,NowChessSystems.modules.core.scoverage,NowChessSystems.modules.core.test,NowChessSystems.modules.io.integrationTest,NowChessSystems.modules.io.main,NowChessSystems.modules.io.native-test,NowChessSystems.modules.io.quarkus-generated-sources,NowChessSystems.modules.io.quarkus-test-generated-sources,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.bot.main,NowChessSystems.modules.bot.scoverage,NowChessSystems.modules.bot.test,NowChessSystems.modules.core.integrationTest,NowChessSystems.modules.core.main,NowChessSystems.modules.core.native-test,NowChessSystems.modules.core.quarkus-generated-sources,NowChessSystems.modules.core.quarkus-test-generated-sources,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>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
vars {
|
||||
baseUrl: http://localhost:8080
|
||||
ioBaseUrl: http://localhost:8081
|
||||
}
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
meta {
|
||||
name: Export FEN
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
http {
|
||||
method: POST
|
||||
url: {{ioBaseUrl}}/io/export/fen
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"board": {
|
||||
"a1": {"color": "White", "pieceType": "Rook"},
|
||||
"b1": {"color": "White", "pieceType": "Knight"},
|
||||
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||
"d1": {"color": "White", "pieceType": "Queen"},
|
||||
"e1": {"color": "White", "pieceType": "King"},
|
||||
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||
"g1": {"color": "White", "pieceType": "Knight"},
|
||||
"h1": {"color": "White", "pieceType": "Rook"},
|
||||
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||
"e8": {"color": "Black", "pieceType": "King"},
|
||||
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||
},
|
||||
"turn": "White",
|
||||
"castlingRights": {
|
||||
"whiteKingSide": true,
|
||||
"whiteQueenSide": true,
|
||||
"blackKingSide": true,
|
||||
"blackQueenSide": true
|
||||
},
|
||||
"enPassantSquare": null,
|
||||
"halfMoveClock": 0,
|
||||
"moves": [],
|
||||
"result": null,
|
||||
"initialBoard": {
|
||||
"a1": {"color": "White", "pieceType": "Rook"},
|
||||
"b1": {"color": "White", "pieceType": "Knight"},
|
||||
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||
"d1": {"color": "White", "pieceType": "Queen"},
|
||||
"e1": {"color": "White", "pieceType": "King"},
|
||||
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||
"g1": {"color": "White", "pieceType": "Knight"},
|
||||
"h1": {"color": "White", "pieceType": "Rook"},
|
||||
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||
"e8": {"color": "Black", "pieceType": "King"},
|
||||
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
meta {
|
||||
name: Export PGN
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
http {
|
||||
method: POST
|
||||
url: {{ioBaseUrl}}/io/export/pgn
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"board": {
|
||||
"a1": {"color": "White", "pieceType": "Rook"},
|
||||
"b1": {"color": "White", "pieceType": "Knight"},
|
||||
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||
"d1": {"color": "White", "pieceType": "Queen"},
|
||||
"e1": {"color": "White", "pieceType": "King"},
|
||||
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||
"g1": {"color": "White", "pieceType": "Knight"},
|
||||
"h1": {"color": "White", "pieceType": "Rook"},
|
||||
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||
"e8": {"color": "Black", "pieceType": "King"},
|
||||
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||
},
|
||||
"turn": "White",
|
||||
"castlingRights": {
|
||||
"whiteKingSide": true,
|
||||
"whiteQueenSide": true,
|
||||
"blackKingSide": true,
|
||||
"blackQueenSide": true
|
||||
},
|
||||
"enPassantSquare": null,
|
||||
"halfMoveClock": 0,
|
||||
"moves": [],
|
||||
"result": null,
|
||||
"initialBoard": {
|
||||
"a1": {"color": "White", "pieceType": "Rook"},
|
||||
"b1": {"color": "White", "pieceType": "Knight"},
|
||||
"c1": {"color": "White", "pieceType": "Bishop"},
|
||||
"d1": {"color": "White", "pieceType": "Queen"},
|
||||
"e1": {"color": "White", "pieceType": "King"},
|
||||
"f1": {"color": "White", "pieceType": "Bishop"},
|
||||
"g1": {"color": "White", "pieceType": "Knight"},
|
||||
"h1": {"color": "White", "pieceType": "Rook"},
|
||||
"a2": {"color": "White", "pieceType": "Pawn"},
|
||||
"b2": {"color": "White", "pieceType": "Pawn"},
|
||||
"c2": {"color": "White", "pieceType": "Pawn"},
|
||||
"d2": {"color": "White", "pieceType": "Pawn"},
|
||||
"e2": {"color": "White", "pieceType": "Pawn"},
|
||||
"f2": {"color": "White", "pieceType": "Pawn"},
|
||||
"g2": {"color": "White", "pieceType": "Pawn"},
|
||||
"h2": {"color": "White", "pieceType": "Pawn"},
|
||||
"a7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"b7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"c7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"d7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"e7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"f7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"g7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"h7": {"color": "Black", "pieceType": "Pawn"},
|
||||
"a8": {"color": "Black", "pieceType": "Rook"},
|
||||
"b8": {"color": "Black", "pieceType": "Knight"},
|
||||
"c8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"d8": {"color": "Black", "pieceType": "Queen"},
|
||||
"e8": {"color": "Black", "pieceType": "King"},
|
||||
"f8": {"color": "Black", "pieceType": "Bishop"},
|
||||
"g8": {"color": "Black", "pieceType": "Knight"},
|
||||
"h8": {"color": "Black", "pieceType": "Rook"}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
meta {
|
||||
name: export
|
||||
seq: 2
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: Import FEN
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
http {
|
||||
method: POST
|
||||
url: {{ioBaseUrl}}/io/import/fen
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
meta {
|
||||
name: Import PGN
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
http {
|
||||
method: POST
|
||||
url: {{ioBaseUrl}}/io/import/pgn
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
headers {
|
||||
Content-Type: application/json
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"pgn": "1. e4 e5 2. Nf3 Nc6 *"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
meta {
|
||||
name: import
|
||||
seq: 1
|
||||
}
|
||||
@@ -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"]!!}")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"created": "2026-04-13T19:58:38.629943",
|
||||
"total_positions": 3022562,
|
||||
"stockfish_depth": 12,
|
||||
"sources": [
|
||||
{
|
||||
"type": "legacy_import",
|
||||
"path": "data/training_data.jsonl",
|
||||
"count": 2009355,
|
||||
"note": "Migrated from data/training_data.jsonl"
|
||||
},
|
||||
{
|
||||
"type": "test_extend",
|
||||
"count": 4,
|
||||
"actual_count": 3
|
||||
},
|
||||
{
|
||||
"type": "test_new_positions",
|
||||
"count": 3,
|
||||
"actual_count": 3
|
||||
},
|
||||
{
|
||||
"type": "test_mixed",
|
||||
"count": 5,
|
||||
"actual_count": 0
|
||||
},
|
||||
{
|
||||
"type": "test_all_dups",
|
||||
"count": 2,
|
||||
"actual_count": 0
|
||||
},
|
||||
{
|
||||
"type": "guaranteed_unique",
|
||||
"count": 10,
|
||||
"actual_count": 8
|
||||
},
|
||||
{
|
||||
"type": "merged_sources",
|
||||
"count": 600000,
|
||||
"sources": [
|
||||
{
|
||||
"type": "tactical",
|
||||
"count": 600000,
|
||||
"max_puzzles": 600000
|
||||
}
|
||||
],
|
||||
"actual_count": 599993
|
||||
},
|
||||
{
|
||||
"type": "merged_sources",
|
||||
"count": 500000,
|
||||
"sources": [
|
||||
{
|
||||
"type": "lichess",
|
||||
"count": 500000,
|
||||
"params": {
|
||||
"min_depth": 20,
|
||||
"max_positions": 500000
|
||||
}
|
||||
}
|
||||
],
|
||||
"actual_count": 500000
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
quarkus:
|
||||
http:
|
||||
port: 8080
|
||||
application:
|
||||
name: nowchess-core
|
||||
rest-client:
|
||||
io-service:
|
||||
url: http://localhost:8081
|
||||
greeting:
|
||||
message: "hello"
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package de.nowchess.chess.client
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.MediaType
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||
|
||||
@Path("/io")
|
||||
@RegisterRestClient(configKey = "io-service")
|
||||
trait IoServiceClient:
|
||||
|
||||
@POST
|
||||
@Path("/import/fen")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def importFen(body: ImportFenRequest): GameContext
|
||||
|
||||
@POST
|
||||
@Path("/import/pgn")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def importPgn(body: ImportPgnRequest): GameContext
|
||||
|
||||
@POST
|
||||
@Path("/export/fen")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.TEXT_PLAIN))
|
||||
def exportFen(ctx: GameContext): String
|
||||
|
||||
@POST
|
||||
@Path("/export/pgn")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array("application/x-chess-pgn"))
|
||||
def exportPgn(ctx: GameContext): String
|
||||
@@ -2,10 +2,7 @@ package de.nowchess.chess.config
|
||||
|
||||
import com.fasterxml.jackson.core.Version
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@@ -18,7 +15,3 @@ class JacksonConfig extends ObjectMapperCustomizer:
|
||||
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||
// scalafix:on DisableSyntax.null
|
||||
})
|
||||
val squareModule = new SimpleModule()
|
||||
squareModule.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||
squareModule.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||
mapper.registerModule(squareModule)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package de.nowchess.chess.config
|
||||
|
||||
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.dto.*
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
|
||||
@RegisterForReflection(
|
||||
@@ -21,21 +18,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
classOf[LegalMovesResponseDto],
|
||||
classOf[OkResponseDto],
|
||||
classOf[PlayerInfoDto],
|
||||
|
||||
classOf[GameContext],
|
||||
classOf[Color],
|
||||
classOf[Piece],
|
||||
classOf[PieceType],
|
||||
classOf[CastlingRights],
|
||||
classOf[Square],
|
||||
classOf[File],
|
||||
classOf[Rank],
|
||||
classOf[Move],
|
||||
classOf[MoveType],
|
||||
classOf[PromotionPiece],
|
||||
classOf[GameResult],
|
||||
classOf[DrawReason],
|
||||
|
||||
),
|
||||
)
|
||||
class NativeReflectionConfig
|
||||
|
||||
@@ -6,21 +6,18 @@ import de.nowchess.api.dto.*
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||
import de.nowchess.chess.client.IoServiceClient
|
||||
import de.nowchess.chess.controller.Parser
|
||||
import de.nowchess.chess.engine.GameEngine
|
||||
import de.nowchess.chess.exception.{BadRequestException, GameNotFoundException}
|
||||
import de.nowchess.chess.observer.*
|
||||
import de.nowchess.chess.registry.{GameEntry, GameRegistry}
|
||||
import de.nowchess.io.fen.FenExporter
|
||||
import de.nowchess.io.pgn.PgnExporter
|
||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest}
|
||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||
import io.smallrye.mutiny.Multi
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.{MediaType, Response}
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import scala.compiletime.uninitialized
|
||||
@@ -35,10 +32,6 @@ class GameResource:
|
||||
|
||||
@Inject
|
||||
var objectMapper: ObjectMapper = uninitialized
|
||||
|
||||
@Inject
|
||||
@RestClient
|
||||
var ioClient: IoServiceClient = uninitialized
|
||||
// scalafix:on DisableSyntax.var
|
||||
|
||||
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
|
||||
@@ -149,6 +142,7 @@ class GameResource:
|
||||
val black = playerInfoFrom(req.black, DefaultBlack)
|
||||
val entry = newEntry(GameContext.initial, white, black)
|
||||
registry.store(entry)
|
||||
println(s"Created game ${entry.gameId}")
|
||||
created(toGameFullDto(entry))
|
||||
|
||||
@GET
|
||||
@@ -270,7 +264,9 @@ class GameResource:
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def importFen(body: ImportFenRequestDto): Response =
|
||||
val ctx = ioClient.importFen(ImportFenRequest(body.fen))
|
||||
val ctx = FenParser.parseFen(body.fen) match
|
||||
case Left(err) => throw BadRequestException("INVALID_FEN", err, Some("fen"))
|
||||
case Right(ctx) => ctx
|
||||
val white = playerInfoFrom(body.white, DefaultWhite)
|
||||
val black = playerInfoFrom(body.black, DefaultBlack)
|
||||
val entry = newEntry(ctx, white, black)
|
||||
@@ -282,8 +278,11 @@ class GameResource:
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
def importPgn(body: ImportPgnRequestDto): Response =
|
||||
val ctx = ioClient.importPgn(ImportPgnRequest(body.pgn))
|
||||
val entry = newEntry(ctx, DefaultWhite, DefaultBlack)
|
||||
val engine = GameEngine()
|
||||
engine.loadGame(PgnParser, body.pgn) match
|
||||
case Left(err) => throw BadRequestException("INVALID_PGN", err, Some("pgn"))
|
||||
case Right(_) => ()
|
||||
val entry = GameEntry(registry.generateId(), engine, DefaultWhite, DefaultBlack)
|
||||
registry.store(entry)
|
||||
created(toGameFullDto(entry))
|
||||
|
||||
@@ -292,12 +291,21 @@ class GameResource:
|
||||
@Produces(Array(MediaType.TEXT_PLAIN))
|
||||
def exportFen(@PathParam("gameId") gameId: String): Response =
|
||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||
ok(ioClient.exportFen(entry.engine.context))
|
||||
ok(FenExporter.exportGameContext(entry.engine.context))
|
||||
|
||||
@GET
|
||||
@Path("/{gameId}/export/pgn")
|
||||
@Produces(Array("application/x-chess-pgn"))
|
||||
def exportPgn(@PathParam("gameId") gameId: String): Response =
|
||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||
ok(ioClient.exportPgn(entry.engine.context))
|
||||
val pgn = PgnExporter.exportGame(
|
||||
Map(
|
||||
"Event" -> "NowChess game",
|
||||
"White" -> entry.white.displayName,
|
||||
"Black" -> entry.black.displayName,
|
||||
"Result" -> "*",
|
||||
),
|
||||
entry.engine.context.moves,
|
||||
)
|
||||
ok(pgn)
|
||||
// scalafix:on DisableSyntax.throw
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
plugins {
|
||||
id("scala")
|
||||
id("org.scoverage") version "8.1"
|
||||
id("io.quarkus")
|
||||
}
|
||||
|
||||
group = "de.nowchess"
|
||||
@@ -29,10 +28,6 @@ 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 {
|
||||
|
||||
compileOnly("org.scala-lang:scala3-compiler_3") {
|
||||
@@ -58,50 +53,19 @@ dependencies {
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
||||
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${versions["JACKSON"]!!}")
|
||||
|
||||
implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
|
||||
implementation("io.quarkus:quarkus-rest")
|
||||
implementation("io.quarkus:quarkus-rest-jackson")
|
||||
implementation("io.quarkus:quarkus-arc")
|
||||
implementation("io.quarkus:quarkus-config-yaml")
|
||||
implementation("io.quarkus:quarkus-smallrye-health")
|
||||
implementation("io.quarkus:quarkus-smallrye-openapi")
|
||||
|
||||
testImplementation(platform("org.junit:junit-bom:5.13.4"))
|
||||
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.rest-assured:rest-assured")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
includeEngines("scalatest")
|
||||
testLogging {
|
||||
events("passed", "skipped", "failed")
|
||||
events("skipped", "failed")
|
||||
}
|
||||
}
|
||||
finalizedBy(tasks.reportScoverage)
|
||||
@@ -109,6 +73,3 @@ tasks.test {
|
||||
tasks.reportScoverage {
|
||||
dependsOn(tasks.test)
|
||||
}
|
||||
tasks.jar {
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
quarkus:
|
||||
http:
|
||||
port: 8081
|
||||
application:
|
||||
name: nowchess-io
|
||||
smallrye-openapi:
|
||||
info-title: NowChess IO Service
|
||||
info-version: 1.0.0
|
||||
info-description: Chess notation import and export — FEN and PGN
|
||||
path: /openapi
|
||||
swagger-ui:
|
||||
always-include: true
|
||||
path: /swagger-ui
|
||||
@@ -1,8 +0,0 @@
|
||||
package de.nowchess.io.json
|
||||
|
||||
import com.fasterxml.jackson.databind.{DeserializationContext, KeyDeserializer}
|
||||
import de.nowchess.api.board.Square
|
||||
|
||||
class SquareKeyDeserializer extends KeyDeserializer:
|
||||
override def deserializeKey(key: String, ctx: DeserializationContext): AnyRef =
|
||||
Square.fromAlgebraic(key).orNull
|
||||
@@ -1,9 +0,0 @@
|
||||
package de.nowchess.io.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
|
||||
import de.nowchess.api.board.Square
|
||||
|
||||
class SquareKeySerializer extends JsonSerializer[Square]:
|
||||
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
|
||||
gen.writeFieldName(value.toString)
|
||||
@@ -1,24 +0,0 @@
|
||||
package de.nowchess.io.service.config
|
||||
|
||||
import com.fasterxml.jackson.core.Version
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.io.json.{SquareKeyDeserializer, SquareKeySerializer}
|
||||
import io.quarkus.jackson.ObjectMapperCustomizer
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class JacksonConfig extends ObjectMapperCustomizer:
|
||||
def customize(mapper: ObjectMapper): Unit =
|
||||
mapper.registerModule(new DefaultScalaModule() {
|
||||
override def version(): Version =
|
||||
// scalafix:off DisableSyntax.null
|
||||
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
|
||||
// scalafix:on DisableSyntax.null
|
||||
})
|
||||
val squareModule = new SimpleModule()
|
||||
squareModule.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||
squareModule.addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||
mapper.registerModule(squareModule)
|
||||
@@ -1,29 +0,0 @@
|
||||
package de.nowchess.io.service.config
|
||||
|
||||
import de.nowchess.api.board.{CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
|
||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||
import io.quarkus.runtime.annotations.RegisterForReflection
|
||||
|
||||
@RegisterForReflection(
|
||||
targets = Array(
|
||||
classOf[ImportFenRequest],
|
||||
classOf[ImportPgnRequest],
|
||||
classOf[IoErrorDto],
|
||||
classOf[GameContext],
|
||||
classOf[GameResult],
|
||||
classOf[DrawReason],
|
||||
classOf[Color],
|
||||
classOf[Piece],
|
||||
classOf[PieceType],
|
||||
classOf[CastlingRights],
|
||||
classOf[Square],
|
||||
classOf[File],
|
||||
classOf[Rank],
|
||||
classOf[Move],
|
||||
classOf[MoveType],
|
||||
classOf[PromotionPiece],
|
||||
),
|
||||
)
|
||||
class NativeReflectionConfig
|
||||
@@ -1,3 +0,0 @@
|
||||
package de.nowchess.io.service.dto
|
||||
|
||||
case class ImportFenRequest(fen: String)
|
||||
@@ -1,3 +0,0 @@
|
||||
package de.nowchess.io.service.dto
|
||||
|
||||
case class ImportPgnRequest(pgn: String)
|
||||
@@ -1,3 +0,0 @@
|
||||
package de.nowchess.io.service.dto
|
||||
|
||||
case class IoErrorDto(code: String, message: String)
|
||||
@@ -1,77 +0,0 @@
|
||||
package de.nowchess.io.service.resource
|
||||
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||
import io.smallrye.mutiny.Uni
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.ws.rs.*
|
||||
import jakarta.ws.rs.core.{MediaType, Response}
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation
|
||||
import org.eclipse.microprofile.openapi.annotations.media.{Content, Schema}
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.{APIResponse, APIResponses}
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag
|
||||
|
||||
@Path("/io")
|
||||
@ApplicationScoped
|
||||
@Tag(name = "IO", description = "Chess notation import and export")
|
||||
class IoResource:
|
||||
|
||||
@POST
|
||||
@Path("/import/fen")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
@Operation(summary = "Import FEN", description = "Parse a FEN string into a GameContext")
|
||||
@APIResponses(
|
||||
Array(
|
||||
new APIResponse(responseCode = "200", description = "Parsed GameContext"),
|
||||
new APIResponse(responseCode = "400", description = "Invalid FEN"),
|
||||
),
|
||||
)
|
||||
def importFen(body: ImportFenRequest): Uni[Response] =
|
||||
Uni.createFrom().item {
|
||||
FenParser.parseFen(body.fen) match
|
||||
case Left(err) =>
|
||||
Response.status(400).entity(IoErrorDto("INVALID_FEN", err)).build()
|
||||
case Right(ctx) =>
|
||||
Response.ok(ctx).build()
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/import/pgn")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
@Operation(summary = "Import PGN", description = "Parse a PGN string into a GameContext")
|
||||
@APIResponses(
|
||||
Array(
|
||||
new APIResponse(responseCode = "200", description = "Parsed GameContext"),
|
||||
new APIResponse(responseCode = "400", description = "Invalid PGN"),
|
||||
),
|
||||
)
|
||||
def importPgn(body: ImportPgnRequest): Uni[Response] =
|
||||
Uni.createFrom().item {
|
||||
PgnParser.importGameContext(body.pgn) match
|
||||
case Left(err) =>
|
||||
Response.status(400).entity(IoErrorDto("INVALID_PGN", err)).build()
|
||||
case Right(ctx) =>
|
||||
Response.ok(ctx).build()
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/export/fen")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.TEXT_PLAIN))
|
||||
@Operation(summary = "Export FEN", description = "Serialize a GameContext to FEN notation")
|
||||
@APIResponse(responseCode = "200", description = "FEN string")
|
||||
def exportFen(ctx: GameContext): Uni[Response] =
|
||||
Uni.createFrom().item(Response.ok(FenExporter.exportGameContext(ctx)).build())
|
||||
|
||||
@POST
|
||||
@Path("/export/pgn")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array("application/x-chess-pgn"))
|
||||
@Operation(summary = "Export PGN", description = "Serialize a GameContext to PGN notation")
|
||||
@APIResponse(responseCode = "200", description = "PGN text")
|
||||
def exportPgn(ctx: GameContext): Uni[Response] =
|
||||
Uni.createFrom().item(Response.ok(PgnExporter.exportGameContext(ctx)).build())
|
||||
@@ -1,83 +0,0 @@
|
||||
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 => }
|
||||
}
|
||||
+63
-9
@@ -6,7 +6,7 @@ import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class JsonExporterSuite extends AnyFunSuite with Matchers:
|
||||
class JsonExporterTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("exportGameContext: exports initial position") {
|
||||
val context = GameContext.initial
|
||||
@@ -87,14 +87,6 @@ class JsonExporterSuite 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)
|
||||
@@ -113,3 +105,65 @@ class JsonExporterSuite 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 => }
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user