feat(game): introduce game modes and time control features
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-23 21:56:21 +02:00
parent 21d3d87543
commit 3df199afa1
100 changed files with 1676 additions and 604 deletions
+1
View File
@@ -44,6 +44,7 @@ dependencies {
}
implementation(project(":modules:api"))
implementation(project(":modules:json"))
implementation(platform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
implementation("io.quarkus:quarkus-rest")
@@ -0,0 +1,27 @@
{
"reflection": [
{ "type": "scala.Tuple1[]" },
{ "type": "scala.Tuple2[]" },
{ "type": "scala.Tuple3[]" },
{ "type": "scala.Tuple4[]" },
{ "type": "scala.Tuple5[]" },
{ "type": "scala.Tuple6[]" },
{ "type": "scala.Tuple7[]" },
{ "type": "scala.Tuple8[]" },
{ "type": "scala.Tuple9[]" },
{ "type": "scala.Tuple10[]" },
{ "type": "scala.Tuple11[]" },
{ "type": "scala.Tuple12[]" },
{ "type": "scala.Tuple13[]" },
{ "type": "scala.Tuple14[]" },
{ "type": "scala.Tuple15[]" },
{ "type": "scala.Tuple16[]" },
{ "type": "scala.Tuple17[]" },
{ "type": "scala.Tuple18[]" },
{ "type": "scala.Tuple19[]" },
{ "type": "scala.Tuple20[]" },
{ "type": "scala.Tuple21[]" },
{ "type": "scala.Tuple22[]" },
{ "type": "com.fasterxml.jackson.module.scala.introspect.PropertyDescriptor[]" }
]
}
@@ -2,11 +2,8 @@ package de.nowchess.rules.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.api.move.MoveType
import de.nowchess.rules.json.*
import de.nowchess.json.ChessJacksonModule
import io.quarkus.jackson.ObjectMapperCustomizer
import jakarta.inject.Singleton
@@ -19,11 +16,4 @@ class JacksonConfig extends ObjectMapperCustomizer:
new Version(2, 21, 1, null, "com.fasterxml.jackson.module", "jackson-module-scala")
// scalafix:on DisableSyntax.null
})
val mod = new SimpleModule()
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
mod.addSerializer(classOf[Square], new SquareSerializer())
mod.addDeserializer(classOf[Square], new SquareDeserializer())
mod.addSerializer(classOf[MoveType], new MoveTypeSerializer())
mod.addDeserializer(classOf[MoveType], new MoveTypeDeserializer())
mapper.registerModule(mod)
mapper.registerModule(new ChessJacksonModule())
@@ -1,19 +0,0 @@
package de.nowchess.rules.json
import com.fasterxml.jackson.core.{JsonParseException, JsonParser}
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.{DeserializationContext, JsonDeserializer}
import de.nowchess.api.move.{MoveType, PromotionPiece}
class MoveTypeDeserializer extends JsonDeserializer[MoveType]:
// scalafix:off DisableSyntax.throw
override def deserialize(p: JsonParser, ctx: DeserializationContext): MoveType =
val node = p.getCodec.readTree[ObjectNode](p)
node.get("type").asText() match
case "normal" => MoveType.Normal(node.get("isCapture").asBoolean(false))
case "castleKingside" => MoveType.CastleKingside
case "castleQueenside" => MoveType.CastleQueenside
case "enPassant" => MoveType.EnPassant
case "promotion" => MoveType.Promotion(PromotionPiece.valueOf(node.get("piece").asText()))
case t => throw new JsonParseException(p, s"Unknown move type: $t")
// scalafix:on DisableSyntax.throw
@@ -1,23 +0,0 @@
package de.nowchess.rules.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
import de.nowchess.api.move.MoveType
class MoveTypeSerializer extends JsonSerializer[MoveType]:
override def serialize(value: MoveType, gen: JsonGenerator, provider: SerializerProvider): Unit =
gen.writeStartObject()
value match
case MoveType.Normal(isCapture) =>
gen.writeStringField("type", "normal")
gen.writeBooleanField("isCapture", isCapture)
case MoveType.CastleKingside =>
gen.writeStringField("type", "castleKingside")
case MoveType.CastleQueenside =>
gen.writeStringField("type", "castleQueenside")
case MoveType.EnPassant =>
gen.writeStringField("type", "enPassant")
case MoveType.Promotion(piece) =>
gen.writeStringField("type", "promotion")
gen.writeStringField("piece", piece.toString)
gen.writeEndObject()
@@ -1,9 +0,0 @@
package de.nowchess.rules.json
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.{DeserializationContext, JsonDeserializer}
import de.nowchess.api.board.Square
class SquareDeserializer extends JsonDeserializer[Square]:
override def deserialize(p: JsonParser, ctx: DeserializationContext): Square =
Square.fromAlgebraic(p.getText).orNull
@@ -1,8 +0,0 @@
package de.nowchess.rules.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.rules.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,9 +0,0 @@
package de.nowchess.rules.json
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
import de.nowchess.api.board.Square
class SquareSerializer extends JsonSerializer[Square]:
override def serialize(value: Square, gen: JsonGenerator, provider: SerializerProvider): Unit =
gen.writeString(value.toString)
@@ -11,7 +11,7 @@ import org.scalatest.matchers.should.Matchers
class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
private def contextFromFen(fen: String): GameContext =
FenParser.parseFen(fen).fold(err => fail(err), identity)
FenParser.parseFen(fen).fold(err => fail(err.message), identity)
private def sq(alg: String): Square =
Square.fromAlgebraic(alg).getOrElse(fail(s"Invalid square in test: $alg"))
@@ -1,102 +1,31 @@
package de.nowchess.rules.json
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.{File, Rank, Square}
import de.nowchess.api.move.{MoveType, PromotionPiece}
import de.nowchess.api.move.MoveType
import de.nowchess.rules.config.JacksonConfig
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
class JsonSerializersTest extends AnyFunSuite with Matchers:
private val mapper: ObjectMapper =
val m = new ObjectMapper()
val mod = new SimpleModule()
m.registerModule(DefaultScalaModule)
mod.addKeySerializer(classOf[Square], new SquareKeySerializer())
mod.addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
mod.addSerializer(classOf[Square], new SquareSerializer())
mod.addDeserializer(classOf[Square], new SquareDeserializer())
mod.addSerializer(classOf[MoveType], new MoveTypeSerializer())
mod.addDeserializer(classOf[MoveType], new MoveTypeDeserializer())
m.registerModule(mod)
val m = new ObjectMapper()
new JacksonConfig().customize(m)
m
private val e4 = Square(File.E, Rank.R4)
test("customize enables Option serialization via DefaultScalaModule"):
mapper.writeValueAsString(None) shouldBe "null"
mapper.writeValueAsString(Some("hello")) shouldBe """"hello""""
// ── SquareKeySerializer ───────────────────────────────────────────
test("customize registers SquareSerializer"):
mapper.writeValueAsString(Square(File.E, Rank.R4)) shouldBe """"e4""""
test("SquareKeySerializer writes square as map field name"):
mapper.writeValueAsString(Map(e4 -> "piece")) shouldBe """{"e4":"piece"}"""
test("customize registers SquareDeserializer"):
mapper.readValue(""""e4"""", classOf[Square]) shouldBe Square(File.E, Rank.R4)
// ── SquareKeyDeserializer ─────────────────────────────────────────
// scalafix:off DisableSyntax.null
test("SquareKeyDeserializer returns square for valid key"):
new SquareKeyDeserializer().deserializeKey("e4", null) shouldBe e4
test("SquareKeyDeserializer returns null for invalid key"):
new SquareKeyDeserializer().deserializeKey("z9", null) shouldBe null
// scalafix:on DisableSyntax.null
// ── SquareSerializer/Deserializer ─────────────────────────────────
test("SquareSerializer writes square as string"):
mapper.writeValueAsString(e4) shouldBe """"e4""""
test("SquareDeserializer reads valid square string"):
mapper.readValue(""""e4"""", classOf[Square]) shouldBe e4
// scalafix:off DisableSyntax.null
test("SquareDeserializer returns null for invalid square string"):
mapper.readValue(""""z9"""", classOf[Square]) shouldBe null
// scalafix:on DisableSyntax.null
// ── MoveTypeSerializer ────────────────────────────────────────────
test("MoveTypeSerializer serializes Normal non-capture"):
mapper.writeValueAsString(MoveType.Normal(false)) shouldBe """{"type":"normal","isCapture":false}"""
test("MoveTypeSerializer serializes Normal capture"):
mapper.writeValueAsString(MoveType.Normal(true)) shouldBe """{"type":"normal","isCapture":true}"""
test("MoveTypeSerializer serializes CastleKingside"):
test("customize registers MoveTypeSerializer"):
mapper.writeValueAsString(MoveType.CastleKingside) shouldBe """{"type":"castleKingside"}"""
test("MoveTypeSerializer serializes CastleQueenside"):
mapper.writeValueAsString(MoveType.CastleQueenside) shouldBe """{"type":"castleQueenside"}"""
test("MoveTypeSerializer serializes EnPassant"):
mapper.writeValueAsString(MoveType.EnPassant) shouldBe """{"type":"enPassant"}"""
test("MoveTypeSerializer serializes Promotion"):
mapper.writeValueAsString(MoveType.Promotion(PromotionPiece.Queen)) shouldBe
"""{"type":"promotion","piece":"Queen"}"""
// ── MoveTypeDeserializer ──────────────────────────────────────────
test("MoveTypeDeserializer deserializes normal non-capture"):
mapper.readValue("""{"type":"normal","isCapture":false}""", classOf[MoveType]) shouldBe
MoveType.Normal(false)
test("MoveTypeDeserializer deserializes normal capture"):
mapper.readValue("""{"type":"normal","isCapture":true}""", classOf[MoveType]) shouldBe
MoveType.Normal(true)
test("MoveTypeDeserializer deserializes castleKingside"):
mapper.readValue("""{"type":"castleKingside"}""", classOf[MoveType]) shouldBe MoveType.CastleKingside
test("MoveTypeDeserializer deserializes castleQueenside"):
mapper.readValue("""{"type":"castleQueenside"}""", classOf[MoveType]) shouldBe MoveType.CastleQueenside
test("MoveTypeDeserializer deserializes enPassant"):
test("customize registers MoveTypeDeserializer"):
mapper.readValue("""{"type":"enPassant"}""", classOf[MoveType]) shouldBe MoveType.EnPassant
test("MoveTypeDeserializer deserializes promotion"):
mapper.readValue("""{"type":"promotion","piece":"Rook"}""", classOf[MoveType]) shouldBe
MoveType.Promotion(PromotionPiece.Rook)
test("MoveTypeDeserializer throws for unknown type"):
an[Exception] should be thrownBy
mapper.readValue("""{"type":"unknown"}""", classOf[MoveType])