From ecec6556d15eb120f4c049ce4b5cc57d57a29f84 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Tue, 22 Jul 2025 19:08:29 +0200 Subject: [PATCH] Code cleaning: move history helper creation in tournament class, factorize main score function --- .../org/jeudego/pairgoth/api/ApiTools.kt | 68 +++++-------------- .../org/jeudego/pairgoth/model/Pairing.kt | 4 +- .../org/jeudego/pairgoth/model/Tournament.kt | 48 ++++++++++++- .../pairgoth/pairing/BasePairingHelper.kt | 34 ++++------ .../jeudego/pairgoth/pairing/HistoryHelper.kt | 21 +++--- .../pairgoth/pairing/solver/BaseSolver.kt | 10 +-- .../pairgoth/pairing/solver/MacMahonSolver.kt | 25 ++----- .../pairgoth/pairing/solver/SwissSolver.kt | 12 +--- .../src/main/webapp/explain-pairing.html | 0 9 files changed, 104 insertions(+), 118 deletions(-) create mode 100644 view-webapp/src/main/webapp/explain-pairing.html diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt index 8d74eff..c48e00e 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt @@ -13,12 +13,8 @@ import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.historyBefore import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.solver.MacMahonSolver -import kotlin.math.floor import kotlin.math.max import kotlin.math.min -import kotlin.math.round - -// TODO CB avoid code redundancy with solvers fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = false): List { @@ -27,42 +23,12 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f return min(max(rank, pairing.mmFloor), pairing.mmBar) + MacMahonSolver.mmsZero + mmsCorrection } - fun roundScore(score: Double): Double { - val epsilon = 0.00001 - // Note: this works for now because we only have .0 and .5 fractional parts - return if (pairing.pairingParams.main.roundDownScore) floor(score + epsilon) - else round(2 * score) / 2 - } - if (frozen != null) { return ArrayList(frozen!!.map { it -> it as Json.Object }) } - // CB TODO - factorize history helper creation between here and solver classes - val historyHelper = HistoryHelper(historyBefore(round + 1)) { - if (pairing.type == PairingType.SWISS) { - pairables.mapValues { - Pair(0.0, wins[it.key] ?: 0.0) - } - } - else { - pairables.mapValues { - it.value.let { pairable -> - val mmBase = pairable.mmBase() - val score = roundScore(mmBase + - (nbW(pairable) ?: 0.0) + - (1..round).sumOf { round -> - if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0.0 else 1.0 - } * pairing.pairingParams.main.mmsValueAbsent) - Pair( - if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase - else roundScore(mmBase + round/2), - score - ) - } - } - } - } + val history = historyHelper(round) + val neededCriteria = ArrayList(pairing.placementParams.criteria) if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW) if (!neededCriteria.contains(Criterion.RATING)) neededCriteria.add(Criterion.RATING) @@ -73,24 +39,24 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f Criterion.CATEGORY -> StandingsHandler.nullMap Criterion.RANK -> pairables.mapValues { it.value.rank } Criterion.RATING -> pairables.mapValues { it.value.rating } - Criterion.NBW -> historyHelper.wins - Criterion.MMS -> historyHelper.mms - Criterion.SCOREX -> historyHelper.scoresX + Criterion.NBW -> history.wins + Criterion.MMS -> history.mms + Criterion.SCOREX -> history.scoresX Criterion.STS -> StandingsHandler.nullMap Criterion.CPS -> StandingsHandler.nullMap - Criterion.SOSW -> historyHelper.sos - Criterion.SOSWM1 -> historyHelper.sosm1 - Criterion.SOSWM2 -> historyHelper.sosm2 - Criterion.SODOSW -> historyHelper.sodos - Criterion.SOSOSW -> historyHelper.sosos - Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else historyHelper.cumScore - Criterion.SOSM -> historyHelper.sos - Criterion.SOSMM1 -> historyHelper.sosm1 - Criterion.SOSMM2 -> historyHelper.sosm2 - Criterion.SODOSM -> historyHelper.sodos - Criterion.SOSOSM -> historyHelper.sosos - Criterion.CUSSM -> historyHelper.cumScore + Criterion.SOSW -> history.sos + Criterion.SOSWM1 -> history.sosm1 + Criterion.SOSWM2 -> history.sosm2 + Criterion.SODOSW -> history.sodos + Criterion.SOSOSW -> history.sosos + Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else history.cumScore + Criterion.SOSM -> history.sos + Criterion.SOSMM1 -> history.sosm1 + Criterion.SOSMM2 -> history.sosm2 + Criterion.SODOSM -> history.sodos + Criterion.SOSOSM -> history.sosos + Criterion.CUSSM -> history.cumScore Criterion.SOSTS -> StandingsHandler.nullMap diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt index 167e981..e4a3877 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt @@ -175,7 +175,7 @@ class Swiss( ): Pairing(SWISS, pairingParams, placementParams) { companion object {} override fun solver(tournament: Tournament<*>, round: Int, pairables: List) = - SwissSolver(round, tournament.rounds, tournament.historyBefore(round), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round)) + SwissSolver(round, tournament.rounds, tournament.historyHelper(round), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round)) } class MacMahon( @@ -203,7 +203,7 @@ class MacMahon( ): Pairing(MAC_MAHON, pairingParams, placementParams) { companion object {} override fun solver(tournament: Tournament<*>, round: Int, pairables: List) = - MacMahonSolver(round, tournament.rounds, tournament.historyBefore(round), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round), mmFloor, mmBar) + MacMahonSolver(round, tournament.rounds, tournament.historyHelper(round), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round), mmFloor, mmBar) } class RoundRobin( 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 2d8efaf..aefbdc3 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 @@ -7,7 +7,8 @@ import com.republicate.kson.toJsonObject //import kotlinx.datetime.LocalDate import java.time.LocalDate import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest -import org.jeudego.pairgoth.api.ApiHandler.Companion.logger +import org.jeudego.pairgoth.pairing.HistoryHelper +import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.store.nextGameId import org.jeudego.pairgoth.store.nextPlayerId import org.jeudego.pairgoth.store.nextTournamentId @@ -15,8 +16,10 @@ import org.jeudego.pairgoth.util.MutableBiMultiMap import org.jeudego.pairgoth.util.mutableBiMultiMapOf import kotlin.math.max import java.util.* -import java.util.regex.Pattern import kotlin.collections.get +import kotlin.math.floor +import kotlin.math.min +import kotlin.math.round import kotlin.math.roundToInt sealed class Tournament ( @@ -208,6 +211,47 @@ sealed class Tournament ( } return excluded } + + fun roundScore(score: Double): Double { + val epsilon = 0.00001 + // Note: this works for now because we only have .0 and .5 fractional parts + return if (pairing.pairingParams.main.roundDownScore) floor(score + epsilon) + else round(2 * score) / 2 + } + + fun Pairable.mmBase(): Double { + if (pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon") + return min(max(rank, pairing.mmFloor), pairing.mmBar) + MacMahonSolver.mmsZero + mmsCorrection + } + + fun historyHelper(round: Int): HistoryHelper { + return HistoryHelper(historyBefore(round + 1)) { + if (pairing.type == PairingType.SWISS) { + pairables.mapValues { + // In a Swiss tournament the main criterion is the number of wins + Pair(0.0, wins[it.key] ?: 0.0) + } + } + else { + pairables.mapValues { + // In a MacMahon tournament the main criterion is the mms + it.value.let { pairable -> + val mmBase = pairable.mmBase() + val score = roundScore(mmBase + + (nbW(pairable) ?: 0.0) + + (1..round).sumOf { round -> + if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0.0 else 1.0 + } * pairing.pairingParams.main.mmsValueAbsent) + Pair( + if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase + else roundScore(mmBase + round/2), + score + ) + } + } + } + } + } } // standard tournament of individuals diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt index e15bc8e..bb309a1 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt @@ -5,20 +5,14 @@ import org.jeudego.pairgoth.model.* abstract class BasePairingHelper( val round: Int, val totalRounds: Int, - history: List>, // History of all games played for each round + val history: HistoryHelper, // Digested history of all games played for each round var pairables: List, // All pairables for this round, it may include the bye player - val pairablesMap: Map, // Map of all known pairables for this tournament val pairing: PairingParams, val placement: PlacementParams, ) { - abstract val scores: Map> + val scores get() = history.scores abstract val scoresX: Map - val historyHelper = - if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper( - history - ) { scores } - else HistoryHelper(history) { scores } // Extend pairables with members from all rounds @@ -84,28 +78,28 @@ abstract class BasePairingHelper( } // already paired players map - protected fun Pairable.played(other: Pairable) = historyHelper.playedTogether(this, other) + protected fun Pairable.played(other: Pairable) = history.playedTogether(this, other) // color balance (nw - nb) - protected val Pairable.colorBalance: Int get() = historyHelper.colorBalance(this) ?: 0 + protected val Pairable.colorBalance: Int get() = history.colorBalance(this) ?: 0 protected val Pairable.group: Int get() = _groups[id]!! - protected val Pairable.drawnUpDown: Pair get() = historyHelper.drawnUpDown(this) ?: Pair(0, 0) + protected val Pairable.drawnUpDown: Pair get() = history.drawnUpDown(this) ?: Pair(0, 0) - protected val Pairable.nbBye: Int get() = historyHelper.nbPlayedWithBye(this) ?: 0 + protected val Pairable.nbBye: Int get() = history.nbPlayedWithBye(this) ?: 0 // score (number of wins) - val Pairable.nbW: Double get() = historyHelper.nbW(this) ?: 0.0 + val Pairable.nbW: Double get() = history.nbW(this) ?: 0.0 - val Pairable.sos: Double get() = historyHelper.sos[id] ?: 0.0 - val Pairable.sosm1: Double get() = historyHelper.sosm1[id] ?: 0.0 - val Pairable.sosm2: Double get() = historyHelper.sosm2[id] ?: 0.0 - val Pairable.sosos: Double get() = historyHelper.sosos[id] ?: 0.0 - val Pairable.sodos: Double get() = historyHelper.sodos[id] ?: 0.0 - val Pairable.cums: Double get() = historyHelper.cumScore[id] ?: 0.0 + val Pairable.sos: Double get() = history.sos[id] ?: 0.0 + val Pairable.sosm1: Double get() = history.sosm1[id] ?: 0.0 + val Pairable.sosm2: Double get() = history.sosm2[id] ?: 0.0 + val Pairable.sosos: Double get() = history.sosos[id] ?: 0.0 + val Pairable.sodos: Double get() = history.sodos[id] ?: 0.0 + val Pairable.cums: Double get() = history.cumScore[id] ?: 0.0 fun Pairable.missedRounds(): Int = (1 until round).map { round -> - if (historyHelper.playersPerRound.getOrNull(round - 1) + if (history.playersPerRound.getOrNull(round - 1) ?.contains(id) == true ) 0 else 1 }.sum() diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt index 11b3e54..68895fa 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt @@ -3,11 +3,21 @@ package org.jeudego.pairgoth.pairing import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.Game.Result.* import org.jeudego.pairgoth.model.TeamTournament.Team +import org.jeudego.pairgoth.pairing.solver.MacMahonSolver +import kotlin.math.max +import kotlin.math.min + +/** + * Map from a pairable ID to a pair of (missed rounds increment, main score). + * The missed rounds increment is 0 for Swiss, and a function of the MMS base of the pairable for MacMahon. + * The main score is the NBW for the Swiss, the MMS for MacMahon. + */ +typealias ScoreMapBuilder = HistoryHelper.()-> Map> open class HistoryHelper( protected val history: List>, // scoresGetter() returns Pair(sos value for missed rounds, score) where score is nbw for Swiss, mms for MM, ... - scoresGetter: HistoryHelper.()-> Map>) { + scoresGetter: ScoreMapBuilder) { private val Game.blackScore get() = when (result) { BLACK, BOTHWIN -> 1.0 @@ -19,7 +29,7 @@ open class HistoryHelper( else -> 0.0 } - private val scores by lazy { + val scores by lazy { scoresGetter() } @@ -241,11 +251,4 @@ open class HistoryHelper( else null } } - -} - -// CB TODO - a big problem with the current naive implementation is that the team score is -for now- the sum of team members individual scores - -class TeamOfIndividualsHistoryHelper(history: List>, scoresGetter: () -> Map>): - HistoryHelper(history, { scoresGetter() }) { } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt index efb9983..57d2b2d 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt @@ -4,6 +4,7 @@ import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.MainCritParams.DrawUpDown import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.* import org.jeudego.pairgoth.pairing.BasePairingHelper +import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.detRandom import org.jeudego.pairgoth.pairing.nonDetRandom import org.jeudego.pairgoth.store.nextGameId @@ -21,13 +22,12 @@ import kotlin.math.* sealed class BaseSolver( round: Int, totalRounds: Int, - history: List>, // History of all games played for each round + history: HistoryHelper, // History of all games played for each round pairables: List, // All pairables for this round, it may include the bye player - pairablesMap: Map, // Map of all known pairables in this tournament pairing: PairingParams, placement: PlacementParams, val usedTables: BitSet - ) : BasePairingHelper(round, totalRounds, history, pairables, pairablesMap, pairing, placement) { + ) : BasePairingHelper(round, totalRounds, history, pairables, pairing, placement) { companion object { val rand = Random(/* seed from properties - TODO */) @@ -79,12 +79,12 @@ sealed class BaseSolver( // Choose bye player and remove from pairables if (ByePlayer in nameSortedPairables){ nameSortedPairables.remove(ByePlayer) - var minWeight = 1000.0*round + (Pairable.MAX_RANK - Pairable.MIN_RANK) + 1; + var minWeight = 1000.0 * round + (Pairable.MAX_RANK - Pairable.MIN_RANK) + 1; var weightForBye : Double var byePlayerIndex = 0 for (p in nameSortedPairables){ weightForBye = computeWeightForBye(p) - if (p.id in historyHelper.byePlayers) weightForBye += 1000 + if (p.id in history.byePlayers) weightForBye += 1000 if (weightForBye <= minWeight){ minWeight = weightForBye chosenByePlayer = p diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt index c9c7503..6030d54 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt @@ -1,6 +1,7 @@ package org.jeudego.pairgoth.pairing.solver import org.jeudego.pairgoth.model.* +import org.jeudego.pairgoth.pairing.HistoryHelper import java.util.* import kotlin.math.max import kotlin.math.min @@ -8,34 +9,18 @@ import kotlin.math.roundToInt class MacMahonSolver(round: Int, totalRounds: Int, - history: List>, + history: HistoryHelper, pairables: List, - pairablesMap: Map, + allPairablesMap: Map, pairingParams: PairingParams, placementParams: PlacementParams, usedTables: BitSet, private val mmFloor: Int, private val mmBar: Int) : - BaseSolver(round, totalRounds, history, pairables, pairablesMap, pairingParams, placementParams, usedTables) { - - override val scores: Map> by lazy { - require (mmBar > mmFloor) { "MMFloor is higher than MMBar" } - pairablesMap.mapValues { - it.value.let { pairable -> - val score = roundScore(pairable.mmBase + - pairable.nbW + - pairable.missedRounds() * pairingParams.main.mmsValueAbsent) - Pair( - if (pairingParams.main.sosValueAbsentUseBase) pairable.mmBase - else roundScore(pairable.mmBase + round/2), - score - ) - } - } - } + BaseSolver(round, totalRounds, history, pairables, pairingParams, placementParams, usedTables) { override val scoresX: Map by lazy { require (mmBar > mmFloor) { "MMFloor is higher than MMBar" } - pairablesMap.mapValues { + allPairablesMap.mapValues { it.value.let { pairable -> roundScore(pairable.mmBase + pairable.nbW) } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt index aa1f396..5e3891f 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt @@ -1,26 +1,20 @@ package org.jeudego.pairgoth.pairing.solver import org.jeudego.pairgoth.model.* +import org.jeudego.pairgoth.pairing.HistoryHelper import java.util.* class SwissSolver(round: Int, totalRounds: Int, - history: List>, + history: HistoryHelper, pairables: List, pairablesMap: Map, pairingParams: PairingParams, placementParams: PlacementParams, usedTables: BitSet ): - BaseSolver(round, totalRounds, history, pairables, pairablesMap, pairingParams, placementParams, usedTables) { + BaseSolver(round, totalRounds, history, pairables, pairingParams, placementParams, usedTables) { - // In a Swiss tournament the main criterion is the number of wins and already computed - - override val scores by lazy { - pairablesMap.mapValues { - Pair(0.0, historyHelper.wins[it.value.id] ?: 0.0) - } - } override val scoresX: Map get() = scores.mapValues { it.value.second } override val mainLimits = Pair(0.0, round - 1.0) diff --git a/view-webapp/src/main/webapp/explain-pairing.html b/view-webapp/src/main/webapp/explain-pairing.html new file mode 100644 index 0000000..e69de29