Code cleaning: move history helper creation in tournament class, factorize main score function

This commit is contained in:
Claude Brisson
2025-07-22 19:08:29 +02:00
parent 17bb013feb
commit ecec6556d1
9 changed files with 104 additions and 118 deletions

View File

@@ -13,12 +13,8 @@ import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.historyBefore import org.jeudego.pairgoth.model.historyBefore
import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.HistoryHelper
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
import kotlin.math.floor
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.round
// TODO CB avoid code redundancy with solvers
fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = false): List<Json.Object> { fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = false): List<Json.Object> {
@@ -27,42 +23,12 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
return min(max(rank, pairing.mmFloor), pairing.mmBar) + MacMahonSolver.mmsZero + mmsCorrection 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) { if (frozen != null) {
return ArrayList(frozen!!.map { it -> it as Json.Object }) return ArrayList(frozen!!.map { it -> it as Json.Object })
} }
// CB TODO - factorize history helper creation between here and solver classes val history = historyHelper(round)
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 neededCriteria = ArrayList(pairing.placementParams.criteria) val neededCriteria = ArrayList(pairing.placementParams.criteria)
if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW) if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW)
if (!neededCriteria.contains(Criterion.RATING)) neededCriteria.add(Criterion.RATING) 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.CATEGORY -> StandingsHandler.nullMap
Criterion.RANK -> pairables.mapValues { it.value.rank } Criterion.RANK -> pairables.mapValues { it.value.rank }
Criterion.RATING -> pairables.mapValues { it.value.rating } Criterion.RATING -> pairables.mapValues { it.value.rating }
Criterion.NBW -> historyHelper.wins Criterion.NBW -> history.wins
Criterion.MMS -> historyHelper.mms Criterion.MMS -> history.mms
Criterion.SCOREX -> historyHelper.scoresX Criterion.SCOREX -> history.scoresX
Criterion.STS -> StandingsHandler.nullMap Criterion.STS -> StandingsHandler.nullMap
Criterion.CPS -> StandingsHandler.nullMap Criterion.CPS -> StandingsHandler.nullMap
Criterion.SOSW -> historyHelper.sos Criterion.SOSW -> history.sos
Criterion.SOSWM1 -> historyHelper.sosm1 Criterion.SOSWM1 -> history.sosm1
Criterion.SOSWM2 -> historyHelper.sosm2 Criterion.SOSWM2 -> history.sosm2
Criterion.SODOSW -> historyHelper.sodos Criterion.SODOSW -> history.sodos
Criterion.SOSOSW -> historyHelper.sosos Criterion.SOSOSW -> history.sosos
Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else historyHelper.cumScore Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else history.cumScore
Criterion.SOSM -> historyHelper.sos Criterion.SOSM -> history.sos
Criterion.SOSMM1 -> historyHelper.sosm1 Criterion.SOSMM1 -> history.sosm1
Criterion.SOSMM2 -> historyHelper.sosm2 Criterion.SOSMM2 -> history.sosm2
Criterion.SODOSM -> historyHelper.sodos Criterion.SODOSM -> history.sodos
Criterion.SOSOSM -> historyHelper.sosos Criterion.SOSOSM -> history.sosos
Criterion.CUSSM -> historyHelper.cumScore Criterion.CUSSM -> history.cumScore
Criterion.SOSTS -> StandingsHandler.nullMap Criterion.SOSTS -> StandingsHandler.nullMap

View File

@@ -175,7 +175,7 @@ class Swiss(
): Pairing(SWISS, pairingParams, placementParams) { ): Pairing(SWISS, pairingParams, placementParams) {
companion object {} companion object {}
override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>) = override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>) =
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( class MacMahon(
@@ -203,7 +203,7 @@ class MacMahon(
): Pairing(MAC_MAHON, pairingParams, placementParams) { ): Pairing(MAC_MAHON, pairingParams, placementParams) {
companion object {} companion object {}
override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>) = override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>) =
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( class RoundRobin(

View File

@@ -7,7 +7,8 @@ import com.republicate.kson.toJsonObject
//import kotlinx.datetime.LocalDate //import kotlinx.datetime.LocalDate
import java.time.LocalDate import java.time.LocalDate
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest 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.nextGameId
import org.jeudego.pairgoth.store.nextPlayerId import org.jeudego.pairgoth.store.nextPlayerId
import org.jeudego.pairgoth.store.nextTournamentId import org.jeudego.pairgoth.store.nextTournamentId
@@ -15,8 +16,10 @@ import org.jeudego.pairgoth.util.MutableBiMultiMap
import org.jeudego.pairgoth.util.mutableBiMultiMapOf import org.jeudego.pairgoth.util.mutableBiMultiMapOf
import kotlin.math.max import kotlin.math.max
import java.util.* import java.util.*
import java.util.regex.Pattern
import kotlin.collections.get import kotlin.collections.get
import kotlin.math.floor
import kotlin.math.min
import kotlin.math.round
import kotlin.math.roundToInt import kotlin.math.roundToInt
sealed class Tournament <P: Pairable>( sealed class Tournament <P: Pairable>(
@@ -208,6 +211,47 @@ sealed class Tournament <P: Pairable>(
} }
return excluded 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 // standard tournament of individuals

View File

@@ -5,20 +5,14 @@ import org.jeudego.pairgoth.model.*
abstract class BasePairingHelper( abstract class BasePairingHelper(
val round: Int, val round: Int,
val totalRounds: Int, val totalRounds: Int,
history: List<List<Game>>, // History of all games played for each round val history: HistoryHelper, // Digested history of all games played for each round
var pairables: List<Pairable>, // All pairables for this round, it may include the bye player var pairables: List<Pairable>, // All pairables for this round, it may include the bye player
val pairablesMap: Map<ID, Pairable>, // Map of all known pairables for this tournament
val pairing: PairingParams, val pairing: PairingParams,
val placement: PlacementParams, val placement: PlacementParams,
) { ) {
abstract val scores: Map<ID, Pair<Double, Double>> val scores get() = history.scores
abstract val scoresX: Map<ID, Double> abstract val scoresX: Map<ID, Double>
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 // Extend pairables with members from all rounds
@@ -84,28 +78,28 @@ abstract class BasePairingHelper(
} }
// already paired players map // 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) // 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.group: Int get() = _groups[id]!!
protected val Pairable.drawnUpDown: Pair<Int, Int> get() = historyHelper.drawnUpDown(this) ?: Pair(0, 0) protected val Pairable.drawnUpDown: Pair<Int, Int> 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) // 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.sos: Double get() = history.sos[id] ?: 0.0
val Pairable.sosm1: Double get() = historyHelper.sosm1[id] ?: 0.0 val Pairable.sosm1: Double get() = history.sosm1[id] ?: 0.0
val Pairable.sosm2: Double get() = historyHelper.sosm2[id] ?: 0.0 val Pairable.sosm2: Double get() = history.sosm2[id] ?: 0.0
val Pairable.sosos: Double get() = historyHelper.sosos[id] ?: 0.0 val Pairable.sosos: Double get() = history.sosos[id] ?: 0.0
val Pairable.sodos: Double get() = historyHelper.sodos[id] ?: 0.0 val Pairable.sodos: Double get() = history.sodos[id] ?: 0.0
val Pairable.cums: Double get() = historyHelper.cumScore[id] ?: 0.0 val Pairable.cums: Double get() = history.cumScore[id] ?: 0.0
fun Pairable.missedRounds(): Int = (1 until round).map { round -> fun Pairable.missedRounds(): Int = (1 until round).map { round ->
if (historyHelper.playersPerRound.getOrNull(round - 1) if (history.playersPerRound.getOrNull(round - 1)
?.contains(id) == true ?.contains(id) == true
) 0 else 1 ) 0 else 1
}.sum() }.sum()

View File

@@ -3,11 +3,21 @@ package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.model.Game.Result.* import org.jeudego.pairgoth.model.Game.Result.*
import org.jeudego.pairgoth.model.TeamTournament.Team 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<ID, Pair<Double, Double>>
open class HistoryHelper( open class HistoryHelper(
protected val history: List<List<Game>>, protected val history: List<List<Game>>,
// scoresGetter() returns Pair(sos value for missed rounds, score) where score is nbw for Swiss, mms for MM, ... // scoresGetter() returns Pair(sos value for missed rounds, score) where score is nbw for Swiss, mms for MM, ...
scoresGetter: HistoryHelper.()-> Map<ID, Pair<Double, Double>>) { scoresGetter: ScoreMapBuilder) {
private val Game.blackScore get() = when (result) { private val Game.blackScore get() = when (result) {
BLACK, BOTHWIN -> 1.0 BLACK, BOTHWIN -> 1.0
@@ -19,7 +29,7 @@ open class HistoryHelper(
else -> 0.0 else -> 0.0
} }
private val scores by lazy { val scores by lazy {
scoresGetter() scoresGetter()
} }
@@ -241,11 +251,4 @@ open class HistoryHelper(
else null 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<List<Game>>, scoresGetter: () -> Map<ID, Pair<Double, Double>>):
HistoryHelper(history, { scoresGetter() }) {
} }

View File

@@ -4,6 +4,7 @@ import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.model.MainCritParams.DrawUpDown import org.jeudego.pairgoth.model.MainCritParams.DrawUpDown
import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.* import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.*
import org.jeudego.pairgoth.pairing.BasePairingHelper import org.jeudego.pairgoth.pairing.BasePairingHelper
import org.jeudego.pairgoth.pairing.HistoryHelper
import org.jeudego.pairgoth.pairing.detRandom import org.jeudego.pairgoth.pairing.detRandom
import org.jeudego.pairgoth.pairing.nonDetRandom import org.jeudego.pairgoth.pairing.nonDetRandom
import org.jeudego.pairgoth.store.nextGameId import org.jeudego.pairgoth.store.nextGameId
@@ -21,13 +22,12 @@ import kotlin.math.*
sealed class BaseSolver( sealed class BaseSolver(
round: Int, round: Int,
totalRounds: Int, totalRounds: Int,
history: List<List<Game>>, // History of all games played for each round history: HistoryHelper, // History of all games played for each round
pairables: List<Pairable>, // All pairables for this round, it may include the bye player pairables: List<Pairable>, // All pairables for this round, it may include the bye player
pairablesMap: Map<ID, Pairable>, // Map of all known pairables in this tournament
pairing: PairingParams, pairing: PairingParams,
placement: PlacementParams, placement: PlacementParams,
val usedTables: BitSet val usedTables: BitSet
) : BasePairingHelper(round, totalRounds, history, pairables, pairablesMap, pairing, placement) { ) : BasePairingHelper(round, totalRounds, history, pairables, pairing, placement) {
companion object { companion object {
val rand = Random(/* seed from properties - TODO */) val rand = Random(/* seed from properties - TODO */)
@@ -79,12 +79,12 @@ sealed class BaseSolver(
// Choose bye player and remove from pairables // Choose bye player and remove from pairables
if (ByePlayer in nameSortedPairables){ if (ByePlayer in nameSortedPairables){
nameSortedPairables.remove(ByePlayer) 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 weightForBye : Double
var byePlayerIndex = 0 var byePlayerIndex = 0
for (p in nameSortedPairables){ for (p in nameSortedPairables){
weightForBye = computeWeightForBye(p) weightForBye = computeWeightForBye(p)
if (p.id in historyHelper.byePlayers) weightForBye += 1000 if (p.id in history.byePlayers) weightForBye += 1000
if (weightForBye <= minWeight){ if (weightForBye <= minWeight){
minWeight = weightForBye minWeight = weightForBye
chosenByePlayer = p chosenByePlayer = p

View File

@@ -1,6 +1,7 @@
package org.jeudego.pairgoth.pairing.solver package org.jeudego.pairgoth.pairing.solver
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.pairing.HistoryHelper
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -8,34 +9,18 @@ import kotlin.math.roundToInt
class MacMahonSolver(round: Int, class MacMahonSolver(round: Int,
totalRounds: Int, totalRounds: Int,
history: List<List<Game>>, history: HistoryHelper,
pairables: List<Pairable>, pairables: List<Pairable>,
pairablesMap: Map<ID, Pairable>, allPairablesMap: Map<ID, Pairable>,
pairingParams: PairingParams, pairingParams: PairingParams,
placementParams: PlacementParams, placementParams: PlacementParams,
usedTables: BitSet, usedTables: BitSet,
private val mmFloor: Int, private val mmBar: Int) : private val mmFloor: Int, private val mmBar: Int) :
BaseSolver(round, totalRounds, history, pairables, pairablesMap, pairingParams, placementParams, usedTables) { BaseSolver(round, totalRounds, history, pairables, pairingParams, placementParams, usedTables) {
override val scores: Map<ID, Pair<Double, Double>> 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
)
}
}
}
override val scoresX: Map<ID, Double> by lazy { override val scoresX: Map<ID, Double> by lazy {
require (mmBar > mmFloor) { "MMFloor is higher than MMBar" } require (mmBar > mmFloor) { "MMFloor is higher than MMBar" }
pairablesMap.mapValues { allPairablesMap.mapValues {
it.value.let { pairable -> it.value.let { pairable ->
roundScore(pairable.mmBase + pairable.nbW) roundScore(pairable.mmBase + pairable.nbW)
} }

View File

@@ -1,26 +1,20 @@
package org.jeudego.pairgoth.pairing.solver package org.jeudego.pairgoth.pairing.solver
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.pairing.HistoryHelper
import java.util.* import java.util.*
class SwissSolver(round: Int, class SwissSolver(round: Int,
totalRounds: Int, totalRounds: Int,
history: List<List<Game>>, history: HistoryHelper,
pairables: List<Pairable>, pairables: List<Pairable>,
pairablesMap: Map<ID, Pairable>, pairablesMap: Map<ID, Pairable>,
pairingParams: PairingParams, pairingParams: PairingParams,
placementParams: PlacementParams, placementParams: PlacementParams,
usedTables: BitSet 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<ID, Double> get() = scores.mapValues { it.value.second } override val scoresX: Map<ID, Double> get() = scores.mapValues { it.value.second }
override val mainLimits = Pair(0.0, round - 1.0) override val mainLimits = Pair(0.0, round - 1.0)