Merge remote-tracking branch 'origin/refactor' into pairing2

This commit is contained in:
Theo Barollet
2023-10-23 15:06:41 +02:00
7 changed files with 175 additions and 156 deletions

View File

@@ -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(

View File

@@ -0,0 +1,130 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.*
abstract class BasePairingHelper(
history: List<List<Game>>, // History of all games played for each round
var pairables: List<Pairable>, // All pairables for this round, it may include the bye player
val pairing: PairingParams,
val placement: PlacementParams,
) {
abstract val scores: Map<ID, Double>
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<Double, Double>
// 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<Int, Int> 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
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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<List<Game>>, // History of all games played for each round
var pairables: List<Pairable>, // All pairables for this round, it may include the bye player
val pairing: PairingParams,
val placement: PlacementParams,
history: List<List<Game>>, // History of all games played for each round
pairables: List<Pairable>, // 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<ID, Double>
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<Double, Double>
// SOS and variants will be computed based on this score
fun pair(): List<Game> {
// The byeGame is a list of one game with the bye player or an empty list
val byeGame: List<Game> = 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<Int, Int> 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}")
}
}

View File

@@ -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<Pairable>,
pairingParams: PairingParams,
placementParams: PlacementParams):
Solver(round, history, pairables, pairingParams, placementParams) {
BaseSolver(round, history, pairables, pairingParams, placementParams) {
override val scores: Map<ID, Double> by lazy {
historyHelper.wins.mapValues {

View File

@@ -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<List<Game>>,
pairables: List<Pairable>,
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