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
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ScalaProjectSettings">
<option name="scala3DisclaimerShown" value="true" />
</component>
</project>
@@ -1,6 +1,5 @@
package de.nowchess.chess.config package de.nowchess.chess.config
import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.fasterxml.jackson.module.scala.DefaultScalaModule
import io.quarkus.jackson.ObjectMapperCustomizer import io.quarkus.jackson.ObjectMapperCustomizer
@@ -9,9 +8,4 @@ import jakarta.inject.Singleton
@Singleton @Singleton
class JacksonConfig extends ObjectMapperCustomizer: class JacksonConfig extends ObjectMapperCustomizer:
def customize(mapper: ObjectMapper): Unit = def customize(mapper: ObjectMapper): Unit =
mapper.registerModule(new DefaultScalaModule() { mapper.registerModule(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
})
@@ -9,5 +9,6 @@ final case class GameEntry(
engine: GameEngine, engine: GameEngine,
white: PlayerInfo, white: PlayerInfo,
black: PlayerInfo, black: PlayerInfo,
drawOfferedBy: Option[Color] = None,
resigned: Boolean = false, resigned: Boolean = false,
) )
@@ -20,19 +20,10 @@ import jakarta.ws.rs.*
import jakarta.ws.rs.core.{MediaType, Response} import jakarta.ws.rs.core.{MediaType, Response}
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
import scala.compiletime.uninitialized
@Path("/api/board/game") @Path("/api/board/game")
@ApplicationScoped @ApplicationScoped
class GameResource: class GameResource(@Inject val registry: GameRegistry, @Inject val objectMapper: ObjectMapper):
// scalafix:off DisableSyntax.var
@Inject
var registry: GameRegistry = uninitialized
@Inject
var objectMapper: ObjectMapper = uninitialized
// scalafix:on DisableSyntax.var
private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1") private val DefaultWhite = PlayerInfo(PlayerId("p1"), "Player 1")
private val DefaultBlack = PlayerInfo(PlayerId("p2"), "Player 2") private val DefaultBlack = PlayerInfo(PlayerId("p2"), "Player 2")
@@ -40,7 +31,7 @@ class GameResource:
// ── mapping ────────────────────────────────────────────────────────────── // ── mapping ──────────────────────────────────────────────────────────────
private def statusOf(entry: GameEntry): String = private def statusOf(entry: GameEntry): String =
if entry.engine.pendingDrawOfferBy.isDefined then "drawOffered" if entry.drawOfferedBy.isDefined then "drawOffered"
else else
val ctx = entry.engine.context val ctx = entry.engine.context
ctx.result match ctx.result match
@@ -113,7 +104,7 @@ class GameResource:
val error = new AtomicReference[Option[String]](None) val error = new AtomicReference[Option[String]](None)
val obs = new Observer: val obs = new Observer:
def onGameEvent(e: GameEvent): Unit = e match def onGameEvent(e: GameEvent): Unit = e match
case InvalidMoveEvent(_, reason) => error.set(Some(reason.toString)) case InvalidMoveEvent(_, reason) => error.set(Some(reason))
case _ => () case _ => ()
engine.subscribe(obs) engine.subscribe(obs)
engine.processUserInput(uci) engine.processUserInput(uci)
@@ -142,7 +133,6 @@ class GameResource:
val black = playerInfoFrom(req.black, DefaultBlack) val black = playerInfoFrom(req.black, DefaultBlack)
val entry = newEntry(GameContext.initial, white, black) val entry = newEntry(GameContext.initial, white, black)
registry.store(entry) registry.store(entry)
println(s"Created game ${entry.gameId}")
created(toGameFullDto(entry)) created(toGameFullDto(entry))
@GET @GET
@@ -245,16 +235,26 @@ class GameResource:
assertGameNotOver(entry) assertGameNotOver(entry)
action match action match
case "offer" => case "offer" =>
entry.engine.offerDraw(entry.engine.context.turn) registry.update(entry.copy(drawOfferedBy = Some(entry.engine.context.turn)))
ok(OkResponseDto()) ok(OkResponseDto())
case "accept" => case "accept" =>
entry.engine.acceptDraw(entry.engine.context.turn) entry.drawOfferedBy match
ok(OkResponseDto()) 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" => 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()) ok(OkResponseDto())
case "claim" => 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()) ok(OkResponseDto())
case _ => case _ =>
throw BadRequestException("INVALID_ACTION", s"Unknown draw action: $action", Some("action")) throw BadRequestException("INVALID_ACTION", s"Unknown draw action: $action", Some("action"))