From ddf904f6d1c00f2abc699017a3fa3839defb5ed5 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Fri, 31 Jan 2025 10:49:05 +0100 Subject: [PATCH] Team tournaments debugging --- .../jeudego/pairgoth/api/PairingHandler.kt | 8 +- .../jeudego/pairgoth/api/ResultsHandler.kt | 2 +- .../org/jeudego/pairgoth/model/Tournament.kt | 82 ++++++++++++++---- .../org/jeudego/pairgoth/util/MultiMap.kt | 86 +++++++++++++++++++ pom.xml | 13 +-- 5 files changed, 162 insertions(+), 29 deletions(-) create mode 100644 pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/MultiMap.kt diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index 544814e..3ca1fd4 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -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) } } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt index d9c4b63..6671080 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt @@ -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() } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index 29402e6..b2c4ee9 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -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 ( var frozen: Json.Array? = null // pairing - fun pair(round: Int, pairables: List): List { + open fun pair(round: Int, pairables: List): List { // 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 ( } } + 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>() fun games(round: Int) = games.getOrNull(round - 1) ?: if (round > games.size + 1) throw Error("invalid round") else mutableMapOf().also { games.add(it) } + + open fun individualGames(round: Int): Map = games(round) + + open fun postPair(round: Int, games: List) {} + fun lastRound() = max(1, games.size) fun recomputeDUDD(round: Int, gameID: ID) { @@ -196,7 +212,7 @@ sealed class Tournament ( // 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 = mutableListOf() + tablesExclusion: MutableList = mutableListOf(), + val individualGames: MutableBiMultiMap = mutableBiMultiMapOf() ): Tournament(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() val teams: MutableMap = _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>() + override fun individualGames(round: Int): Map { + 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 { - 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) = + 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(*it.toTypedArray()) + } ?: (default as? TeamTournament)?.individualGames ?: mutableBiMultiMapOf() ) 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() } diff --git a/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/MultiMap.kt b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/MultiMap.kt new file mode 100644 index 0000000..1bc9c3d --- /dev/null +++ b/pairgoth-common/src/main/kotlin/org/jeudego/pairgoth/util/MultiMap.kt @@ -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: + *
    + *
  • each key can have one or several values (as in a MultiMap)
  • + *
  • each value has only a distinct key
  • + *
+ */ + +// CB TODO - ways to have Set instead of MutableSet here? +interface MultiMap: Map> + +interface MutableMultiMap: MultiMap, MutableMap> { + fun put(key: K, value: V): Boolean + fun putAll(vararg pairs: Pair) +} + +open class LinkedHashMultiMap(vararg pairs: Pair): + MutableMultiMap, + LinkedHashMap>(pairs.groupBy { + it.first + }.mapValues { + it.value.map { it.second }.toMutableSet() + }) { + override fun put(key: K, value: V): Boolean { + val set = super.computeIfAbsent(key) { mutableSetOf() } + return set.add(value) + } + + override fun putAll(vararg pairs: Pair) { + pairs.forEach { put(it.first, it.second) } + } +} + +interface BiMultiMap: MultiMap { + val inverse: Map +} + +interface MutableBiMultiMap: MutableMultiMap, BiMultiMap { + override val inverse: MutableMap +} + +open class LinkedHashBiMultiMap(vararg pairs: Pair +): MutableBiMultiMap, LinkedHashMultiMap(*pairs) { + override val inverse: MutableMap = mutableMapOf(*pairs.map { Pair(it.second, it.first) }.toTypedArray()) + override fun put(key: K, value: V): Boolean { + inverse[value] = key + return super.put(key, value) + } + override fun remove(key: K): MutableSet? { + return super.remove(key)?.also { + it.forEach { inverse.remove(it) } + } + } +} + +fun multiMapOf(vararg pairs: Pair): MultiMap = + LinkedHashMultiMap().apply { + pairs.forEach { (k, v) -> + put(k, v) + } + } + +fun mutableMultiMapOf(vararg pairs: Pair): MutableMultiMap = + LinkedHashMultiMap().apply { + pairs.forEach { (k, v) -> + put(k, v) + } + } + +fun biMultiMapOf(vararg pairs: Pair): BiMultiMap = + LinkedHashBiMultiMap().apply { + pairs.forEach { (k, v) -> + put(k, v) + } + } + +fun mutableBiMultiMapOf(vararg pairs: Pair): MutableBiMultiMap = + LinkedHashBiMultiMap().apply { + pairs.forEach { (k, v) -> + put(k, v) + } + } diff --git a/pom.xml b/pom.xml index 822ab21..c0907d4 100644 --- a/pom.xml +++ b/pom.xml @@ -81,13 +81,13 @@ file tournamentfiles none - this_should_be_overriden - pairtogh - - + this_should_be_overridden + pairgoth + + 587 - - + + info [%level] %ip [%logger] %message @@ -190,6 +190,7 @@ ${kotlin.version} 11 + 2.1