feat(game): introduce game modes and time control features
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
plugins {
|
||||
id("scala")
|
||||
id("org.scoverage") version "8.1"
|
||||
}
|
||||
|
||||
group = "de.nowchess"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val versions = rootProject.extra["VERSIONS"] as Map<String, String>
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
scala {
|
||||
scalaVersion = versions["SCALA3"]!!
|
||||
}
|
||||
|
||||
scoverage {
|
||||
scoverageVersion.set(versions["SCOVERAGE"]!!)
|
||||
}
|
||||
|
||||
tasks.withType<ScalaCompile> {
|
||||
scalaCompileOptions.additionalParameters = listOf("-encoding", "UTF-8")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
compileOnly("org.scala-lang:scala3-compiler_3") {
|
||||
version {
|
||||
strictly(versions["SCALA3"]!!)
|
||||
}
|
||||
}
|
||||
implementation("org.scala-lang:scala3-library_3") {
|
||||
version {
|
||||
strictly(versions["SCALA3"]!!)
|
||||
}
|
||||
}
|
||||
implementation("org.scala-lang:scala-library") {
|
||||
version {
|
||||
strictly(versions["SCALA_LIBRARY"]!!)
|
||||
}
|
||||
}
|
||||
|
||||
implementation(project(":modules:api"))
|
||||
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind:${versions["JACKSON"]!!}")
|
||||
implementation("com.fasterxml.jackson.core:jackson-core:${versions["JACKSON"]!!}")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-scala_3:${versions["JACKSON_SCALA"]!!}")
|
||||
|
||||
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"]!!}")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
configurations.matching { !it.name.startsWith("scoverage") }.configureEach {
|
||||
resolutionStrategy.force("org.scala-lang:scala-library:${versions["SCALA_LIBRARY"]!!}")
|
||||
}
|
||||
configurations.scoverage {
|
||||
resolutionStrategy.eachDependency {
|
||||
if (requested.group == "org.scoverage" && requested.name.startsWith("scalac-scoverage-plugin_")) {
|
||||
useTarget("${requested.group}:scalac-scoverage-plugin_2.13.16:2.3.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform {
|
||||
includeEngines("scalatest")
|
||||
testLogging {
|
||||
events("passed", "skipped", "failed")
|
||||
}
|
||||
}
|
||||
finalizedBy(tasks.reportScoverage)
|
||||
}
|
||||
tasks.reportScoverage {
|
||||
dependsOn(tasks.test)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.nowchess.json
|
||||
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.game.GameResult
|
||||
import de.nowchess.api.move.MoveType
|
||||
|
||||
class ChessJacksonModule extends SimpleModule:
|
||||
addKeySerializer(classOf[Square], new SquareKeySerializer())
|
||||
addKeyDeserializer(classOf[Square], new SquareKeyDeserializer())
|
||||
addSerializer(classOf[Square], new SquareSerializer())
|
||||
addDeserializer(classOf[Square], new SquareDeserializer())
|
||||
addSerializer(classOf[MoveType], new MoveTypeSerializer())
|
||||
addDeserializer(classOf[MoveType], new MoveTypeDeserializer())
|
||||
addSerializer(classOf[GameResult], new GameResultSerializer())
|
||||
addDeserializer(classOf[GameResult], new GameResultDeserializer())
|
||||
@@ -0,0 +1,21 @@
|
||||
package de.nowchess.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.board.Color
|
||||
import de.nowchess.api.game.{DrawReason, GameResult, WinReason}
|
||||
|
||||
class GameResultDeserializer extends JsonDeserializer[GameResult]:
|
||||
// scalafix:off DisableSyntax.throw
|
||||
override def deserialize(p: JsonParser, ctx: DeserializationContext): GameResult =
|
||||
val node = p.getCodec.readTree[ObjectNode](p)
|
||||
node.get("type").asText() match
|
||||
case "win" =>
|
||||
GameResult.Win(
|
||||
Color.valueOf(node.get("color").asText()),
|
||||
WinReason.valueOf(node.get("winReason").asText()),
|
||||
)
|
||||
case "draw" => GameResult.Draw(DrawReason.valueOf(node.get("reason").asText()))
|
||||
case t => throw new JsonParseException(p, s"Unknown game result type: $t")
|
||||
// scalafix:on DisableSyntax.throw
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.nowchess.json
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator
|
||||
import com.fasterxml.jackson.databind.{JsonSerializer, SerializerProvider}
|
||||
import de.nowchess.api.game.GameResult
|
||||
|
||||
class GameResultSerializer extends JsonSerializer[GameResult]:
|
||||
override def serialize(value: GameResult, gen: JsonGenerator, provider: SerializerProvider): Unit =
|
||||
gen.writeStartObject()
|
||||
value match
|
||||
case GameResult.Win(color, winReason) =>
|
||||
gen.writeStringField("type", "win")
|
||||
gen.writeStringField("color", color.toString)
|
||||
gen.writeStringField("winReason", winReason.toString)
|
||||
case GameResult.Draw(reason) =>
|
||||
gen.writeStringField("type", "draw")
|
||||
gen.writeStringField("reason", reason.toString)
|
||||
gen.writeEndObject()
|
||||
@@ -0,0 +1,19 @@
|
||||
package de.nowchess.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
|
||||
@@ -0,0 +1,23 @@
|
||||
package de.nowchess.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()
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.nowchess.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
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.nowchess.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
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.nowchess.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)
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.nowchess.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)
|
||||
@@ -0,0 +1,156 @@
|
||||
package de.nowchess.json
|
||||
|
||||
import com.fasterxml.jackson.core.`type`.TypeReference
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.module.scala.DefaultScalaModule
|
||||
import de.nowchess.api.board.{Color, File, Rank, Square}
|
||||
import de.nowchess.api.game.{DrawReason, GameResult, WinReason}
|
||||
import de.nowchess.api.move.{MoveType, PromotionPiece}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class ChessJacksonModuleTest extends AnyFunSuite with Matchers:
|
||||
|
||||
private val mapper: ObjectMapper =
|
||||
val m = new ObjectMapper()
|
||||
m.registerModule(DefaultScalaModule)
|
||||
m.registerModule(new ChessJacksonModule())
|
||||
m
|
||||
|
||||
private val e4 = Square(File.E, Rank.R4)
|
||||
|
||||
// ── SquareSerializer ──────────────────────────────────────────────
|
||||
|
||||
test("SquareSerializer writes square as string"):
|
||||
mapper.writeValueAsString(e4) shouldBe """"e4""""
|
||||
|
||||
// ── SquareDeserializer ────────────────────────────────────────────
|
||||
|
||||
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
|
||||
|
||||
// ── SquareKeySerializer/Deserializer ──────────────────────────────
|
||||
|
||||
test("SquareKeySerializer writes square as map field name"):
|
||||
mapper.writeValueAsString(Map(e4 -> "piece")) shouldBe """{"e4":"piece"}"""
|
||||
|
||||
// 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
|
||||
|
||||
test("Square round-trips as map key"):
|
||||
val original = Map(Square(File.D, Rank.R5) -> 99)
|
||||
val json = mapper.writeValueAsString(original)
|
||||
val result = mapper.readValue(json, new TypeReference[Map[Square, Int]] {})
|
||||
result shouldBe original
|
||||
|
||||
// ── 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"):
|
||||
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"):
|
||||
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])
|
||||
|
||||
// ── GameResultSerializer ──────────────────────────────────────────
|
||||
|
||||
test("GameResultSerializer serializes Win"):
|
||||
mapper.writeValueAsString(GameResult.Win(Color.White, WinReason.Checkmate)) shouldBe
|
||||
"""{"type":"win","color":"White","winReason":"Checkmate"}"""
|
||||
|
||||
test("GameResultSerializer serializes Win by Resignation"):
|
||||
mapper.writeValueAsString(GameResult.Win(Color.Black, WinReason.Resignation)) shouldBe
|
||||
"""{"type":"win","color":"Black","winReason":"Resignation"}"""
|
||||
|
||||
test("GameResultSerializer serializes Win by TimeControl"):
|
||||
mapper.writeValueAsString(GameResult.Win(Color.White, WinReason.TimeControl)) shouldBe
|
||||
"""{"type":"win","color":"White","winReason":"TimeControl"}"""
|
||||
|
||||
test("GameResultSerializer serializes Draw"):
|
||||
mapper.writeValueAsString(GameResult.Draw(DrawReason.Stalemate)) shouldBe
|
||||
"""{"type":"draw","reason":"Stalemate"}"""
|
||||
|
||||
test("GameResultSerializer serializes Draw InsufficientMaterial"):
|
||||
mapper.writeValueAsString(GameResult.Draw(DrawReason.InsufficientMaterial)) shouldBe
|
||||
"""{"type":"draw","reason":"InsufficientMaterial"}"""
|
||||
|
||||
test("GameResultSerializer serializes Draw FiftyMoveRule"):
|
||||
mapper.writeValueAsString(GameResult.Draw(DrawReason.FiftyMoveRule)) shouldBe
|
||||
"""{"type":"draw","reason":"FiftyMoveRule"}"""
|
||||
|
||||
test("GameResultSerializer serializes Draw ThreefoldRepetition"):
|
||||
mapper.writeValueAsString(GameResult.Draw(DrawReason.ThreefoldRepetition)) shouldBe
|
||||
"""{"type":"draw","reason":"ThreefoldRepetition"}"""
|
||||
|
||||
test("GameResultSerializer serializes Draw Agreement"):
|
||||
mapper.writeValueAsString(GameResult.Draw(DrawReason.Agreement)) shouldBe
|
||||
"""{"type":"draw","reason":"Agreement"}"""
|
||||
|
||||
// ── GameResultDeserializer ────────────────────────────────────────
|
||||
|
||||
test("GameResultDeserializer deserializes Win"):
|
||||
mapper.readValue("""{"type":"win","color":"White","winReason":"Checkmate"}""", classOf[GameResult]) shouldBe
|
||||
GameResult.Win(Color.White, WinReason.Checkmate)
|
||||
|
||||
test("GameResultDeserializer deserializes Win Black Resignation"):
|
||||
mapper.readValue("""{"type":"win","color":"Black","winReason":"Resignation"}""", classOf[GameResult]) shouldBe
|
||||
GameResult.Win(Color.Black, WinReason.Resignation)
|
||||
|
||||
test("GameResultDeserializer deserializes Draw"):
|
||||
mapper.readValue("""{"type":"draw","reason":"Stalemate"}""", classOf[GameResult]) shouldBe
|
||||
GameResult.Draw(DrawReason.Stalemate)
|
||||
|
||||
test("GameResultDeserializer deserializes Draw ThreefoldRepetition"):
|
||||
mapper.readValue("""{"type":"draw","reason":"ThreefoldRepetition"}""", classOf[GameResult]) shouldBe
|
||||
GameResult.Draw(DrawReason.ThreefoldRepetition)
|
||||
|
||||
test("GameResultDeserializer throws for unknown type"):
|
||||
an[Exception] should be thrownBy
|
||||
mapper.readValue("""{"type":"unknown"}""", classOf[GameResult])
|
||||
Reference in New Issue
Block a user