From e20e8330ea126714fb045bc2fa4c5b501f92e2c2 Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Mon, 23 Oct 2023 11:20:54 +0200 Subject: [PATCH] =?UTF-8?q?Proposition=20de=20refactor=20pour=20les=20solv?= =?UTF-8?q?ers=20avec=20s=C3=A9paration=20de=20l'extension=20des=20Pairabl?= =?UTF-8?q?es=20et=20du=20calcul=20des=20poids.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jeudego/pairgoth/model/Pairing.kt | 4 +- .../pairgoth/pairing/BasePairingHelper.kt | 130 ++++++++++++++ .../org/jeudego/pairgoth/pairing/README.md | 5 + .../org/jeudego/pairgoth/pairing/Random.kt | 22 +++ .../{Solver.kt => solver/BaseSolver.kt} | 161 ++---------------- .../pairing/{ => solver}/MacMahonSolver.kt | 4 +- .../pairing/{ => solver}/SwissSolver.kt | 5 +- 7 files changed, 175 insertions(+), 156 deletions(-) create mode 100644 api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt create mode 100644 api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Random.kt rename api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/{Solver.kt => solver/BaseSolver.kt} (77%) rename api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/{ => solver}/MacMahonSolver.kt (88%) rename api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/{ => solver}/SwissSolver.kt (78%) 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 37346df..e5ee98a 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 @@ -4,8 +4,8 @@ import com.republicate.kson.Json import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.SPLIT_AND_SLIP import org.jeudego.pairgoth.model.PairingType.* -import org.jeudego.pairgoth.pairing.MacMahonSolver -import org.jeudego.pairgoth.pairing.SwissSolver +import org.jeudego.pairgoth.pairing.solver.MacMahonSolver +import org.jeudego.pairgoth.pairing.solver.SwissSolver // base pairing parameters data class BaseCritParams( 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 new file mode 100644 index 0000000..abdbd57 --- /dev/null +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt @@ -0,0 +1,130 @@ +package org.jeudego.pairgoth.pairing + +import org.jeudego.pairgoth.model.* + +abstract class BasePairingHelper( + history: List>, // History of all games played for each round + var pairables: List, // All pairables for this round, it may include the bye player + val pairing: PairingParams, + val placement: PlacementParams, + ) { + + abstract val scores: Map + val historyHelper = if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history) { scores } + else HistoryHelper(history) { scores } + + // The main criterion that will be used to define the groups should be defined by subclasses + // SOS and variants will be computed based on this score + val Pairable.main: Double get() = scores[id] ?: 0.0 + abstract val mainLimits: Pair + + // pairables sorted using overloadable sort function + protected val sortedPairables by lazy { + pairables.sortedWith(::sort) + } + // pairables sorted for pairing purposes + protected val pairingSortedPairables by lazy { + pairables.sortedWith(::pairingSort) + } + // pairables sorted for pairing purposes + protected val nameSortedPairables by lazy { + pairables.sortedWith(::nameSort) + } + + protected val pairablesMap by lazy { + pairables.associateBy { it.id } + } + + // Generic parameters calculation + //private val standingScore by lazy { computeStandingScore() } + + // Decide each pairable group based on the main criterion + protected val groupsCount get() = 1 + (mainLimits.second - mainLimits.first).toInt() + private val _groups by lazy { + pairables.associate { pairable -> Pair(pairable.id, pairable.main.toInt()) } + } + + // place (among sorted pairables) + val Pairable.place: Int get() = _place[id]!! + private val _place by lazy { + pairingSortedPairables.mapIndexed { index, pairable -> + Pair(pairable.id, index) + }.toMap() + } + + // placeInGroup (of same score) : Pair(place, groupSize) + protected val Pairable.placeInGroup: Pair get() = _placeInGroup[id]!! + private val _placeInGroup by lazy { + // group by group number + pairingSortedPairables.groupBy { + it.group + // get a list { id { placeInGroup, groupSize } } + }.values.flatMap { group -> + group.mapIndexed { index, pairable -> + Pair(pairable.id, Pair(index, group.size)) + } + // get a map id -> { placeInGroup, groupSize } + }.toMap() + } + + // already paired players map + protected fun Pairable.played(other: Pairable) = historyHelper.playedTogether(this, other) + + // color balance (nw - nb) + protected val Pairable.colorBalance: Int get() = historyHelper.colorBalance(this) ?: 0 + + protected val Pairable.group: Int get() = _groups[id]!! + + // score (number of wins) + val Pairable.nbW: Double get() = historyHelper.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 + + fun Pairable.eval(criterion: Criterion) = evalCriterion(this, criterion) + open fun evalCriterion(pairable: Pairable, criterion: Criterion) = when (criterion) { + Criterion.NONE -> 0.0 + Criterion.CATEGORY -> TODO() + Criterion.RANK -> pairable.rank.toDouble() + Criterion.RATING -> pairable.rating.toDouble() + Criterion.NBW -> pairable.nbW + Criterion.SOSW -> pairable.sos + Criterion.SOSWM1 -> pairable.sosm1 + Criterion.SOSWM2 -> pairable.sosm2 + Criterion.SOSOSW -> pairable.sosos + Criterion.SODOSW -> pairable.sodos + Criterion.CUSSW -> pairable.cums + else -> throw Error("criterion cannot be evaluated: ${criterion.name}") + } + + open fun sort(p: Pairable, q: Pairable): Int { + for (criterion in placement.criteria) { + val criterionP = p.eval(criterion) + val criterionQ = q.eval(criterion) + if (criterionP != criterionQ) { + return (criterionQ * 100 - criterionP * 100).toInt() + } + } + return 0 + } + open fun pairingSort(p: Pairable, q: Pairable): Int { + for (criterion in placement.criteria) { + val criterionP = p.eval(criterion) + val criterionQ = q.eval(criterion) + if (criterionP != criterionQ) { + return (criterionQ * 1e6 - criterionP * 1e6).toInt() + } + } + if (p.rating == q.rating) { + return if (p.name > q.name) 1 else -1 + } + return q.rating - p.rating + } + open fun nameSort(p: Pairable, q: Pairable): Int { + return if (p.name > q.name) 1 else -1 + } +} \ No newline at end of file diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/README.md b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/README.md index 5afb35d..520714b 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/README.md +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/README.md @@ -1,3 +1,8 @@ +# File hierarchy +- `HistoryHelper.kt` computes all the criterion for pairings and standings like number of wins, colors, sos, etc +- `BasePairingHelper.kt` extends the `Pairable` objects with attributes corresponding to these criteria. This class plays the role of the `ScoredPlayer` in OpenGotha. +- `solver` folder contains the actual solver base class and the concrete solvers for different tournaments type (Swiss, MacMahon, etc). + # Weights internal name ## Base criteria - avoiddup diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Random.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Random.kt new file mode 100644 index 0000000..91a2d32 --- /dev/null +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Random.kt @@ -0,0 +1,22 @@ +package org.jeudego.pairgoth.pairing + +import org.jeudego.pairgoth.model.Pairable + +fun detRandom(max: Double, p1: Pairable, p2: Pairable): Double { + var inverse = false + var name1 = p1.nameSeed("") + var name2 = p2.nameSeed("") + if (name1 > name2) { + name1 = name2.also { name2 = name1 } + inverse = true + } + var nR = "$name1$name2".mapIndexed { i, c -> + c.code.toDouble() * (i + 1) + }.sum() * 1234567 % (max + 1) + if (inverse) nR = max - nR + return nR +} + +fun nonDetRandom(max: Double) = + if (max == 0.0) 0.0 + else Math.random() * (max + 1.0) \ No newline at end of file diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt similarity index 77% rename from api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt rename to api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt index 6d26e69..a55903c 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt @@ -1,8 +1,10 @@ -package org.jeudego.pairgoth.pairing +package org.jeudego.pairgoth.pairing.solver import org.jeudego.pairgoth.model.* -import org.jeudego.pairgoth.model.Criterion.* import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.* +import org.jeudego.pairgoth.pairing.BasePairingHelper +import org.jeudego.pairgoth.pairing.detRandom +import org.jeudego.pairgoth.pairing.nonDetRandom import org.jeudego.pairgoth.store.Store import org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching import org.jgrapht.alg.matching.blossom.v5.ObjectiveSense @@ -16,88 +18,20 @@ import kotlin.math.abs import kotlin.math.max import kotlin.math.min -fun detRandom(max: Double, p1: Pairable, p2: Pairable): Double { - var inverse = false - var name1 = p1.nameSeed("") - var name2 = p2.nameSeed("") - if (name1 > name2) { - name1 = name2.also { name2 = name1 } - inverse = true - } - var nR = "$name1$name2".mapIndexed { i, c -> - c.code.toDouble() * (i + 1) - }.sum() * 1234567 % (max + 1) - if (inverse) nR = max - nR - return nR -} - -private fun nonDetRandom(max: Double) = - if (max == 0.0) 0.0 - else Math.random() * (max + 1.0) - -sealed class Solver( +sealed class BaseSolver( val round: Int, // Round number - val history: List>, // History of all games played for each round - var pairables: List, // All pairables for this round, it may include the bye player - val pairing: PairingParams, - val placement: PlacementParams, + history: List>, // History of all games played for each round + pairables: List, // All pairables for this round, it may include the bye player + pairing: PairingParams, + placement: PlacementParams, val forcedBye: Pairable? = null, // This parameter is non-null to force the given pairable to be chosen as a bye player. - ) { + ) : BasePairingHelper(history, pairables, pairing, placement) { companion object { val rand = Random(/* seed from properties - TODO */) val DEBUG_EXPORT_WEIGHT = true } - abstract val scores: Map - val historyHelper = if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history) { scores } - else HistoryHelper(history) { scores } - - // pairables sorted using overloadable sort function - private val sortedPairables by lazy { - pairables.sortedWith(::sort) - } - // pairables sorted for pairing purposes - private val pairingSortedPairables by lazy { - pairables.sortedWith(::pairingSort) - } - // pairables sorted for pairing purposes - private val nameSortedPairables by lazy { - pairables.sortedWith(::nameSort) - } - - protected val pairablesMap by lazy { - pairables.associateBy { it.id } - } - - open fun sort(p: Pairable, q: Pairable): Int { - for (criterion in placement.criteria) { - val criterionP = p.eval(criterion) - val criterionQ = q.eval(criterion) - if (criterionP != criterionQ) { - return (criterionQ * 100 - criterionP * 100).toInt() - } - } - return 0 - } - open fun pairingSort(p: Pairable, q: Pairable): Int { - for (criterion in placement.criteria) { - val criterionP = p.eval(criterion) - val criterionQ = q.eval(criterion) - if (criterionP != criterionQ) { - return (criterionQ * 1e6 - criterionP * 1e6).toInt() - } - } - if (p.rating == q.rating) { - return if (p.name > q.name) 1 else -1 - } - return q.rating - p.rating - - } - open fun nameSort(p: Pairable, q: Pairable): Int { - return if (p.name > q.name) 1 else -1 - } - open fun openGothaWeight(p1: Pairable, p2: Pairable) = 1.0 + // 1 is minimum value because 0 means "no matching allowed" pairing.base.apply(p1, p2) + @@ -109,11 +43,6 @@ sealed class Solver( openGothaWeight(p1, p2) + pairing.handicap.color(p1, p2) - // The main criterion that will be used to define the groups should be defined by subclasses - val Pairable.main: Double get() = scores[id] ?: 0.0 - abstract val mainLimits: Pair - // SOS and variants will be computed based on this score - fun pair(): List { // The byeGame is a list of one game with the bye player or an empty list val byeGame: List = if (pairables.size % 2 != 0) { @@ -189,11 +118,11 @@ sealed class Solver( } fun chooseByePlayer(): Pairable { + // TODO https://github.com/lucvannier/opengotha/blob/master/src/info/vannier/gotha/Tournament.java#L1471 return ByePlayer } - // base criteria - + // Base criteria open fun BaseCritParams.apply(p1: Pairable, p2: Pairable): Double { var score = 0.0 // Base Criterion 1 : Avoid Duplicating Game @@ -524,70 +453,4 @@ sealed class Solver( // CB TODO team of individuals pairing return listOf(Game(id = Store.nextGameId, black = black.id, white = white.id, handicap = pairing.handicap.handicap(black, white))) } - - // Generic parameters calculation - //private val standingScore by lazy { computeStandingScore() } - - // Decide each pairable group based on the main criterion - private val groupsCount get() = 1 + (mainLimits.second - mainLimits.first).toInt() - private val _groups by lazy { - pairables.associate { pairable -> Pair(pairable.id, pairable.main.toInt()) } - } - - // place (among sorted pairables) - val Pairable.place: Int get() = _place[id]!! - private val _place by lazy { - pairingSortedPairables.mapIndexed { index, pairable -> - Pair(pairable.id, index) - }.toMap() - } - - // placeInGroup (of same score) : Pair(place, groupSize) - private val Pairable.placeInGroup: Pair get() = _placeInGroup[id]!! - private val _placeInGroup by lazy { - // group by group number - pairingSortedPairables.groupBy { - it.group - // get a list { id { placeInGroup, groupSize } } - }.values.flatMap { group -> - group.mapIndexed { index, pairable -> - Pair(pairable.id, Pair(index, group.size)) - } - // get a map id -> { placeInGroup, groupSize } - }.toMap() - } - - // already paired players map - private fun Pairable.played(other: Pairable) = historyHelper.playedTogether(this, other) - - // color balance (nw - nb) - private val Pairable.colorBalance: Int get() = historyHelper.colorBalance(this) ?: 0 - - private val Pairable.group: Int get() = _groups[id]!! - - // score (number of wins) - val Pairable.nbW: Double get() = historyHelper.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 - - fun Pairable.eval(criterion: Criterion) = evalCriterion(this, criterion) - open fun evalCriterion(pairable: Pairable, criterion: Criterion) = when (criterion) { - NONE -> 0.0 - CATEGORY -> TODO() - RANK -> pairable.rank.toDouble() - RATING -> pairable.rating.toDouble() - NBW -> pairable.nbW - SOSW -> pairable.sos - SOSWM1 -> pairable.sosm1 - SOSWM2 -> pairable.sosm2 - SOSOSW -> pairable.sosos - SODOSW -> pairable.sodos - CUSSW -> pairable.cums - else -> throw Error("criterion cannot be evaluated: ${criterion.name}") - } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt similarity index 88% rename from api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt rename to api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt index 751b336..17ccb57 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt @@ -1,4 +1,4 @@ -package org.jeudego.pairgoth.pairing +package org.jeudego.pairgoth.pairing.solver import org.jeudego.pairgoth.model.* @@ -7,7 +7,7 @@ class MacMahonSolver(round: Int, pairables: List, pairingParams: PairingParams, placementParams: PlacementParams): - Solver(round, history, pairables, pairingParams, placementParams) { + BaseSolver(round, history, pairables, pairingParams, placementParams) { override val scores: Map by lazy { historyHelper.wins.mapValues { diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt similarity index 78% rename from api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt rename to api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt index d4dc7b7..9a0065d 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/SwissSolver.kt @@ -1,14 +1,13 @@ -package org.jeudego.pairgoth.pairing +package org.jeudego.pairgoth.pairing.solver import org.jeudego.pairgoth.model.* -import kotlin.properties.Delegates class SwissSolver(round: Int, history: List>, pairables: List, pairingParams: PairingParams, placementParams: PlacementParams): - Solver(round, history, pairables, pairingParams, placementParams) { + BaseSolver(round, history, pairables, pairingParams, placementParams) { // In a Swiss tournament the main criterion is the number of wins and already computed