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
@@ -20,8 +20,8 @@ object TournamentBotConfig:
tournamentId <- env.get("TOURNAMENT_ID").filter(_.nonEmpty) tournamentId <- env.get("TOURNAMENT_ID").filter(_.nonEmpty)
token <- env.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty) token <- env.get("TOURNAMENT_BOT_TOKEN").filter(_.nonEmpty)
botId <- jwtSubject(token) botId <- jwtSubject(token)
serverUrl = env.getOrElse("TOURNAMENT_SERVER_URL", "http://localhost:8089") serverUrl = env.getOrElse("TOURNAMENT_SERVER_URL", "http://localhost:8089")
difficulty = env.getOrElse("TOURNAMENT_BOT_DIFFICULTY", "medium") difficulty = env.getOrElse("TOURNAMENT_BOT_DIFFICULTY", "medium")
yield TournamentBotConfig(serverUrl, tournamentId, token, botId, difficulty) yield TournamentBotConfig(serverUrl, tournamentId, token, botId, difficulty)
def jwtSubject(token: String): Option[String] = def jwtSubject(token: String): Option[String] =
@@ -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 _ => ()
} }
@@ -14,10 +14,10 @@ class Tournament:
@Column(nullable = false) @Column(nullable = false)
var fullName: String = uninitialized var fullName: String = uninitialized
var nbRounds: Int = 0 var nbRounds: Int = 0
var clockLimit: Int = 0 var clockLimit: Int = 0
var clockIncrement: Int = 0 var clockIncrement: Int = 0
var rated: Boolean = true var rated: Boolean = true
@Column(nullable = false) @Column(nullable = false)
var status: String = "created" var status: String = "created"
@@ -27,7 +27,7 @@ class Tournament:
@Column(nullable = false) @Column(nullable = false)
var createdBy: String = uninitialized var createdBy: String = uninitialized
var startsAt: Instant = uninitialized var startsAt: Instant = uninitialized
var winnerId: String = uninitialized var winnerId: String = uninitialized
var winnerName: String = uninitialized var winnerName: String = uninitialized
// scalafix:on // scalafix:on
@@ -15,8 +15,8 @@ class TournamentPairing:
@Column(nullable = false) @Column(nullable = false)
var tournamentId: String = uninitialized var tournamentId: String = uninitialized
var round: Int = 0 var round: Int = 0
var whiteId: String = uninitialized var whiteId: String = uninitialized
var whiteName: String = uninitialized var whiteName: String = uninitialized
@Column(nullable = false) @Column(nullable = false)
@@ -25,7 +25,7 @@ class TournamentPairing:
@Column(nullable = false) @Column(nullable = false)
var blackName: String = uninitialized var blackName: String = uninitialized
var gameId: String = uninitialized var gameId: String = uninitialized
var winner: String = uninitialized var winner: String = uninitialized
var moveList: String = uninitialized var moveList: String = uninitialized
// scalafix:on // scalafix:on
@@ -21,11 +21,11 @@ class TournamentParticipant:
@Column(nullable = false) @Column(nullable = false)
var botName: String = uninitialized var botName: String = uninitialized
var points: Double = 0.0 var points: Double = 0.0
var tieBreak: Double = 0.0 var tieBreak: Double = 0.0
var nbGames: Int = 0 var nbGames: Int = 0
var wins: Int = 0 var wins: Int = 0
var draws: Int = 0 var draws: Int = 0
var losses: Int = 0 var losses: Int = 0
var byeCount: Int = 0 var byeCount: Int = 0
// scalafix:on // scalafix:on
@@ -1,10 +1,10 @@
package de.nowchess.tournament.error package de.nowchess.tournament.error
enum TournamentError(val message: String): enum TournamentError(val message: String):
case NotFound(id: String) extends TournamentError(s"Tournament $id not found") case NotFound(id: String) extends TournamentError(s"Tournament $id not found")
case NotDirector extends TournamentError("Not the tournament director") case NotDirector extends TournamentError("Not the tournament director")
case WrongStatus(expected: String) extends TournamentError(s"Tournament must be in $expected status") case WrongStatus(expected: String) extends TournamentError(s"Tournament must be in $expected status")
case AlreadyJoined extends TournamentError("Already joined this tournament") case AlreadyJoined extends TournamentError("Already joined this tournament")
case NotJoined extends TournamentError("Not joined this tournament") case NotJoined extends TournamentError("Not joined this tournament")
case NotEnoughParticipants extends TournamentError("Need at least 2 participants to start") case NotEnoughParticipants extends TournamentError("Need at least 2 participants to start")
case NotABot extends TournamentError("Only bot accounts can join tournaments") case NotABot extends TournamentError("Only bot accounts can join tournaments")
@@ -21,11 +21,11 @@ import java.util.UUID
@ApplicationScoped @ApplicationScoped
class GameResultStreamListener: class GameResultStreamListener:
// scalafix:off DisableSyntax.var // scalafix:off DisableSyntax.var
@Inject var redis: RedisDataSource = uninitialized @Inject var redis: RedisDataSource = uninitialized
@Inject var objectMapper: ObjectMapper = uninitialized @Inject var objectMapper: ObjectMapper = uninitialized
@Inject var tournamentService: TournamentService = uninitialized @Inject var tournamentService: TournamentService = uninitialized
@Inject var executor: ManagedExecutor = uninitialized @Inject var executor: ManagedExecutor = uninitialized
@Inject var redisConfig: RedisConfig = uninitialized @Inject var redisConfig: RedisConfig = uninitialized
// scalafix:on // scalafix:on
private val log = Logger.getLogger(classOf[GameResultStreamListener]) private val log = Logger.getLogger(classOf[GameResultStreamListener])
@@ -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
groupName, .stream(classOf[String])
consumerId, .xreadgroup(
streamKey, groupName,
">", consumerId,
new XReadGroupArgs().count(10).block(java.time.Duration.ofSeconds(2)), streamKey,
) ">",
if messages != null then messages.forEach(msg => handleMessage(msg)) new XReadGroupArgs().count(10).block(java.time.Duration.ofSeconds(2)),
)
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
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}}""" .getResults(id)
}.mkString("\n") .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}}"""
}
.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:
@@ -9,9 +9,9 @@ object SwissPairingService:
participants: List[TournamentParticipant], participants: List[TournamentParticipant],
pastPairings: List[TournamentPairing], pastPairings: List[TournamentPairing],
): (List[(TournamentParticipant, TournamentParticipant)], Option[TournamentParticipant]) = ): (List[(TournamentParticipant, TournamentParticipant)], Option[TournamentParticipant]) =
val sorted = sortParticipants(participants) val sorted = sortParticipants(participants)
val (remaining, byeOpt) = extractByePlayer(sorted) val (remaining, byeOpt) = extractByePlayer(sorted)
val pairs = buildPairs(remaining, pastPairings) val pairs = buildPairs(remaining, pastPairings)
(pairs, byeOpt) (pairs, byeOpt)
private def sortParticipants(participants: List[TournamentParticipant]): List[TournamentParticipant] = private def sortParticipants(participants: List[TournamentParticipant]): List[TournamentParticipant] =
@@ -37,16 +37,21 @@ 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
case Array(a, b) => Some(assignColors(a, b)) .grouped(2)
case _ => None .flatMap {
}.toList case Array(a, b) => Some(assignColors(a, b))
case _ => None
}
.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)
arr(i + 1) = arr(i + 2) arr(i + 1) = arr(i + 2)
arr(i + 2) = tmp arr(i + 2) = tmp
i += 2 i += 2
@@ -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] =
@@ -115,7 +123,7 @@ class TournamentService:
Either.cond(ps.size >= 2, ps, TournamentError.NotEnoughParticipants) Either.cond(ps.size >= 2, ps, TournamentError.NotEnoughParticipants)
private def startRound(t: Tournament, round: Int, participants: List[TournamentParticipant]): Unit = private def startRound(t: Tournament, round: Int, participants: List[TournamentParticipant]): Unit =
val pastPairings = pairingRepository.findByTournamentId(t.id) val pastPairings = pairingRepository.findByTournamentId(t.id)
val (pairs, byeOpt) = SwissPairingService.computePairings(participants, pastPairings) val (pairs, byeOpt) = SwissPairingService.computePairings(participants, pastPairings)
byeOpt.foreach(bye => createByePairing(t.id, round, bye)) byeOpt.foreach(bye => createByePairing(t.id, round, bye))
pairs.foreach { case (white, black) => createRealPairing(t.id, round, white, black, t) } pairs.foreach { case (white, black) => createRealPairing(t.id, round, white, black, t) }
@@ -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(_) => ()
@@ -212,7 +229,7 @@ class TournamentService:
private def checkRoundCompletion(tournamentId: String): Unit = private def checkRoundCompletion(tournamentId: String): Unit =
tournamentRepository.findOptById(tournamentId).foreach { t => tournamentRepository.findOptById(tournamentId).foreach { t =>
val roundPairings = pairingRepository.findByTournamentIdAndRound(tournamentId, t.currentRound) val roundPairings = pairingRepository.findByTournamentIdAndRound(tournamentId, t.currentRound)
val allDone = roundPairings.nonEmpty && roundPairings.forall(p => Option(p.winner).isDefined) val allDone = roundPairings.nonEmpty && roundPairings.forall(p => Option(p.winner).isDefined)
if allDone then onRoundComplete(t) if allDone then onRoundComplete(t)
} }
@@ -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
@@ -16,21 +21,21 @@ class SwissPairingServiceTest:
@Test @Test
def pairs2PlayersRandomlyAssignsColors(): Unit = def pairs2PlayersRandomlyAssignsColors(): Unit =
val p1 = makeParticipant("b1", "BotOne") val p1 = makeParticipant("b1", "BotOne")
val p2 = makeParticipant("b2", "BotTwo") val p2 = makeParticipant("b2", "BotTwo")
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2), Nil) val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2), Nil)
assertEquals(1, pairs.size) assertEquals(1, pairs.size)
assertTrue(bye.isEmpty) assertTrue(bye.isEmpty)
val (white, black) = pairs.head val (white, black) = pairs.head
val ids = Set(white.botId, black.botId) val ids = Set(white.botId, black.botId)
assertEquals(Set("b1", "b2"), ids) assertEquals(Set("b1", "b2"), ids)
@Test @Test
def pairs4PlayersTopVsEachOther(): Unit = def pairs4PlayersTopVsEachOther(): Unit =
val p1 = makeParticipant("b1", "A", points = 2.0) val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.5) val p2 = makeParticipant("b2", "B", points = 1.5)
val p3 = makeParticipant("b3", "C", points = 1.0) val p3 = makeParticipant("b3", "C", points = 1.0)
val p4 = makeParticipant("b4", "D", points = 0.0) val p4 = makeParticipant("b4", "D", points = 0.0)
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3, p4), Nil) val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3, p4), Nil)
assertEquals(2, pairs.size) assertEquals(2, pairs.size)
assertTrue(bye.isEmpty) assertTrue(bye.isEmpty)
@@ -41,9 +46,9 @@ class SwissPairingServiceTest:
@Test @Test
def oddCountLowestRankedGetsBye(): Unit = def oddCountLowestRankedGetsBye(): Unit =
val p1 = makeParticipant("b1", "A", points = 2.0) val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.0) val p2 = makeParticipant("b2", "B", points = 1.0)
val p3 = makeParticipant("b3", "C", points = 0.0) val p3 = makeParticipant("b3", "C", points = 0.0)
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3), Nil) val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3), Nil)
assertEquals(1, pairs.size) assertEquals(1, pairs.size)
assertTrue(bye.isDefined) assertTrue(bye.isDefined)
@@ -51,22 +56,22 @@ class SwissPairingServiceTest:
@Test @Test
def avoidsRematchSwapsWhenPairAlreadyPlayed(): Unit = def avoidsRematchSwapsWhenPairAlreadyPlayed(): Unit =
val p1 = makeParticipant("b1", "A", points = 2.0) val p1 = makeParticipant("b1", "A", points = 2.0)
val p2 = makeParticipant("b2", "B", points = 1.5) val p2 = makeParticipant("b2", "B", points = 1.5)
val p3 = makeParticipant("b3", "C", points = 1.5) val p3 = makeParticipant("b3", "C", points = 1.5)
val p4 = makeParticipant("b4", "D", points = 0.0) val p4 = makeParticipant("b4", "D", points = 0.0)
val pastPairing = new TournamentPairing() val pastPairing = new TournamentPairing()
pastPairing.whiteId = "b1" pastPairing.whiteId = "b1"
pastPairing.blackId = "b2" pastPairing.blackId = "b2"
val (pairs, _) = SwissPairingService.computePairings(List(p1, p2, p3, p4), List(pastPairing)) val (pairs, _) = SwissPairingService.computePairings(List(p1, p2, p3, p4), List(pastPairing))
val pair1Ids = Set(pairs(0)._1.botId, pairs(0)._2.botId) val pair1Ids = Set(pairs(0)._1.botId, pairs(0)._2.botId)
assertFalse(pair1Ids == Set("b1", "b2"), "b1 and b2 should not be paired again") assertFalse(pair1Ids == Set("b1", "b2"), "b1 and b2 should not be paired again")
@Test @Test
def playerWithFewerByesGetsTheByeFirst(): Unit = def playerWithFewerByesGetsTheByeFirst(): Unit =
val p1 = makeParticipant("b1", "A", points = 1.0, byeCount = 1) val p1 = makeParticipant("b1", "A", points = 1.0, byeCount = 1)
val p2 = makeParticipant("b2", "B", points = 0.5, byeCount = 0) val p2 = makeParticipant("b2", "B", points = 0.5, byeCount = 0)
val p3 = makeParticipant("b3", "C", points = 0.0, byeCount = 0) val p3 = makeParticipant("b3", "C", points = 0.0, byeCount = 0)
val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3), Nil) val (pairs, bye) = SwissPairingService.computePairings(List(p1, p2, p3), Nil)
assertEquals(1, pairs.size) assertEquals(1, pairs.size)
assertTrue(bye.isDefined) assertTrue(bye.isDefined)