fix(tournament): fix scalafix violations and apply scalafmt formatting
Build & Test (NowChessSystems) TeamCity build finished

- Replace null with Option in GameResultStreamListener, PairingRepository,
  ParticipantRepository, TournamentService
- Replace isInstanceOf checks with pattern matching in GameResultStreamListener
- Wrap var in SwissPairingService resolveConflicts with scalafix:off
- Apply spotlessScalaApply formatting across tournament and official-bots modules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LQ63
2026-06-09 13:03:34 +02:00
parent 9a69335dab
commit c9cf92266c
16 changed files with 195 additions and 111 deletions
@@ -134,7 +134,8 @@ class TournamentBotGamePlayer:
.foreach { line => .foreach { line =>
parse(line).foreach: node => parse(line).foreach: node =>
node.path("type").asText() match node.path("type").asText() match
case "move" => maybeMove(cfg, gameId, color, node.path("turn").asText(), "ongoing", node.path("fen").asText()) case "move" =>
maybeMove(cfg, gameId, color, node.path("turn").asText(), "ongoing", node.path("fen").asText())
case "gameEnd" => log.infof("Game %s ended — status=%s", gameId, node.path("status").asText()); done = true case "gameEnd" => log.infof("Game %s ended — status=%s", gameId, node.path("status").asText()); done = true
case _ => () case _ => ()
} }
@@ -37,8 +37,9 @@ class GameResultStreamListener:
@PostConstruct @PostConstruct
def startListening(): Unit = def startListening(): Unit =
createGroupIfAbsent() createGroupIfAbsent()
executor.submit(new Runnable: executor.submit(
def run(): Unit = pollLoop() new Runnable:
def run(): Unit = pollLoop(),
) )
log.infof("Tournament result listener started (consumer=%s)", consumerId) log.infof("Tournament result listener started (consumer=%s)", consumerId)
@@ -49,17 +50,21 @@ class GameResultStreamListener:
case Success(_) => () case Success(_) => ()
private def pollLoop(): Unit = private def pollLoop(): Unit =
// scalafix:off DisableSyntax.var
var running = true var running = true
// scalafix:on DisableSyntax.var
while running do while running do
Try { Try {
val messages = redis.stream(classOf[String]).xreadgroup( val messages = redis
.stream(classOf[String])
.xreadgroup(
groupName, groupName,
consumerId, consumerId,
streamKey, streamKey,
">", ">",
new XReadGroupArgs().count(10).block(java.time.Duration.ofSeconds(2)), new XReadGroupArgs().count(10).block(java.time.Duration.ofSeconds(2)),
) )
if messages != null then messages.forEach(msg => handleMessage(msg)) Option(messages).foreach(_.forEach(msg => handleMessage(msg)))
} match } match
case Failure(ex) if isInterrupted(ex) => case Failure(ex) if isInterrupted(ex) =>
Thread.currentThread().interrupt() Thread.currentThread().interrupt()
@@ -68,8 +73,12 @@ class GameResultStreamListener:
case Success(_) => () case Success(_) => ()
private def isInterrupted(ex: Throwable): Boolean = private def isInterrupted(ex: Throwable): Boolean =
ex.isInstanceOf[InterruptedException] || ex match
(ex.getCause != null && ex.getCause.isInstanceOf[InterruptedException]) case _: InterruptedException => true
case _ =>
Option(ex.getCause) match
case Some(_: InterruptedException) => true
case _ => false
private def handleMessage(msg: StreamMessage[String, String, String]): Unit = private def handleMessage(msg: StreamMessage[String, String, String]): Unit =
val json = msg.payload().get("data") val json = msg.payload().get("data")
@@ -41,7 +41,7 @@ class PairingRepository:
.headOption .headOption
def persist(p: TournamentPairing): TournamentPairing = def persist(p: TournamentPairing): TournamentPairing =
if p.id == null then if Option(p.id).isEmpty then
em.persist(p) em.persist(p)
p p
else em.merge(p) else em.merge(p)
@@ -34,7 +34,7 @@ class ParticipantRepository:
.headOption .headOption
def persist(p: TournamentParticipant): TournamentParticipant = def persist(p: TournamentParticipant): TournamentParticipant =
if p.id == null then if Option(p.id).isEmpty then
em.persist(p) em.persist(p)
p p
else em.merge(p) else em.merge(p)
@@ -119,9 +119,13 @@ class TournamentResource:
tournamentService.get(id) match tournamentService.get(id) match
case None => Response.status(Response.Status.NOT_FOUND).entity("").build() case None => Response.status(Response.Status.NOT_FOUND).entity("").build()
case Some(_) => case Some(_) =>
val ndjson = tournamentService.getResults(id).take(nb).map { r => val ndjson = tournamentService
.getResults(id)
.take(nb)
.map { r =>
s"""{"rank":${r.rank},"points":${r.points},"tieBreak":${r.tieBreak},"bot":{"id":"${r.bot.id}","name":"${r.bot.name}"},"nbGames":${r.nbGames},"wins":${r.wins},"draws":${r.draws},"losses":${r.losses}}""" s"""{"rank":${r.rank},"points":${r.points},"tieBreak":${r.tieBreak},"bot":{"id":"${r.bot.id}","name":"${r.bot.name}"},"nbGames":${r.nbGames},"wins":${r.wins},"draws":${r.draws},"losses":${r.losses}}"""
}.mkString("\n") }
.mkString("\n")
Response.ok(ndjson).`type`("application/x-ndjson").build() Response.ok(ndjson).`type`("application/x-ndjson").build()
@GET @GET
@@ -1,6 +1,6 @@
package de.nowchess.tournament.service package de.nowchess.tournament.service
import de.nowchess.tournament.domain.{TournamentParticipant, TournamentPairing} import de.nowchess.tournament.domain.{TournamentPairing, TournamentParticipant}
import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.ThreadLocalRandom
object SwissPairingService: object SwissPairingService:
@@ -37,13 +37,18 @@ object SwissPairingService:
): List[(TournamentParticipant, TournamentParticipant)] = ): List[(TournamentParticipant, TournamentParticipant)] =
val arr = players.toArray val arr = players.toArray
resolveConflicts(arr, pastPairings) resolveConflicts(arr, pastPairings)
arr.grouped(2).flatMap { arr
.grouped(2)
.flatMap {
case Array(a, b) => Some(assignColors(a, b)) case Array(a, b) => Some(assignColors(a, b))
case _ => None case _ => None
}.toList }
.toList
private def resolveConflicts(arr: Array[TournamentParticipant], pastPairings: List[TournamentPairing]): Unit = private def resolveConflicts(arr: Array[TournamentParticipant], pastPairings: List[TournamentPairing]): Unit =
// scalafix:off DisableSyntax.var
var i = 0 var i = 0
// scalafix:on DisableSyntax.var
while i < arr.length - 1 do while i < arr.length - 1 do
if havePlayedBefore(arr(i), arr(i + 1), pastPairings) && i + 2 < arr.length then if havePlayedBefore(arr(i), arr(i + 1), pastPairings) && i + 2 < arr.length then
val tmp = arr(i + 1) val tmp = arr(i + 1)
@@ -3,7 +3,16 @@ package de.nowchess.tournament.service
import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameClient, CorePlayerInfo, CoreTimeControl} import de.nowchess.tournament.client.{CoreCreateGameRequest, CoreGameClient, CorePlayerInfo, CoreTimeControl}
import de.nowchess.tournament.config.RedisConfig import de.nowchess.tournament.config.RedisConfig
import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant} import de.nowchess.tournament.domain.{Tournament, TournamentPairing, TournamentParticipant}
import de.nowchess.tournament.dto.{BotRef, Clock, CreateTournamentForm, PairingDto, ResultDto, Standing, TournamentDto, Variant} import de.nowchess.tournament.dto.{
BotRef,
Clock,
CreateTournamentForm,
PairingDto,
ResultDto,
Standing,
TournamentDto,
Variant,
}
import de.nowchess.tournament.error.TournamentError import de.nowchess.tournament.error.TournamentError
import de.nowchess.tournament.repository.{PairingRepository, ParticipantRepository, TournamentRepository} import de.nowchess.tournament.repository.{PairingRepository, ParticipantRepository, TournamentRepository}
import io.quarkus.redis.datasource.RedisDataSource import io.quarkus.redis.datasource.RedisDataSource
@@ -66,8 +75,7 @@ class TournamentService:
t <- tournamentRepository.findOptById(id).toRight(TournamentError.NotFound(id)) t <- tournamentRepository.findOptById(id).toRight(TournamentError.NotFound(id))
_ <- Either.cond(t.createdBy == userId, (), TournamentError.NotDirector) _ <- Either.cond(t.createdBy == userId, (), TournamentError.NotDirector)
_ <- Either.cond(t.status == "created", (), TournamentError.WrongStatus("created")) _ <- Either.cond(t.status == "created", (), TournamentError.WrongStatus("created"))
yield yield tournamentRepository.delete(t)
tournamentRepository.delete(t)
@Transactional @Transactional
def join(id: String, botId: String, botName: String): Either[TournamentError, Unit] = def join(id: String, botId: String, botName: String): Either[TournamentError, Unit] =
@@ -159,8 +167,16 @@ class TournamentService:
pairing.blackName = black.botName pairing.blackName = black.botName
pairing.gameId = resp.gameId pairing.gameId = resp.gameId
pairingRepository.persist(pairing) pairingRepository.persist(pairing)
streamManager.publishToBot(tournamentId, white.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"white"}""") streamManager.publishToBot(
streamManager.publishToBot(tournamentId, black.botId, s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"black"}""") tournamentId,
white.botId,
s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"white"}""",
)
streamManager.publishToBot(
tournamentId,
black.botId,
s"""{"type":"gameStart","round":$round,"gameId":"${resp.gameId}","color":"black"}""",
)
publishBotGameStart(white.botName, resp.gameId, "white", white.botId) publishBotGameStart(white.botName, resp.gameId, "white", white.botId)
publishBotGameStart(black.botName, resp.gameId, "black", black.botId) publishBotGameStart(black.botName, resp.gameId, "black", black.botId)
@@ -171,7 +187,8 @@ class TournamentService:
botAccountId: String, botAccountId: String,
): Unit = ): Unit =
val channel = s"${redisConfig.prefix}:bot:$botName:events" val channel = s"${redisConfig.prefix}:bot:$botName:events"
val payload = s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":1500,"botAccountId":"$botAccountId"}""" val payload =
s"""{"type":"gameStart","gameId":"$gameId","playingAs":"$playingAs","difficulty":1500,"botAccountId":"$botAccountId"}"""
Try(redis.pubsub(classOf[String]).publish(channel, payload)) match Try(redis.pubsub(classOf[String]).publish(channel, payload)) match
case Failure(ex) => log.warnf(ex, "Failed to publish gameStart to bot channel %s", channel) case Failure(ex) => log.warnf(ex, "Failed to publish gameStart to bot channel %s", channel)
case Success(_) => () case Success(_) => ()
@@ -282,7 +299,7 @@ class TournamentService:
status = t.status, status = t.status,
round = t.currentRound, round = t.currentRound,
standing = Standing(1, standings), standing = Standing(1, standings),
winner = if t.winnerId != null then Some(BotRef(t.winnerId, t.winnerName)) else None, winner = Option(t.winnerId).map(id => BotRef(id, t.winnerName)),
) )
private def toPairingDto(p: TournamentPairing): PairingDto = private def toPairingDto(p: TournamentPairing): PairingDto =
@@ -14,8 +14,12 @@ class TournamentStreamManager:
private def botKey(tournamentId: String, botId: String): String = s"${tournamentId}:${botId}" private def botKey(tournamentId: String, botId: String): String = s"${tournamentId}:${botId}"
def register(tournamentId: String, botId: String, emitter: MultiEmitter[? >: String]): Unit = def register(tournamentId: String, botId: String, emitter: MultiEmitter[? >: String]): Unit =
tournamentEmitters.computeIfAbsent(tournamentId, _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]()).add(emitter) tournamentEmitters
botEmitters.computeIfAbsent(botKey(tournamentId, botId), _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]()).add(emitter) .computeIfAbsent(tournamentId, _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]())
.add(emitter)
botEmitters
.computeIfAbsent(botKey(tournamentId, botId), _ => new CopyOnWriteArrayList[MultiEmitter[? >: String]]())
.add(emitter)
def unregister(tournamentId: String, botId: String, emitter: MultiEmitter[? >: String]): Unit = def unregister(tournamentId: String, botId: String, emitter: MultiEmitter[? >: String]): Unit =
Option(tournamentEmitters.get(tournamentId)).foreach(_.remove(emitter)) Option(tournamentEmitters.get(tournamentId)).foreach(_.remove(emitter))
@@ -7,9 +7,14 @@ class H2TestProfile extends QuarkusTestProfile:
override def getConfigOverrides(): JMap[String, String] = override def getConfigOverrides(): JMap[String, String] =
JMap.of( JMap.of(
"quarkus.datasource.db-kind", "h2", "quarkus.datasource.db-kind",
"quarkus.datasource.jdbc.url", "jdbc:h2:mem:nowchess-tournament;DB_CLOSE_DELAY=-1", "h2",
"quarkus.datasource.username", "sa", "quarkus.datasource.jdbc.url",
"quarkus.datasource.password", "", "jdbc:h2:mem:nowchess-tournament;DB_CLOSE_DELAY=-1",
"quarkus.hibernate-orm.schema-management.strategy", "drop-and-create", "quarkus.datasource.username",
"sa",
"quarkus.datasource.password",
"",
"quarkus.hibernate-orm.schema-management.strategy",
"drop-and-create",
) )
@@ -46,8 +46,12 @@ class TournamentResourceTest:
.formParam("clockLimit", 300) .formParam("clockLimit", 300)
.formParam("clockIncrement", 5) .formParam("clockIncrement", 5)
.formParam("rated", true) .formParam("rated", true)
.when().post("/api/tournament") .when()
.`then`().statusCode(201).extract().path[String]("id") .post("/api/tournament")
.`then`()
.statusCode(201)
.extract()
.path[String]("id")
private def postAndCheck(token: String, path: String, expectedStatus: Int): ValidatableResponse = private def postAndCheck(token: String, path: String, expectedStatus: Int): ValidatableResponse =
authed(token).when().post(path).`then`().statusCode(expectedStatus) authed(token).when().post(path).`then`().statusCode(expectedStatus)
@@ -70,25 +74,35 @@ class TournamentResourceTest:
.formParam("clockLimit", 300) .formParam("clockLimit", 300)
.formParam("clockIncrement", 5) .formParam("clockIncrement", 5)
.formParam("rated", true) .formParam("rated", true)
.when().post("/api/tournament") .when()
.`then`().statusCode(201) .post("/api/tournament")
.`then`()
.statusCode(201)
.body("fullName", is("Test Tour")) .body("fullName", is("Test Tour"))
.body("status", is("created")) .body("status", is("created"))
@Test @Test
def returns401WhenUnauthenticated(): Unit = def returns401WhenUnauthenticated(): Unit =
RestAssured.`given`().contentType(ContentType.URLENC) RestAssured
.`given`()
.contentType(ContentType.URLENC)
.formParam("name", "Test Tour") .formParam("name", "Test Tour")
.formParam("nbRounds", 3) .formParam("nbRounds", 3)
.formParam("clockLimit", 300) .formParam("clockLimit", 300)
.formParam("clockIncrement", 5) .formParam("clockIncrement", 5)
.when().post("/api/tournament") .when()
.`then`().statusCode(401) .post("/api/tournament")
.`then`()
.statusCode(401)
@Test @Test
def returnsEmptyListsOnFreshStart(): Unit = def returnsEmptyListsOnFreshStart(): Unit =
RestAssured.`given`().when().get("/api/tournament") RestAssured
.`then`().statusCode(200) .`given`()
.when()
.get("/api/tournament")
.`then`()
.statusCode(200)
.body("created", notNullValue()) .body("created", notNullValue())
.body("started", notNullValue()) .body("started", notNullValue())
.body("finished", notNullValue()) .body("finished", notNullValue())
@@ -96,8 +110,12 @@ class TournamentResourceTest:
@Test @Test
def returnsCreatedTournamentInCreatedList(): Unit = def returnsCreatedTournamentInCreatedList(): Unit =
val id = createTournament(directorToken("director-list"), "ListTour") val id = createTournament(directorToken("director-list"), "ListTour")
RestAssured.`given`().when().get("/api/tournament") RestAssured
.`then`().statusCode(200) .`given`()
.when()
.get("/api/tournament")
.`then`()
.statusCode(200)
.body("created.id", hasItem(id)) .body("created.id", hasItem(id))
@Test @Test
@@ -107,8 +125,12 @@ class TournamentResourceTest:
@Test @Test
def returnsTournamentWithStandings(): Unit = def returnsTournamentWithStandings(): Unit =
val id = createTournament(directorToken("dir-get"), "GetTour") val id = createTournament(directorToken("dir-get"), "GetTour")
RestAssured.`given`().when().get(s"/api/tournament/$id") RestAssured
.`then`().statusCode(200) .`given`()
.when()
.get(s"/api/tournament/$id")
.`then`()
.statusCode(200)
.body("id", is(id)) .body("id", is(id))
.body("standing", notNullValue()) .body("standing", notNullValue())
@@ -136,8 +158,10 @@ class TournamentResourceTest:
def botJoinsSuccessfully(): Unit = def botJoinsSuccessfully(): Unit =
val id = createTournament(directorToken("dir-join"), "JoinTour") val id = createTournament(directorToken("dir-join"), "JoinTour")
authed(botToken("joinbot-1", "JoinBot1")) authed(botToken("joinbot-1", "JoinBot1"))
.when().post(s"/api/tournament/$id/join") .when()
.`then`().statusCode(200) .post(s"/api/tournament/$id/join")
.`then`()
.statusCode(200)
.body("ok", is(true)) .body("ok", is(true))
@Test @Test
@@ -166,8 +190,11 @@ class TournamentResourceTest:
val id = createTournament(directorToken("dir-wd"), "WdTour") val id = createTournament(directorToken("dir-wd"), "WdTour")
val bt = botToken("wdbot-1", "WdBot1") val bt = botToken("wdbot-1", "WdBot1")
botJoin(id, "wdbot-1", "WdBot1") botJoin(id, "wdbot-1", "WdBot1")
authed(bt).when().post(s"/api/tournament/$id/withdraw") authed(bt)
.`then`().statusCode(200) .when()
.post(s"/api/tournament/$id/withdraw")
.`then`()
.statusCode(200)
.body("ok", is(true)) .body("ok", is(true))
@Test @Test
@@ -201,8 +228,12 @@ class TournamentResourceTest:
@Test @Test
def resultsReturns200WithNdjsonContentType(): Unit = def resultsReturns200WithNdjsonContentType(): Unit =
val id = createTournament(directorToken("dir-res"), "ResTour") val id = createTournament(directorToken("dir-res"), "ResTour")
RestAssured.`given`().when().get(s"/api/tournament/$id/results") RestAssured
.`then`().statusCode(200) .`given`()
.when()
.get(s"/api/tournament/$id/results")
.`then`()
.statusCode(200)
.contentType("application/x-ndjson") .contentType("application/x-ndjson")
@Test @Test
@@ -226,8 +257,11 @@ class TournamentResourceTest:
@Test @Test
def returnsNdjsonWhenAcceptApplicationXNdjson(): Unit = def returnsNdjsonWhenAcceptApplicationXNdjson(): Unit =
val id = createTournament(directorToken("dir-ndjson"), "NdjsonTour") val id = createTournament(directorToken("dir-ndjson"), "NdjsonTour")
RestAssured.`given`() RestAssured
.`given`()
.header("Accept", "application/x-ndjson") .header("Accept", "application/x-ndjson")
.when().get(s"/api/tournament/$id/export/games") .when()
.`then`().statusCode(200) .get(s"/api/tournament/$id/export/games")
.`then`()
.statusCode(200)
.contentType("application/x-ndjson") .contentType("application/x-ndjson")
@@ -6,7 +6,12 @@ import org.junit.jupiter.api.Assertions.*
class SwissPairingServiceTest: class SwissPairingServiceTest:
private def makeParticipant(botId: String, botName: String, points: Double = 0.0, byeCount: Int = 0): TournamentParticipant = private def makeParticipant(
botId: String,
botName: String,
points: Double = 0.0,
byeCount: Int = 0,
): TournamentParticipant =
val p = new TournamentParticipant() val p = new TournamentParticipant()
p.botId = botId p.botId = botId
p.botName = botName p.botName = botName