Team tournaments debugging

This commit is contained in:
Claude Brisson
2025-01-31 10:49:05 +01:00
parent 47c729e61a
commit ddf904f6d1
5 changed files with 162 additions and 29 deletions

View File

@@ -37,7 +37,7 @@ object PairingHandler: PairgothApiHandler {
"unpairables" to unpairables
)
if (tournament is TeamTournament) {
ret["individualGames"] = tournament.individualGames(round).map { it.toJson() }.toJsonArray()
ret["individualGames"] = tournament.individualGames(round).values.map { it.toJson() }.toJsonArray()
}
return ret
}
@@ -169,17 +169,17 @@ object PairingHandler: PairgothApiHandler {
val allPlayers = payload.size == 1 && payload[0] == "all"
if (allPlayers) {
// TODO - just remove this, it is never used ; and no check is done on whether the players are playing...
tournament.games(round).clear()
tournament.unpair(round)
} else {
payload.forEach {
val id = (it as Number).toInt()
val id = (it as Number).toID()
val game = tournament.games(round)[id] ?: throw Error("invalid game id")
if (game.result != Game.Result.UNKNOWN && game.black != 0 && game.white != 0) {
ApiHandler.logger.error("cannot unpair game id ${game.id}: it has a result")
// we'll only skip it
// throw Error("cannot unpair ")
} else {
tournament.games(round).remove(id)
tournament.unpair(round, id)
}
}
}

View File

@@ -14,7 +14,7 @@ object ResultsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val games = tournament.games(round).values
val games = tournament.individualGames(round).values
return games.map { it.toJson() }.toJsonArray()
}

View File

@@ -2,6 +2,7 @@ package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import com.republicate.kson.toJsonArray
import com.republicate.kson.toJsonObject
// CB TODO - review
//import kotlinx.datetime.LocalDate
import java.time.LocalDate
@@ -10,6 +11,8 @@ import org.jeudego.pairgoth.api.ApiHandler.Companion.logger
import org.jeudego.pairgoth.store.nextGameId
import org.jeudego.pairgoth.store.nextPlayerId
import org.jeudego.pairgoth.store.nextTournamentId
import org.jeudego.pairgoth.util.MutableBiMultiMap
import org.jeudego.pairgoth.util.mutableBiMultiMapOf
import kotlin.math.max
import java.util.*
import java.util.regex.Pattern
@@ -57,7 +60,7 @@ sealed class Tournament <P: Pairable>(
var frozen: Json.Array? = null
// pairing
fun pair(round: Int, pairables: List<Pairable>): List<Game> {
open fun pair(round: Int, pairables: List<Pairable>): List<Game> {
// Minimal check on round number.
// CB TODO - the complete check should verify, for each player, that he was either non pairable or implied in the previous round
if (round > games.size + 1) badRequest("previous round not paired")
@@ -71,12 +74,25 @@ sealed class Tournament <P: Pairable>(
}
}
open fun unpair(round: Int) {
games(round).clear()
}
open fun unpair(round: Int, id: ID) {
games(round).remove(id)
}
// games per id for each round
protected val games = mutableListOf<MutableMap<ID, Game>>()
fun games(round: Int) = games.getOrNull(round - 1) ?:
if (round > games.size + 1) throw Error("invalid round")
else mutableMapOf<ID, Game>().also { games.add(it) }
open fun individualGames(round: Int): Map<ID, Game> = games(round)
open fun postPair(round: Int, games: List<Game>) {}
fun lastRound() = max(1, games.size)
fun recomputeDUDD(round: Int, gameID: ID) {
@@ -196,7 +212,7 @@ sealed class Tournament <P: Pairable>(
// standard tournament of individuals
class StandardTournament(
id: ID,
type: Tournament.Type,
type: Type,
name: String,
shortName: String,
startDate: LocalDate,
@@ -219,7 +235,7 @@ class StandardTournament(
// team tournament
class TeamTournament(
id: ID,
type: Tournament.Type,
type: Type,
name: String,
shortName: String,
startDate: LocalDate,
@@ -234,7 +250,8 @@ class TeamTournament(
rules: Rules = Rules.FRENCH,
gobanSize: Int = 19,
komi: Double = 7.5,
tablesExclusion: MutableList<String> = mutableListOf()
tablesExclusion: MutableList<String> = mutableListOf(),
val individualGames: MutableBiMultiMap<ID, Game> = mutableBiMultiMapOf<ID, Game>()
): Tournament<TeamTournament.Team>(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi, tablesExclusion) {
companion object {
private val epsilon = 0.0001
@@ -242,24 +259,43 @@ class TeamTournament(
override val players = mutableMapOf<ID, Player>()
val teams: MutableMap<ID, Team> = _pairables
// For teams of individual players, map from a team game id to the list of individual games
// (filled on demand - it is merely a cache)
private val individualGames = mutableMapOf<ID, List<Game>>()
override fun individualGames(round: Int): Map<ID, Game> {
val teamGames = games(round)
return if (type.individual) {
return teamGames.values.flatMap { game ->
if (game.white == 0 || game.black == 0 ) listOf()
else individualGames[game.id]?.toList() ?: listOf()
}.associateBy { it.id }
} else {
teamGames
}
}
fun individualGames(round: Int): List<Game> {
val teamGames = games(round).values.toList()
return teamGames.flatMap { game ->
if (game.white == 0 || game.black ==0 ) listOf()
else {
individualGames.computeIfAbsent(game.id) { id ->
val whitePlayers = teams[game.white]!!.activePlayers(round)
val blackPlayers = teams[game.black]!!.activePlayers(round)
whitePlayers.zip(blackPlayers).map {
Game(nextGameId, game.table, it.first.id, it.second.id)
override fun pair(round: Int, pairables: List<Pairable>) =
super.pair(round, pairables).also { games ->
if (type.individual) {
games.forEach { game ->
individualGames.computeIfAbsent(game.id) { id ->
val whitePlayers = teams[game.white]!!.activePlayers(round)
val blackPlayers = teams[game.black]!!.activePlayers(round)
whitePlayers.zip(blackPlayers).map {
Game(nextGameId, game.table, it.first.id, it.second.id)
}.toMutableSet()
}
}
}
}
override fun unpair(round: Int) {
games(round).values.forEach { game ->
individualGames.remove(game.id)
}
super.unpair(round)
}
override fun unpair(round: Int, id: ID) {
individualGames.remove(id)
super.unpair(round, id)
}
fun pairedTeams() = super.pairedPlayers()
@@ -368,7 +404,12 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"),
pairing = json.getObject("pairing")?.let { Pairing.fromJson(it, default?.pairing) } ?: default?.pairing ?: badRequest("missing pairing"),
tablesExclusion = json.getArray("tablesExclusion")?.map { item -> item as String }?.toMutableList() ?: default?.tablesExclusion ?: mutableListOf()
tablesExclusion = json.getArray("tablesExclusion")?.map { item -> item as String }?.toMutableList() ?: default?.tablesExclusion ?: mutableListOf(),
individualGames = json.getObject("individualGames")?.entries?.flatMap {
(key, value) -> (value as Json.Array).map { game -> Game.fromJson(game as Json.Object) }.map { Pair(key.toID(), it) }
}?.let {
mutableBiMultiMapOf<ID, Game>(*it.toTypedArray())
} ?: (default as? TeamTournament)?.individualGames ?: mutableBiMultiMapOf<ID, Game>()
)
json.getArray("players")?.forEach { obj ->
val pairable = obj as Json.Object
@@ -428,6 +469,11 @@ fun Tournament<*>.toFullJson(): Json.Object {
json["teams"] = Json.Array(teams.values.map { it.toJson() })
}
json["games"] = Json.Array((1..lastRound()).mapTo(Json.MutableArray()) { round -> games(round).values.mapTo(Json.MutableArray()) { it.toJson() } });
if (this is TeamTournament && type.individual) {
json["individualGames"] = individualGames.mapValues { it ->
it.value.map { game -> game.toJson() }.toJsonArray()
}.toJsonObject()
}
if (tablesExclusion.isNotEmpty()) {
json["tablesExclusion"] = tablesExclusion.toJsonArray()
}

View File

@@ -0,0 +1,86 @@
package org.jeudego.pairgoth.util
/**
* MultiMap is an associative structure where each key can have one or several values.
*
* BiMultiMap is an associative structure where:
* <ul>
* <li>each key can have one or several values (as in a MultiMap)</li>
* <li>each value has only a distinct key</li>
* </ul>
*/
// CB TODO - ways to have Set instead of MutableSet here?
interface MultiMap<K, V>: Map<K, MutableSet<V>>
interface MutableMultiMap<K, V>: MultiMap<K, V>, MutableMap<K, MutableSet<V>> {
fun put(key: K, value: V): Boolean
fun putAll(vararg pairs: Pair<K, V>)
}
open class LinkedHashMultiMap<K, V>(vararg pairs: Pair<K, V>):
MutableMultiMap<K, V>,
LinkedHashMap<K, MutableSet<V>>(pairs.groupBy {
it.first
}.mapValues {
it.value.map { it.second }.toMutableSet()
}) {
override fun put(key: K, value: V): Boolean {
val set = super<LinkedHashMap>.computeIfAbsent(key) { mutableSetOf() }
return set.add(value)
}
override fun putAll(vararg pairs: Pair<K, V>) {
pairs.forEach { put(it.first, it.second) }
}
}
interface BiMultiMap<K, V>: MultiMap<K, V> {
val inverse: Map<V, K>
}
interface MutableBiMultiMap<K, V>: MutableMultiMap<K, V>, BiMultiMap<K, V> {
override val inverse: MutableMap<V, K>
}
open class LinkedHashBiMultiMap<K, V>(vararg pairs: Pair<K, V>
): MutableBiMultiMap<K, V>, LinkedHashMultiMap<K, V>(*pairs) {
override val inverse: MutableMap<V, K> = mutableMapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray())
override fun put(key: K, value: V): Boolean {
inverse[value] = key
return super<LinkedHashMultiMap>.put(key, value)
}
override fun remove(key: K): MutableSet<V>? {
return super<LinkedHashMultiMap>.remove(key)?.also {
it.forEach { inverse.remove(it) }
}
}
}
fun <K, V> multiMapOf(vararg pairs: Pair<K, V>): MultiMap<K, V> =
LinkedHashMultiMap<K, V>().apply {
pairs.forEach { (k, v) ->
put(k, v)
}
}
fun <K, V> mutableMultiMapOf(vararg pairs: Pair<K, V>): MutableMultiMap<K, V> =
LinkedHashMultiMap<K, V>().apply {
pairs.forEach { (k, v) ->
put(k, v)
}
}
fun <K, V> biMultiMapOf(vararg pairs: Pair<K, V>): BiMultiMap<K, V> =
LinkedHashBiMultiMap<K, V>().apply {
pairs.forEach { (k, v) ->
put(k, v)
}
}
fun <K, V> mutableBiMultiMapOf(vararg pairs: Pair<K, V>): MutableBiMultiMap<K, V> =
LinkedHashBiMultiMap<K, V>().apply {
pairs.forEach { (k, v) ->
put(k, v)
}
}

13
pom.xml
View File

@@ -81,13 +81,13 @@
<pairgoth.store>file</pairgoth.store>
<pairgoth.store.file.path>tournamentfiles</pairgoth.store.file.path>
<pairgoth.auth>none</pairgoth.auth>
<pairgoth.auth.sesame>this_should_be_overriden</pairgoth.auth.sesame>
<pairgoth.oauth.ffg.client_id>pairtogh</pairgoth.oauth.ffg.client_id>
<pairgoth.smtp.sender></pairgoth.smtp.sender>
<pairgoth.smtp.host></pairgoth.smtp.host>
<pairgoth.auth.sesame>this_should_be_overridden</pairgoth.auth.sesame>
<pairgoth.oauth.ffg.client_id>pairgoth</pairgoth.oauth.ffg.client_id>
<pairgoth.smtp.sender><!-- email sender address --></pairgoth.smtp.sender>
<pairgoth.smtp.host><!-- smtp host --></pairgoth.smtp.host>
<pairgoth.smtp.port>587</pairgoth.smtp.port>
<pairgoth.smtp.user></pairgoth.smtp.user>
<pairgoth.smtp.password></pairgoth.smtp.password>
<pairgoth.smtp.user><!-- smtp host login user --></pairgoth.smtp.user>
<pairgoth.smtp.password><!-- smtp host login password --></pairgoth.smtp.password>
<pairgoth.logger.level>info</pairgoth.logger.level>
<pairgoth.logger.format>[%level] %ip [%logger] %message</pairgoth.logger.format>
</properties>
@@ -190,6 +190,7 @@
<version>${kotlin.version}</version>
<configuration>
<jvmTarget>11</jvmTarget>
<apiVersion>2.1</apiVersion>
</configuration>
</plugin>
<plugin>