Early rounding of MMS; use player base score for non-played rounds SOS

This commit is contained in:
Claude Brisson
2024-03-10 13:51:03 +01:00
parent 6f658295a9
commit 7bb0016d63
6 changed files with 61 additions and 36 deletions

View File

@@ -4,33 +4,48 @@ import com.republicate.kson.Json
import org.jeudego.pairgoth.model.Criterion import org.jeudego.pairgoth.model.Criterion
import org.jeudego.pairgoth.model.MacMahon import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairable.Companion.MIN_RANK
import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID 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.ceil
import kotlin.math.floor
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
// TODO CB avoid code redundancy with solvers // TODO CB avoid code redundancy with solvers
fun Tournament<*>.mmBase(pairable: Pairable): Double {
if (pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon")
return min(max(pairable.rank, pairing.mmFloor), pairing.mmBar) + MacMahonSolver.mmsZero + pairable.mmsCorrection
}
fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> { fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
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 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 ceil(score - epsilon)
}
val historyHelper = HistoryHelper(historyBefore(round + 1)) { val historyHelper = HistoryHelper(historyBefore(round + 1)) {
if (pairing.type == PairingType.SWISS) wins if (pairing.type == PairingType.SWISS) wins.mapValues { Pair(0.0, it.value) }
else pairables.mapValues { else pairables.mapValues {
it.value.let { it.value.let { pairable ->
pairable -> val mmBase = pairable.mmBase()
mmBase(pairable) + Pair(
mmBase,
roundScore(mmBase +
(nbW(pairable) ?: 0.0) + // TODO take tournament parameter into account (nbW(pairable) ?: 0.0) + // TODO take tournament parameter into account
(1..round).map { round -> (1..round).map { round ->
if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0 else 1 if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0 else 1
}.sum() * pairing.pairingParams.main.mmsValueAbsent }.sum() * pairing.pairingParams.main.mmsValueAbsent)
)
} }
} }
} }

View File

@@ -11,7 +11,7 @@ abstract class BasePairingHelper(
val placement: PlacementParams, val placement: PlacementParams,
) { ) {
abstract val scores: Map<ID, Double> abstract val scores: Map<ID, Pair<Double, Double>>
val historyHelper = if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history) { scores } val historyHelper = if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history) { scores }
else HistoryHelper(history) { scores } else HistoryHelper(history) { scores }
@@ -19,7 +19,7 @@ abstract class BasePairingHelper(
// The main criterion that will be used to define the groups should be defined by subclasses // 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 // SOS and variants will be computed based on this score
val Pairable.main: Double get() = scores[id] ?: 0.0 val Pairable.main: Double get() = scores[id]?.second ?: 0.0
abstract val mainLimits: Pair<Double, Double> abstract val mainLimits: Pair<Double, Double>
// pairables sorted using overloadable sort function // pairables sorted using overloadable sort function

View File

@@ -3,7 +3,7 @@ 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.*
open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter: HistoryHelper.()-> Map<ID, Double>) { open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter: HistoryHelper.()-> Map<ID, Pair<Double, Double>>) {
// List of all the pairables ID present in the history // List of all the pairables ID present in the history
val allPairables = history.flatten() val allPairables = history.flatten()
@@ -95,25 +95,31 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
} }
// define mms to be a synonym of scores // define mms to be a synonym of scores
val mms by lazy { scores } val mms by lazy { scores.mapValues { it -> it.value.second } }
// SOS related functions given a score function // SOS related functions given a score function
val sos by lazy { val sos by lazy {
(history.flatten().map { game -> (history.flatten().map { game ->
Pair(game.black, scores[game.white] ?: 0.0) Pair(game.black, scores[game.white]?.second ?: 0.0)
} + history.flatten().map { game -> } + history.flatten().map { game ->
Pair(game.white, scores[game.black] ?: 0.0) Pair(game.white, scores[game.black]?.second ?: 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next -> }).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second acc + next.second
}.mapValues { (id, score) ->
// "If the player does not participate in a round, the opponent's score is replaced by the starting score of the player himself."
score + playersPerRound.map { players ->
if (players.contains(id)) 0.0
else scores[id]?.first ?: 0.0
}.sum()
} }
} }
// sos-1 // sos-1
val sosm1 by lazy { val sosm1 by lazy {
(history.flatten().map { game -> (history.flatten().map { game ->
Pair(game.black, scores[game.white] ?: 0.0) Pair(game.black, scores[game.white]?.second ?: 0.0)
} + history.flatten().map { game -> } + history.flatten().map { game ->
Pair(game.white, scores[game.black] ?: 0.0) Pair(game.white, scores[game.black]?.second ?: 0.0)
}).groupBy { }).groupBy {
it.first it.first
}.mapValues { }.mapValues {
@@ -125,9 +131,9 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
// sos-2 // sos-2
val sosm2 by lazy { val sosm2 by lazy {
(history.flatten().map { game -> (history.flatten().map { game ->
Pair(game.black, scores[game.white] ?: 0.0) Pair(game.black, scores[game.white]?.second ?: 0.0)
} + history.flatten().map { game -> } + history.flatten().map { game ->
Pair(game.white, scores[game.black] ?: 0.0) Pair(game.white, scores[game.black]?.second ?: 0.0)
}).groupBy { }).groupBy {
it.first it.first
}.mapValues { }.mapValues {
@@ -141,11 +147,11 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
(history.flatten().filter { game -> (history.flatten().filter { game ->
game.white != 0 // Remove games against byePlayer game.white != 0 // Remove games against byePlayer
}.map { game -> }.map { game ->
Pair(game.black, if (game.result == Game.Result.BLACK) scores[game.white] ?: 0.0 else 0.0) Pair(game.black, if (game.result == Game.Result.BLACK) scores[game.white]?.second ?: 0.0 else 0.0)
} + history.flatten().filter { game -> } + history.flatten().filter { game ->
game.white != 0 // Remove games against byePlayer game.white != 0 // Remove games against byePlayer
}.map { game -> }.map { game ->
Pair(game.white, if (game.result == Game.Result.WHITE) scores[game.black] ?: 0.0 else 0.0) Pair(game.white, if (game.result == Game.Result.WHITE) scores[game.black]?.second ?: 0.0 else 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next -> }).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second acc + next.second
} }
@@ -202,7 +208,7 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
// 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 // 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, Double>): class TeamOfIndividualsHistoryHelper(history: List<List<Game>>, scoresGetter: () -> Map<ID, Pair<Double, Double>>):
HistoryHelper(history, { scoresGetter() }) { HistoryHelper(history, { scoresGetter() }) {
private fun Pairable.asTeam() = this as TeamTournament.Team private fun Pairable.asTeam() = this as TeamTournament.Team

View File

@@ -496,11 +496,11 @@ sealed class BaseSolver(
return pairable.rank return pairable.rank
} }
fun roundScore(score: Double): Int { fun roundScore(score: Double): Double {
val epsilon = 0.00001 val epsilon = 0.00001
// Note: this works for now because we only have .0 and .5 fractional parts // Note: this works for now because we only have .0 and .5 fractional parts
return if (pairing.main.roundDownScore) floor(score + epsilon).roundToInt() return if (pairing.main.roundDownScore) floor(score + epsilon)
else ceil(score - epsilon).roundToInt() else ceil(score - epsilon)
} }
open fun HandicapParams.clamp(input: Int): Int { open fun HandicapParams.clamp(input: Int): Int {

View File

@@ -4,6 +4,7 @@ import org.jeudego.pairgoth.model.*
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt
class MacMahonSolver(round: Int, class MacMahonSolver(round: Int,
history: List<List<Game>>, history: List<List<Game>>,
@@ -11,31 +12,33 @@ class MacMahonSolver(round: Int,
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, history, pairables, pairingParams, placementParams, usedTables) { BaseSolver(round, history, pairables, pairingParams, placementParams, usedTables) {
override val scores: Map<ID, Double> by lazy { override val scores: Map<ID, Pair<Double, Double>> by lazy {
require (mmBar > mmFloor) { "MMFloor is higher than MMBar" } require (mmBar > mmFloor) { "MMFloor is higher than MMBar" }
val pairing = pairables.map { it.id }.toSet() val pairing = pairables.map { it.id }.toSet()
pairablesMap.mapValues { pairablesMap.mapValues {
it.value.let { pairable -> it.value.let { pairable ->
pairable.mmBase + Pair(
pairable.mmBase,
roundScore(pairable.mmBase +
pairable.nbW + // TODO take tournament parameter into account pairable.nbW + // TODO take tournament parameter into account
pairable.missedRounds(round, pairing) * pairingParams.main.mmsValueAbsent pairable.missedRounds(round, pairing) * pairingParams.main.mmsValueAbsent))
} }
} }
} }
override fun HandicapParams.pseudoRank(pairable: Pairable): Int { override fun HandicapParams.pseudoRank(pairable: Pairable): Int {
if (useMMS) { if (useMMS) {
return roundScore(pairable.mms + Pairable.MIN_RANK) return pairable.mms.roundToInt()
} else { } else {
return pairable.rank return pairable.rank
} }
} }
val Pairable.mmBase: Double get() = min(max(rank, mmFloor), mmBar) + mmsZero + mmsCorrection val Pairable.mmBase: Double get() = min(max(rank, mmFloor), mmBar) + mmsZero + mmsCorrection
val Pairable.mms: Double get() = scores[id] ?: 0.0 val Pairable.mms: Double get() = scores[id]?.second ?: 0.0
// CB TODO - configurable criteria // CB TODO - configurable criteria
val mainScoreMin = mmFloor + PLA_SMMS_SCORE_MIN - Pairable.MIN_RANK val mainScoreMin = mmFloor + PLA_SMMS_SCORE_MIN - Pairable.MIN_RANK

View File

@@ -15,7 +15,8 @@ class SwissSolver(round: Int,
// In a Swiss tournament the main criterion is the number of wins and already computed // In a Swiss tournament the main criterion is the number of wins and already computed
override val scores by lazy { override val scores by lazy {
historyHelper.wins historyHelper.wins.mapValues {
Pair(0.0, it.value) }
} }
// //
// get() by lazy { historyHelper.wins } // get() by lazy { historyHelper.wins }