feat: NCS-37 Add initial API structure and DTOs for NowChess application

This commit is contained in:
2026-04-19 21:04:40 +02:00
committed by LQ63
parent b706c65a0a
commit 3febed3cca
4 changed files with 26 additions and 25 deletions
@@ -1,6 +1,5 @@
package de.nowchess.chess.config
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import io.quarkus.jackson.ObjectMapperCustomizer
@@ -9,9 +8,4 @@ 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
})
mapper.registerModule(DefaultScalaModule)
@@ -9,5 +9,6 @@ final case class GameEntry(
engine: GameEngine,
white: PlayerInfo,
black: PlayerInfo,
drawOfferedBy: Option[Color] = None,
resigned: Boolean = false,
)
@@ -20,19 +20,10 @@ import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response}
import java.util.concurrent.atomic.AtomicReference
import scala.compiletime.uninitialized
@Path("/api/board/game")
@ApplicationScoped
class GameResource:
// scalafix:off DisableSyntax.var
@Inject
var registry: GameRegistry = uninitialized
@Inject
var objectMapper: ObjectMapper = uninitialized
// scalafix:on DisableSyntax.var
class GameResource(@Inject val registry: GameRegistry, @Inject val objectMapper: ObjectMapper):
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
private val DefaultBlack = PlayerInfo(PlayerId("p2"), "Player 2")
@@ -40,7 +31,7 @@ class GameResource:
// ── mapping ──────────────────────────────────────────────────────────────
private def statusOf(entry: GameEntry): String =
if entry.engine.pendingDrawOfferBy.isDefined then "drawOffered"
if entry.drawOfferedBy.isDefined then "drawOffered"
else
val ctx = entry.engine.context
ctx.result match
@@ -113,7 +104,7 @@ class GameResource:
val error = new AtomicReference[Option[String]](None)
val obs = new Observer:
def onGameEvent(e: GameEvent): Unit = e match
case InvalidMoveEvent(_, reason) => error.set(Some(reason.toString))
case InvalidMoveEvent(_, reason) => error.set(Some(reason))
case _ => ()
engine.subscribe(obs)
engine.processUserInput(uci)
@@ -142,7 +133,6 @@ 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
@@ -245,16 +235,26 @@ class GameResource:
assertGameNotOver(entry)
action match
case "offer" =>
entry.engine.offerDraw(entry.engine.context.turn)
registry.update(entry.copy(drawOfferedBy = Some(entry.engine.context.turn)))
ok(OkResponseDto())
case "accept" =>
entry.engine.acceptDraw(entry.engine.context.turn)
ok(OkResponseDto())
entry.drawOfferedBy match
case None =>
throw BadRequestException("NO_DRAW_OFFER", "No draw offer to accept")
case Some(offerer) if offerer == entry.engine.context.turn =>
throw BadRequestException("CANNOT_ACCEPT_OWN_OFFER", "Cannot accept your own draw offer")
case _ =>
entry.engine.applyDraw(DrawReason.Agreement)
registry.update(entry.copy(drawOfferedBy = None))
ok(OkResponseDto())
case "decline" =>
entry.engine.declineDraw(entry.engine.context.turn)
if entry.drawOfferedBy.isEmpty then throw BadRequestException("NO_DRAW_OFFER", "No draw offer to decline")
registry.update(entry.copy(drawOfferedBy = None))
ok(OkResponseDto())
case "claim" =>
entry.engine.claimDraw()
if entry.engine.context.halfMoveClock < 100 then
throw BadRequestException("CLAIM_NOT_AVAILABLE", "Fifty-move rule draw is not available")
entry.engine.applyDraw(DrawReason.FiftyMoveRule)
ok(OkResponseDto())
case _ =>
throw BadRequestException("INVALID_ACTION", s"Unknown draw action: $action", Some("action"))