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.MacMahon
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.Tournament
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.ceil
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
// 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 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)) {
if (pairing.type == PairingType.SWISS) wins
if (pairing.type == PairingType.SWISS) wins.mapValues { Pair(0.0, it.value) }
else pairables.mapValues {
it.value.let {
pairable ->
mmBase(pairable) +
it.value.let { pairable ->
val mmBase = pairable.mmBase()
Pair(
mmBase,
roundScore(mmBase +
(nbW(pairable) ?: 0.0) + // TODO take tournament parameter into account
(1..round).map { round ->
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,
) {
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 }
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
// 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>
// 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.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
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
val mms by lazy { scores }
val mms by lazy { scores.mapValues { it -> it.value.second } }
// SOS related functions given a score function
val sos by lazy {
(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 ->
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 ->
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
val sosm1 by lazy {
(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 ->
Pair(game.white, scores[game.black] ?: 0.0)
Pair(game.white, scores[game.black]?.second ?: 0.0)
}).groupBy {
it.first
}.mapValues {
@@ -125,9 +131,9 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
// sos-2
val sosm2 by lazy {
(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 ->
Pair(game.white, scores[game.black] ?: 0.0)
Pair(game.white, scores[game.black]?.second ?: 0.0)
}).groupBy {
it.first
}.mapValues {
@@ -141,11 +147,11 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
(history.flatten().filter { game ->
game.white != 0 // Remove games against byePlayer
}.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 ->
game.white != 0 // Remove games against byePlayer
}.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 ->
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
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() }) {
private fun Pairable.asTeam() = this as TeamTournament.Team

View File

@@ -496,11 +496,11 @@ sealed class BaseSolver(
return pairable.rank
}
fun roundScore(score: Double): Int {
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.main.roundDownScore) floor(score + epsilon).roundToInt()
else ceil(score - epsilon).roundToInt()
return if (pairing.main.roundDownScore) floor(score + epsilon)
else ceil(score - epsilon)
}
open fun HandicapParams.clamp(input: Int): Int {

View File

@@ -4,6 +4,7 @@ import org.jeudego.pairgoth.model.*
import java.util.*
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
class MacMahonSolver(round: Int,
history: List<List<Game>>,
@@ -11,31 +12,33 @@ class MacMahonSolver(round: Int,
pairingParams: PairingParams,
placementParams: PlacementParams,
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) {
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" }
val pairing = pairables.map { it.id }.toSet()
pairablesMap.mapValues {
it.value.let { pairable ->
pairable.mmBase +
Pair(
pairable.mmBase,
roundScore(pairable.mmBase +
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 {
if (useMMS) {
return roundScore(pairable.mms + Pairable.MIN_RANK)
return pairable.mms.roundToInt()
} else {
return pairable.rank
}
}
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
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
override val scores by lazy {
historyHelper.wins
historyHelper.wins.mapValues {
Pair(0.0, it.value) }
}
//
// get() by lazy { historyHelper.wins }