Merge remote-tracking branch 'origin/refactor' into pairing2
This commit is contained in:
@@ -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(
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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)
|
@@ -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}")
|
||||
}
|
||||
}
|
@@ -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 {
|
@@ -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
|
||||
|
Reference in New Issue
Block a user