fix(tournament): fix scalafix violations and apply scalafmt formatting
Build & Test (NowChessSystems) TeamCity build finished
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:
+2
-2
@@ -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] =
|
||||||
|
|||||||
+2
-1
@@ -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
|
||||||
|
|||||||
+4
-4
@@ -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
|
||||||
|
|||||||
+6
-6
@@ -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
|
||||||
|
|||||||
+6
-6
@@ -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")
|
||||||
|
|||||||
+26
-17
@@ -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")
|
||||||
|
|||||||
+1
-1
@@ -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)
|
||||||
|
|||||||
+1
-1
@@ -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)
|
||||||
|
|||||||
+7
-3
@@ -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
|
||||||
|
|||||||
+13
-8
@@ -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
|
||||||
|
|||||||
+26
-9
@@ -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 =
|
||||||
|
|||||||
+6
-2
@@ -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))
|
||||||
|
|||||||
+10
-5
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
+56
-22
@@ -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")
|
||||||
|
|||||||
+24
-19
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user