Refactoring criterion computation

This commit is contained in:
Theo Barollet
2023-06-12 15:51:52 +02:00
parent 2378c51a05
commit db456a1a1b
6 changed files with 266 additions and 112 deletions

View File

@@ -35,7 +35,7 @@ private const val MA_MAX_MAXIMIZE_SEEDING: Double = MA_MAX_MINIMIZE_SCORE_DIFFER
enum class SeedMethod { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
sealed class Pairing(val type: PairingType, val pairingParams: PairingParams = PairingParams()) {
sealed class Pairing(val type: PairingType, val pairingParams: PairingParams = PairingParams(), val placementParams: PlacementParams) {
companion object {}
enum class PairingType { SWISS, MAC_MAHON, ROUND_ROBIN }
data class PairingParams(
@@ -61,9 +61,8 @@ sealed class Pairing(val type: PairingType, val pairingParams: PairingParams = P
val maLastRoundForSeedSystem1: Int = 1,
val maSeedSystem1: SeedMethod = SeedMethod.SPLIT_AND_RANDOM,
val maSeedSystem2: SeedMethod = SeedMethod.SPLIT_AND_FOLD,
// TODO get these parameters from Placement parameters
//val maAdditionalPlacementCritSystem1: Int = PlacementParameterSet.PLA_CRIT_RATING,
//val maAdditionalPlacementCritSystem2: Int = PlacementParameterSet.PLA_CRIT_NUL,
val maAdditionalPlacementCritSystem1: PlacementCriterion = PlacementCriterion.RATING,
val maAdditionalPlacementCritSystem2: PlacementCriterion = PlacementCriterion.NULL,
// Secondary criteria
val seBarThresholdActive: Boolean = true, // Do not apply secondary criteria for players above bar
@@ -127,9 +126,9 @@ class Swiss(): Pairing(SWISS, PairingParams(
geo = GeographicalParams.disabled(),
hd = HandicapParams.disabled(),
)) {
), PlacementParams(PlacementCriterion.NBW, PlacementCriterion.SOSW, PlacementCriterion.SOSOSW)) {
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
return SwissSolver(tournament.historyBefore(round), pairables, pairingParams).pair()
return SwissSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams).pair()
}
}
@@ -137,15 +136,16 @@ class MacMahon(
var bar: Int = 0,
var minLevel: Int = -30,
var reducer: Int = 1
): Pairing(MAC_MAHON, PairingParams(seDefSecCrit = MA_MAX_MINIMIZE_SCORE_DIFFERENCE)) {
): Pairing(MAC_MAHON, PairingParams(seDefSecCrit = MA_MAX_MINIMIZE_SCORE_DIFFERENCE),
PlacementParams(PlacementCriterion.MMS, PlacementCriterion.SOSM, PlacementCriterion.SOSOSM)) {
val groups = mutableListOf<Int>()
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
return MacMahonSolver(tournament.historyBefore(round), pairables, pairingParams, mmBase = minLevel, mmBar = bar, reducer = reducer).pair()
return MacMahonSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams, mmBase = minLevel, mmBar = bar, reducer = reducer).pair()
}
}
class RoundRobin: Pairing(ROUND_ROBIN) {
class RoundRobin: Pairing(ROUND_ROBIN, PairingParams(), PlacementParams(PlacementCriterion.NBW, PlacementCriterion.RATING)) {
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
TODO()
}

View File

@@ -0,0 +1,58 @@
package org.jeudego.pairgoth.model
enum class PlacementCriterion {
NULL, // No ranking/tie-break
CATEGORY,
RANK,
RATING,
NBW, // Number win
MMS, // Macmahon score
STS, // Strasbourg score
CPS, // Cup score
SOSW, // Sum of opponents NBW
SOSWM1, //-1
SOSWM2, //-2
SODOSW, // Sum of defeated opponents NBW
SOSOSW, // Sum of opponenent SOS
CUSSW, // Cumulative sum of scores (NBW)
SOSM, // Sum of opponents McMahon score
SOSMM1, // Same as previous group but with McMahon score
SOSMM2,
SODOSM,
SOSOSM,
CUSSM,
SOSTS, // Sum of opponnents Strasbourg score
EXT, // Exploits tentes
EXR, // Exploits reussis
// For the two criteria below see the user documentation
SDC, // Simplified direct confrontation
DC, // Direct confrontation
}
class PlacementParams(vararg criteria: PlacementCriterion) {
companion object {
const val MAX_NUMBER_OF_CRITERIA: Int = 6
}
private fun addNullCriteria(criteria: Array<out PlacementCriterion>): ArrayList<PlacementCriterion> {
var criteria = arrayListOf(*criteria)
while (criteria.size < MAX_NUMBER_OF_CRITERIA) {
criteria.add(PlacementCriterion.NULL)
}
return criteria
}
val criteria = addNullCriteria(criteria)
open fun checkWarnings(): String {
// Returns a warning message if criteria are incoherent
// TODO
return ""
}
}

View File

@@ -1,17 +1,30 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.*
open class HistoryHelper(protected val history: List<Game>) {
open class HistoryHelper(protected val history: List<Game>, score: Map<ID, Double>) {
fun getCriterionValue(p: Pairable, crit: PlacementCriterion): Int {
// Returns generic criterion
// Specific criterion are computed by solvers directly
return when (crit) {
PlacementCriterion.NULL -> 0
PlacementCriterion.CATEGORY -> TODO()
PlacementCriterion.RANK -> p.rank
PlacementCriterion.RATING -> p.rating
PlacementCriterion.EXT -> TODO()
PlacementCriterion.EXR -> TODO()
PlacementCriterion.SDC -> TODO()
PlacementCriterion.DC -> TODO()
else -> -1
}
}
// Generic helper functions
open fun playedTogether(p1: Pairable, p2: Pairable) = paired.contains(Pair(p1.id, p2.id))
open fun colorBalance(p: Pairable) = colorBalance[p.id]
open fun score(p: Pairable) = score[p.id]
open fun sos(p: Pairable) = sos[p.id]
open fun sosos(p: Pairable) = sosos[p.id]
open fun sodos(p: Pairable) = sodos[p.id]
open fun nbW(p: Pairable) = numberWins[p.id]
protected val paired: Set<Pair<Int, Int>> by lazy {
(history.map { game ->
@@ -34,7 +47,7 @@ open class HistoryHelper(protected val history: List<Game>) {
}
}
private val score: Map<Int, Double> by lazy {
val numberWins: Map<Int, Double> by lazy {
mutableMapOf<Int, Double>().apply {
history.forEach { game ->
when (game.result) {
@@ -50,7 +63,8 @@ open class HistoryHelper(protected val history: List<Game>) {
}
}
private val sos by lazy {
// SOS related functions given a score function
val sos by lazy {
(history.map { game ->
Pair(game.black, score[game.white] ?: 0.0)
} + history.map { game ->
@@ -60,17 +74,18 @@ open class HistoryHelper(protected val history: List<Game>) {
}
}
private val sosos by lazy {
(history.map { game ->
Pair(game.black, sos[game.white] ?: 0.0)
} + history.map { game ->
Pair(game.white, sos[game.black] ?: 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
}
// sos-1
val sosm1: Map<ID, Double> by lazy {
TODO()
}
private val sodos by lazy {
// sos-2
val sosm2: Map<ID, Double> by lazy {
TODO()
}
// sodos
val sodos by lazy {
(history.map { game ->
Pair(game.black, if (game.result == Game.Result.BLACK) score[game.white] ?: 0.0 else 0.0)
} + history.map { game ->
@@ -80,12 +95,27 @@ open class HistoryHelper(protected val history: List<Game>) {
}
}
// sosos
val sosos by lazy {
(history.map { game ->
Pair(game.black, sos[game.white] ?: 0.0)
} + history.map { game ->
Pair(game.white, sos[game.black] ?: 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
}
}
// cumulative score
val cumscore: Map<ID, Double> by lazy {
TODO()
}
}
// 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<Game>): HistoryHelper(history) {
class TeamOfIndividualsHistoryHelper(history: List<Game>, score: Map<ID, Double>):
HistoryHelper(history, score) {
private fun Pairable.asTeam() = this as TeamTournament.Team
@@ -93,8 +123,8 @@ class TeamOfIndividualsHistoryHelper(history: List<Game>): HistoryHelper(history
(p2.asTeam()).playerIds.map {Pair(it, id) }
}.toSet()).isNotEmpty()
override fun score(p: Pairable) = p.asTeam().teamPlayers.map { super.score(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
override fun sos(p:Pairable) = p.asTeam().teamPlayers.map { super.sos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
override fun sosos(p:Pairable) = p.asTeam().teamPlayers.map { super.sosos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
override fun sodos(p:Pairable) = p.asTeam().teamPlayers.map { super.sodos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
override fun nbW(p: Pairable) = p.asTeam().teamPlayers.map { super.nbW(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
//override fun sos(p:Pairable) = p.asTeam().teamPlayers.map { super.sos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
//override fun sosos(p:Pairable) = p.asTeam().teamPlayers.map { super.sosos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
//override fun sodos(p:Pairable) = p.asTeam().teamPlayers.map { super.sodos(it) ?: throw Error("unknown player id: #${it.id}") }.sum()
}

View File

@@ -1,16 +1,14 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.*
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.roundToInt
import kotlin.math.sign
class MacMahonSolver(history: List<Game>, pairables: List<Pairable>, pairingParams: Pairing.PairingParams, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(history, pairables, pairingParams) {
class MacMahonSolver(round: Int, history: List<Game>, pairables: List<Pairable>, pairingParams: Pairing.PairingParams, placementParams: PlacementParams, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(round, history, pairables, pairingParams, placementParams) {
val Pairable.mms get() = mmBase + score
val Pairable.mms get() = mmBase + nbW // TODO real calculation
// CB TODO - configurable criteria
override fun mainCriterion(p1: Pairable): Int {
@@ -20,10 +18,23 @@ class MacMahonSolver(history: List<Game>, pairables: List<Pairable>, pairingPara
override fun mainCriterionMinMax(): Pair<Int, Int> {
TODO("Not yet implemented")
}
override fun sort(p: Pairable, q: Pairable): Int =
if (p.mms != q.mms) ((q.mms - p.mms) * 1000).toInt()
else if (p.sos != q.sos) ((q.sos - p.sos) * 1000).toInt()
else if (p.sosos != q.sosos) ((q.sosos - p.sosos) * 1000).toInt()
else 0
override fun computeStandingScore(): Map<ID, Double> {
TODO("Not yet implemented")
}
override fun getSpecificCriterionValue(p: Pairable, criterion: PlacementCriterion): Int {
// TODO solve this double/int conflict
return when (criterion) {
PlacementCriterion.MMS -> TODO()
PlacementCriterion.SOSM -> p.sos.toInt()
PlacementCriterion.SOSMM1 -> p.sosm1.toInt()
PlacementCriterion.SOSMM2 -> p.sosm2.toInt()
PlacementCriterion.SODOSM -> p.sodos.toInt()
PlacementCriterion.SOSOSM -> p.sosos.toInt()
PlacementCriterion.CUSSM -> p.cums.toInt()
else -> -1
}
}
}

View File

@@ -1,9 +1,6 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.store.Store
import org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching
import org.jgrapht.alg.matching.blossom.v5.ObjectiveSense
@@ -47,30 +44,88 @@ private fun nonDetRandom(max: Long): Long {
return r.toLong()
}
sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val pairingParams: Pairing.PairingParams) {
sealed class Solver(
val round: Int,
history: List<Game>,
val pairables: List<Pairable>,
val pairingParams: Pairing.PairingParams,
val placementParams: PlacementParams) {
companion object {
val rand = Random(/* seed from properties - TODO */)
}
open fun sort(p: Pairable, q: Pairable): Int = 0 // no sort by default
open fun sort(p: Pairable, q: Pairable): Int {
for (criterion in placementParams.criteria) {
val criterionP = getCriterionValue(p, criterion)
val criterionQ = getCriterionValue(q, criterion)
if (criterionP != criterionQ) {
return criterionP - criterionQ
}
}
return 0
}
open fun weight(p1: Pairable, p2: Pairable): Double {
var score = 1L // 1 is minimum value because 0 means "no matching allowed"
score += applyBaseCriteria(p1, p2)
score += applyMainCriteria(p1, p2)
return score as Double
}
// The main criterion that will be used to define the groups should be defined by subclasses
abstract fun mainCriterion(p1: Pairable): Int
abstract fun mainCriterionMinMax(): Pair<Int, Int>
// SOS and variants will be computed based on this score
abstract fun computeStandingScore(): Map<ID, Double>
// This function needs to be overridden for criterion specific to the current pairing mode
open fun getSpecificCriterionValue(p1: Pairable, criterion: PlacementCriterion): Int {
return -1
}
private fun getCriterionValue(p1: Pairable, criterion: PlacementCriterion): Int {
val genericCritVal = historyHelper.getCriterionValue(p1, criterion)
// If the value from the history helper is > 0 it means that it is a generic criterion
// Just returns the value
if (genericCritVal != -1) {
return genericCritVal
}
// Otherwise we have to delegate it to the solver
val critVal = getSpecificCriterionValue(p1, criterion)
if (critVal == -1) throw Error("Couldn't compute criterion value")
return critVal
}
fun pair(): List<Game> {
// check that at this stage, we have an even number of pairables
if (pairables.size % 2 != 0) throw Error("expecting an even number of pairables")
val builder = GraphBuilder(SimpleDirectedWeightedGraph<Pairable, DefaultWeightedEdge>(DefaultWeightedEdge::class.java))
for (i in sortedPairables.indices) {
for (j in i + 1 until pairables.size) {
val p = pairables[i]
val q = pairables[j]
weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it) }
weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it) }
}
}
val graph = builder.build()
val matching = KolmogorovWeightedPerfectMatching(graph, ObjectiveSense.MINIMIZE)
val solution = matching.matching
val result = solution.flatMap {
games(black = graph.getEdgeSource(it) , white = graph.getEdgeTarget(it))
}
return result
}
// Weight score computation details
// Base criteria
open fun avoidDuplicatingGames(p1: Pairable, p2: Pairable): Long {
if (historyHelper.playedTogether(p1, p2)) {
return pairingParams.baseAvoidDuplGame
if (p1.played(p2)) {
return 0 // We get no score if pairables already played together
} else {
return 0
return pairingParams.baseAvoidDuplGame
}
}
@@ -129,17 +184,18 @@ sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val pair
}
open fun minimizeScoreDifference(p1: Pairable, p2: Pairable): Long {
var scoCost: Long = 0
val scoRange: Int = numberGroups
var score: Long = 0
val scoreRange: Int = numberGroups
// TODO check category equality if category are used in SwissCat
val x = abs(groups[p1.id]!! - groups[p2.id]!!) as Double / scoRange.toDouble()
val x = abs(p1.group - p2.group) as Double / scoreRange.toDouble()
val k: Double = pairingParams.standardNX1Factor
scoCost = (pairingParams.mainMinimizeScoreDifference * (1.0 - x) * (1.0 + k * x)) as Long
score = (pairingParams.mainMinimizeScoreDifference * (1.0 - x) * (1.0 + k * x)) as Long
return scoCost
return score
}
// Handicap functions
// Has to be overridden if handicap is not based on rank
open fun handicap(p1: Pairable, p2: Pairable): Int {
var hd = 0
var pseudoRank1: Int = p1.rank
@@ -152,8 +208,8 @@ sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val pair
return clampHandicap(hd)
}
open fun clampHandicap(input_hd: Int): Int {
var hd = input_hd
open fun clampHandicap(inputHd: Int): Int {
var hd = inputHd
if (hd > 0) {
hd -= pairingParams.hd.correction
hd = min(hd, 0)
@@ -174,47 +230,25 @@ sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val pair
return listOf(Game(id = Store.nextGameId, black = black.id, white = white.id, handicap = handicap(black, white)))
}
fun pair(): List<Game> {
// check that at this stage, we have an even number of pairables
if (pairables.size % 2 != 0) throw Error("expecting an even number of pairables")
val builder = GraphBuilder(SimpleDirectedWeightedGraph<Pairable, DefaultWeightedEdge>(DefaultWeightedEdge::class.java))
for (i in sortedPairables.indices) {
for (j in i + 1 until n) {
val p = pairables[i]
val q = pairables[j]
weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it) }
weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it) }
}
}
val graph = builder.build()
val matching = KolmogorovWeightedPerfectMatching(graph, ObjectiveSense.MINIMIZE)
val solution = matching.matching
// Generic parameters calculation
private val standingScore = computeStandingScore()
val historyHelper =
if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history, standingScore)
else HistoryHelper(history, standingScore)
val result = solution.flatMap {
games(black = graph.getEdgeSource(it) , white = graph.getEdgeTarget(it))
}
return result
}
private fun computeGroups(): Pair<Map<Int, Int>, Int> {
// Decide each pairable group based on the main criterion
private fun computeGroups(): Pair<Map<ID, Int>, Int> {
val (mainScoreMin, mainScoreMax) = mainCriterionMinMax()
// TODO categories
val groups: Map<Int, Int> = pairables.associate { pairable -> Pair(pairable.id, mainCriterion(pairable)) }
val groups: Map<ID, Int> = pairables.associate { pairable -> Pair(pairable.id, mainCriterion(pairable)) }
return Pair(groups, mainScoreMax - mainScoreMin)
}
// Calculation parameters
val n = pairables.size
private val historyHelper =
if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history)
else HistoryHelper(history)
private val groupsResult = computeGroups()
private val groups = groupsResult.first
private val _groups = groupsResult.first
private val numberGroups = groupsResult.second
// pairables sorted using overloadable sort function
@@ -234,7 +268,7 @@ sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val pair
val Pairable.placeInGroup: Pair<Int, Int> get() = _placeInGroup[id]!!
private val _placeInGroup by lazy {
sortedPairables.groupBy {
it.score
it.group
}.values.flatMap { group ->
group.mapIndexed { index, pairable ->
Pair(pairable.id, Pair(index, group.size))
@@ -243,20 +277,25 @@ sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val pair
}
// already paired players map
fun Pairable.played(other: Pairable) = historyHelper.playedTogether(this, other)
private fun Pairable.played(other: Pairable) = historyHelper.playedTogether(this, other)
// color balance (nw - nb)
val Pairable.colorBalance: Int get() = historyHelper.colorBalance(this) ?: 0
private val Pairable.colorBalance: Int get() = historyHelper.colorBalance(this) ?: 0
private val Pairable.group: Int get() = _groups[id]!!
// score (number of wins)
val Pairable.score: Double get() = historyHelper.score(this) ?: 0.0
val Pairable.nbW: Double get() = historyHelper.nbW(this) ?: 0.0
val Pairable.sos: Double get() = historyHelper.sos[id]!!
val Pairable.sosm1: Double get() = historyHelper.sosm1[id]!!
val Pairable.sosm2: Double get() = historyHelper.sosm2[id]!!
val Pairable.sosos: Double get() = historyHelper.sosos[id]!!
val Pairable.sodos: Double get() = historyHelper.sodos[id]!!
val Pairable.cums: Double get() = historyHelper.cumscore[id]!!
// sos
val Pairable.sos: Double get() = historyHelper.sos(this) ?: 0.0
// sosos
val Pairable.sosos: Double get() = historyHelper.sosos(this) ?: 0.0
// sodos
val Pairable.sodos: Double get() = historyHelper.sodos(this) ?: 0.0
}

View File

@@ -1,23 +1,39 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.*
import kotlin.math.abs
class SwissSolver(history: List<Game>, pairables: List<Pairable>, pairingParams: Pairing.PairingParams): Solver(history, pairables, pairingParams) {
override fun sort(p: Pairable, q: Pairable): Int =
when (p.score) {
q.score -> q.rating - p.rating
else -> ((q.score - p.score) * 1000).toInt()
}
class SwissSolver(round: Int,
history: List<Game>,
pairables: List<Pairable>,
pairingParams: Pairing.PairingParams,
placementParams: PlacementParams):
Solver(round, history, pairables, pairingParams, placementParams) {
// In a Swiss tournament the main criterion is the number of wins and already computed
override fun mainCriterion(p1: Pairable): Int {
TODO("Not yet implemented")
return p1.nbW.toInt() // Rounded Down TODO make it a parameter ?
}
override fun mainCriterionMinMax(): Pair<Int, Int> {
TODO("Not yet implemented")
return Pair(0, round-1)
}
override fun computeStandingScore(): Map<ID, Double> {
return historyHelper.numberWins
}
override fun getSpecificCriterionValue(p: Pairable, criterion: PlacementCriterion): Int {
// TODO solve this double/int conflict
return when (criterion) {
PlacementCriterion.NBW -> p.nbW.toInt()
PlacementCriterion.SOSW -> p.sos.toInt()
PlacementCriterion.SOSWM1 -> p.sosm1.toInt()
PlacementCriterion.SOSWM2 -> p.sosm2.toInt()
PlacementCriterion.SODOSW -> p.sodos.toInt()
PlacementCriterion.SOSOSW -> p.sosos.toInt()
PlacementCriterion.CUSSW -> p.cums.toInt()
else -> -1
}
}
}