Tackle solver reeng

This commit is contained in:
Claude Brisson
2023-06-20 08:17:56 +02:00
parent 2901394052
commit e2cf4bb0fd
5 changed files with 170 additions and 159 deletions

View File

@@ -8,7 +8,7 @@ import org.jeudego.pairgoth.pairing.MacMahonSolver
import org.jeudego.pairgoth.pairing.SwissSolver
// base pairing parameters
data class BasePairingParams(
data class BaseCritParams(
// standard NX1 factor for concavity curves
val nx1: Double = 0.5,
val dupWeight: Double = MAX_AVOIDDUPGAME,
@@ -27,7 +27,7 @@ data class BasePairingParams(
const val MAX_AVOIDDUPGAME = 500000000000000.0 // 5e14
const val MAX_RANDOM = 1000000000.0 // 1e9
const val MAX_COLOR_BALANCE = 1000000.0 // 1e6
val default = BasePairingParams()
val default = BaseCritParams()
}
}
@@ -106,7 +106,7 @@ data class HandicapParams(
enum class PairingType { SWISS, MAC_MAHON, ROUND_ROBIN }
data class PairingParams(
val base: BasePairingParams = BasePairingParams(),
val base: BaseCritParams = BaseCritParams(),
val main: MainCritParams = MainCritParams(),
val secondary: SecondaryCritParams = SecondaryCritParams(),
val geo: GeographicalParams = GeographicalParams(),
@@ -123,11 +123,11 @@ sealed class Pairing(
private fun Tournament<*>.historyBefore(round: Int) =
if (lastRound() == 0) emptyList()
else (0 until round).flatMap { games(round).values }
else (0 until round).map { games(round).values.toList() }
class Swiss(
pairingParams: PairingParams = PairingParams(
base = BasePairingParams(),
base = BaseCritParams(),
main = MainCritParams(
seedSystem1 = SPLIT_AND_SLIP,
seedSystem2 = SPLIT_AND_SLIP
@@ -153,7 +153,7 @@ class Swiss(
class MacMahon(
pairingParams: PairingParams = PairingParams(
base = BasePairingParams(),
base = BaseCritParams(),
main = MainCritParams(),
secondary = SecondaryCritParams(
defSecCrit = MainCritParams.MAX_SCORE_WEIGHT
@@ -184,7 +184,7 @@ class RoundRobin(
// Serialization
fun BasePairingParams.Companion.fromJson(json: Json.Object) = BasePairingParams(
fun BaseCritParams.Companion.fromJson(json: Json.Object) = BaseCritParams(
nx1 = json.getDouble("nx1") ?: default.nx1,
dupWeight = json.getDouble("dupWeight") ?: default.dupWeight,
random = json.getDouble("random") ?: default.random,
@@ -192,7 +192,7 @@ fun BasePairingParams.Companion.fromJson(json: Json.Object) = BasePairingParams(
colorBalance = json.getDouble("colorBalanceWeight") ?: default.colorBalance
)
fun BasePairingParams.toJson() = Json.Object(
fun BaseCritParams.toJson() = Json.Object(
"nx1" to nx1,
"dupWeight" to dupWeight,
"random" to random,
@@ -281,7 +281,7 @@ fun Pairing.Companion.fromJson(json: Json.Object): Pairing {
MAC_MAHON -> MacMahon()
ROUND_ROBIN -> RoundRobin()
}
val base = json.getObject("base")?.let { BasePairingParams.fromJson(it) } ?: defaultParams.pairingParams.base
val base = json.getObject("base")?.let { BaseCritParams.fromJson(it) } ?: defaultParams.pairingParams.base
val main = json.getObject("main")?.let { MainCritParams.fromJson(it) } ?: defaultParams.pairingParams.main
val secondary = json.getObject("secondary")?.let { SecondaryCritParams.fromJson(it) } ?: defaultParams.pairingParams.secondary
val geo = json.getObject("geo")?.let { GeographicalParams.fromJson(it) } ?: defaultParams.pairingParams.geo

View File

@@ -1,8 +1,19 @@
package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.model.Game.Result.*
open class HistoryHelper(protected val history: List<Game>, computeScore: () -> Map<ID, Double>) {
open class HistoryHelper(protected val history: List<List<Game>>, computeScore: () -> Map<ID, Double>) {
private val Game.blackScore get() = when (result) {
BLACK, BOTHWIN -> 1.0
else -> 0.0
}
private val Game.whiteScore get() = when (result) {
WHITE, BOTHWIN -> 1.0
else -> 0.0
}
fun getCriterionValue(p: Pairable, crit: Criterion): Double {
// Returns generic criterion
@@ -23,13 +34,12 @@ open class HistoryHelper(protected val history: List<Game>, computeScore: () ->
// 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 nbW(p: Pairable) = numberWins[p.id]
open fun nbW(p: Pairable) = wins[p.id]
protected val paired: Set<Pair<ID, ID>> by lazy {
(history.map { game ->
(history.flatten().map { game ->
Pair(game.black, game.white)
} + history.map { game ->
} + history.flatten().map { game ->
Pair(game.white, game.black)
}).toSet()
}
@@ -37,25 +47,26 @@ open class HistoryHelper(protected val history: List<Game>, computeScore: () ->
// Returns the number of games played as white
// Only count games without handicap
private val colorBalance: Map<ID, Int> by lazy {
history.flatMap { game -> if (game.handicap == 0) {
history.flatten().filter { game ->
game.handicap == 0
}.flatMap { game ->
listOf(Pair(game.white, +1), Pair(game.black, -1))
} else {
listOf(Pair(game.white, 0), Pair(game.black, 0))
}
}.groupingBy { it.first }.fold(0) { acc, next ->
}.groupingBy {
it.first
}.fold(0) { acc, next ->
acc + next.second
}
}
val numberWins: Map<ID, Double> by lazy {
val wins: Map<ID, Double> by lazy {
mutableMapOf<ID, Double>().apply {
history.forEach { game ->
history.flatten().forEach { game ->
when (game.result) {
Game.Result.BLACK -> put(game.black, getOrDefault(game.black, 0.0) + 1.0)
Game.Result.WHITE -> put(game.white, getOrDefault(game.white, 0.0) + 1.0)
Game.Result.BOTHWIN -> {
put(game.black, getOrDefault(game.black, 0.0) + 0.5)
put(game.white, getOrDefault(game.white, 0.0) + 0.5)
put(game.black, getOrDefault(game.black, 0.0) + 1.0)
put(game.white, getOrDefault(game.white, 0.0) + 1.0)
}
else -> {}
}
@@ -70,9 +81,9 @@ open class HistoryHelper(protected val history: List<Game>, computeScore: () ->
// SOS related functions given a score function
val sos by lazy {
(history.map { game ->
(history.flatten().map { game ->
Pair(game.black, score[game.white] ?: 0.0)
} + history.map { game ->
} + history.flatten().map { game ->
Pair(game.white, score[game.black] ?: 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
@@ -80,20 +91,38 @@ open class HistoryHelper(protected val history: List<Game>, computeScore: () ->
}
// sos-1
val sosm1: Map<ID, Double> by lazy {
TODO()
val sosm1 by lazy {
(history.flatten().map { game ->
Pair(game.black, score[game.white] ?: 0.0)
} + history.flatten().map { game ->
Pair(game.white, score[game.black] ?: 0.0)
}).groupBy {
it.first
}.mapValues {
val scores = it.value.map { it.second }.sorted()
scores.sum() - (scores.firstOrNull() ?: 0.0)
}
}
// sos-2
val sosm2: Map<ID, Double> by lazy {
TODO()
val sosm2 by lazy {
(history.flatten().map { game ->
Pair(game.black, score[game.white] ?: 0.0)
} + history.flatten().map { game ->
Pair(game.white, score[game.black] ?: 0.0)
}).groupBy {
it.first
}.mapValues {
val scores = it.value.map { it.second }.sorted()
scores.sum() - scores.getOrElse(0) { 0.0 } - scores.getOrElse(1) { 0.0 }
}
}
// sodos
val sodos by lazy {
(history.map { game ->
(history.flatten().map { game ->
Pair(game.black, if (game.result == Game.Result.BLACK) score[game.white] ?: 0.0 else 0.0)
} + history.map { game ->
} + history.flatten().map { game ->
Pair(game.white, if (game.result == Game.Result.WHITE) score[game.black] ?: 0.0 else 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
@@ -102,9 +131,9 @@ open class HistoryHelper(protected val history: List<Game>, computeScore: () ->
// sosos
val sosos by lazy {
(history.map { game ->
(history.flatten().map { game ->
Pair(game.black, sos[game.white] ?: 0.0)
} + history.map { game ->
} + history.flatten().map { game ->
Pair(game.white, sos[game.black] ?: 0.0)
}).groupingBy { it.first }.fold(0.0) { acc, next ->
acc + next.second
@@ -112,14 +141,25 @@ open class HistoryHelper(protected val history: List<Game>, computeScore: () ->
}
// cumulative score
val cumscore: Map<ID, Double> by lazy {
TODO()
val cumScore by lazy {
history.map { games ->
(games.groupingBy { it.black }.fold(0.0) { acc, next ->
acc + next.blackScore
}) +
(games.groupingBy { it.white }.fold(0.0) { acc, next ->
acc + next.whiteScore
})
}.reduce { acc, map ->
(acc.keys + map.keys).associate<ID, ID, Double> { id ->
Pair(id, acc.getOrDefault(id, 0.0) + acc.getOrDefault(id, 0.0) + map.getOrDefault(id, 0.0))
}.toMap()
}
}
}
// 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>, computeScore: () -> Map<ID, Double>):
class TeamOfIndividualsHistoryHelper(history: List<List<Game>>, computeScore: () -> Map<ID, Double>):
HistoryHelper(history, computeScore) {
private fun Pairable.asTeam() = this as TeamTournament.Team

View File

@@ -2,7 +2,12 @@ package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.*
class MacMahonSolver(round: Int, history: List<Game>, pairables: List<Pairable>, pairingParams: PairingParams, placementParams: PlacementParams): Solver(round, history, pairables, pairingParams, placementParams) {
class MacMahonSolver(round: Int,
history: List<List<Game>>,
pairables: List<Pairable>,
pairingParams: PairingParams,
placementParams: PlacementParams):
Solver(round, history, pairables, pairingParams, placementParams) {
// val Pairable.mms get() = mmBase + nbW // TODO real calculation
@@ -32,5 +37,4 @@ class MacMahonSolver(round: Int, history: List<Game>, pairables: List<Pairable>,
else -> -1.0
}
}
}

View File

@@ -17,23 +17,15 @@ val DEBUG_EXPORT_WEIGHT = true
private fun detRandom(max: Double, p1: Pairable, p2: Pairable): Double {
var inverse = false
val seed1 = p1.nameSeed()
val seed2 = p2.nameSeed()
var name1 = seed1
var name2 = seed2
var name1 = p1.nameSeed()
var name2 = p2.nameSeed()
if (name1 > name2) {
name1 = name2.also { name2 = name1 }
inverse = true
}
val s = name1 + name2
var nR = 0.0
for (i in s.indices) {
val c = s[i]
nR += (c.code * (i + 1)).toDouble()
}
nR = nR * 1234567 % (max + 1)
var nR = "$name1$name2".mapIndexed { i, c ->
c.code.toDouble() * (i + 1)
}.sum() * 1234567 % (max + 1)
if (inverse) nR = max - nR
return nR
}
@@ -43,18 +35,22 @@ private fun nonDetRandom(max: Double) =
else Math.random() * (max + 1.0)
sealed class Solver(
val round: Int,
history: List<Game>,
val pairables: List<Pairable>,
val pairingParams: PairingParams,
val placementParams: PlacementParams) {
val round: Int,
history: List<List<Game>>,
val pairables: List<Pairable>,
val pairing: PairingParams,
val placement: PlacementParams
) {
companion object {
val rand = Random(/* seed from properties - TODO */)
}
val historyHelper = if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history, ::computeStandingScore)
else HistoryHelper(history, ::computeStandingScore)
open fun sort(p: Pairable, q: Pairable): Int {
for (criterion in placementParams.criteria) {
for (criterion in placement.criteria) {
val criterionP = getCriterionValue(p, criterion)
val criterionQ = getCriterionValue(q, criterion)
if (criterionP != criterionQ) {
@@ -65,9 +61,10 @@ sealed class Solver(
}
open fun weight(p1: Pairable, p2: Pairable) =
1.0 + // 1 is minimum value because 0 means "no matching allowed"
applyBaseCriteria(p1, p2) +
applyMainCriteria(p1, p2) +
applySecondaryCriteria(p1, p2)
pairing.base.apply(p1, p2) +
pairing.main.apply(p1, p2) +
pairing.secondary.apply(p1, p2) +
pairing.geo.apply(p1, p2)
// The main criterion that will be used to define the groups should be defined by subclasses
abstract fun mainCriterion(p1: Pairable): Int
@@ -114,22 +111,48 @@ sealed class Solver(
return result
}
open fun applyBaseCriteria(p1: Pairable, p2: Pairable): Double {
var score = 0.0
// base criteria
open fun BaseCritParams.apply(p1: Pairable, p2: Pairable): Double {
var score = 0.0
// Base Criterion 1 : Avoid Duplicating Game
// Did p1 and p2 already play ?
score += avoidDuplicatingGames(p1, p2)
// Base Criterion 2 : Random
score += applyRandom(p1, p2)
// Base Criterion 3 : Balance W and B
score += applyBalanceBW(p1, p2)
score += applyColorBalance(p1, p2)
return score
}
// Weight score computation details
open fun BaseCritParams.avoidDuplicatingGames(p1: Pairable, p2: Pairable): Double {
return if (p1.played(p2)) 0.0 // We get no score if pairables already played together
else dupWeight
}
open fun BaseCritParams.applyRandom(p1: Pairable, p2: Pairable): Double {
return if (deterministic) detRandom(random, p1, p2)
else nonDetRandom(random)
}
open fun BaseCritParams.applyColorBalance(p1: Pairable, p2: Pairable): Double {
// This cost is never applied if potential Handicap != 0
// It is fully applied if wbBalance(sP1) and wbBalance(sP2) are strictly of different signs
// It is half applied if one of wbBalance is 0 and the other is >=2
val potentialHd: Int = pairing.handicap.handicap(p1, p2)
if (potentialHd == 0) {
val wb1: Int = p1.colorBalance
val wb2: Int = p2.colorBalance
if (wb1 * wb2 < 0) return colorBalance
else if (wb1 == 0 && abs(wb2) >= 2 || wb2 == 0 && abs(wb1) >= 2) return colorBalance / 2
}
return 0.0
}
// Main criteria
open fun applyMainCriteria(p1: Pairable, p2: Pairable): Double {
open fun MainCritParams.apply(p1: Pairable, p2: Pairable): Double {
var score = 0.0
// Main criterion 1 avoid mixing category is moved to Swiss with category
@@ -147,76 +170,26 @@ sealed class Solver(
return score
}
open fun applySecondaryCriteria(p1: Pairable, p2: Pairable): Double {
var score = 0.0
// See Swiss with category for minimizing handicap criterion
// TODO understand where opengotha test if need to be applied
// Geographical criterion
score += avoidSameGeo(p1, p2)
return score
}
// Weight score computation details
// Base criteria
open fun avoidDuplicatingGames(p1: Pairable, p2: Pairable): Double {
if (p1.played(p2)) {
return 0.0 // We get no score if pairables already played together
} else {
return pairingParams.base.dupWeight
}
}
open fun applyRandom(p1: Pairable, p2: Pairable): Double {
if (pairingParams.base.deterministic) {
return detRandom(pairingParams.base.random, p1, p2)
} else {
return nonDetRandom(pairingParams.base.random)
}
}
open fun applyBalanceBW(p1: Pairable, p2: Pairable): Double {
// This cost is never applied if potential Handicap != 0
// It is fully applied if wbBalance(sP1) and wbBalance(sP2) are strictly of different signs
// It is half applied if one of wbBalance is 0 and the other is >=2
val potentialHd: Int = handicap(p1, p2)
if (potentialHd == 0) {
val wb1: Int = p1.colorBalance
val wb2: Int = p2.colorBalance
if (wb1 * wb2 < 0) {
return pairingParams.base.colorBalance
} else if (wb1 == 0 && abs(wb2) >= 2) {
return pairingParams.base.colorBalance / 2
} else if (wb2 == 0 && abs(wb1) >= 2) {
return pairingParams.base.colorBalance / 2
}
}
return 0.0
}
open fun minimizeScoreDifference(p1: Pairable, p2: Pairable): Double {
open fun MainCritParams.minimizeScoreDifference(p1: Pairable, p2: Pairable): Double {
var score = 0.0
val scoreRange: Int = numberGroups
// TODO check category equality if category are used in SwissCat
val x = abs(p1.group - p2.group) as Double / scoreRange.toDouble()
val k: Double = pairingParams.base.nx1
score = pairingParams.main.scoreWeight * (1.0 - x) * (1.0 + k * x)
val x = abs(p1.group - p2.group).toDouble() / scoreRange.toDouble()
val k: Double = pairing.base.nx1
score = scoreWeight * (1.0 - x) * (1.0 + k * x)
return score
}
fun applySeeding(p1: Pairable, p2: Pairable): Double {
fun MainCritParams.applySeeding(p1: Pairable, p2: Pairable): Double {
var score = 0.0
// Apply seeding for players in the same group
if (p1.group == p2.group) {
val (cla1, groupSize) = p1.placeInGroup
val cla2 = p2.placeInGroup.first
val maxSeedingWeight = pairingParams.main.seedingWeight
val maxSeedingWeight = seedingWeight
val currentSeedSystem: MainCritParams.SeedMethod = if (round <= pairingParams.main.lastRoundForSeedSystem1)
pairingParams.main.seedSystem1 else pairingParams.main.seedSystem2
val currentSeedSystem= if (round <= lastRoundForSeedSystem1) seedSystem1 else seedSystem2
score += when(currentSeedSystem) {
// The best is to get 2 * |Cla1 - Cla2| - groupSize close to 0
@@ -245,21 +218,30 @@ sealed class Solver(
return score
}
open fun doNeedToApplySecondaryCriteria(p1: Pairable, p2: Pairable) {
open fun SecondaryCritParams.apply(p1: Pairable, p2: Pairable): Double {
var score = 0.0
// See Swiss with category for minimizing handicap criterion
// TODO understand where opengotha test if need to be applied
return score
}
open fun SecondaryCritParams.notNeeded(p1: Pairable, p2: Pairable) {
// secCase = 0 : No player is above thresholds
// secCase = 1 : One player is above thresholds
// secCase = 2 : Both players are above thresholds
// TODO understand where it is used
}
fun avoidSameGeo(p1: Pairable, p2: Pairable): Double {
fun GeographicalParams.apply(p1: Pairable, p2: Pairable): Double {
val placementScoreRange = numberGroups
val geoMaxCost = pairingParams.geo.avoidSameGeo
val geoMaxCost = avoidSameGeo
val countryFactor = pairingParams.geo.preferMMSDiffRatherThanSameCountry
val clubFactor: Int = pairingParams.geo.preferMMSDiffRatherThanSameClub
//val groupFactor: Int = pairingParams.geo.preferMMSDiffRatherThanSameClubsGroup
val countryFactor = preferMMSDiffRatherThanSameCountry
val clubFactor: Int = preferMMSDiffRatherThanSameClub
//val groupFactor: Int = preferMMSDiffRatherThanSameClubsGroup
// Same country
val countryRatio = if (p1.country != p2.country && countryFactor != 0) {
@@ -298,12 +280,12 @@ sealed class Solver(
var geoRatio = mainPart + secPart / 2.0
if (geoRatio > 0.0) {
geoRatio += 0.5 / placementScoreRange as Double
geoRatio += 0.5 / placementScoreRange.toDouble()
}
// The concavity function is applied to geoRatio to get geoCost
val dbGeoCost: Double = geoMaxCost.toDouble() * (1.0 - geoRatio) * (1.0 + pairingParams.base.nx1 * geoRatio)
var score = pairingParams.main.scoreWeight - dbGeoCost
val dbGeoCost: Double = geoMaxCost.toDouble() * (1.0 - geoRatio) * (1.0 + pairing.base.nx1 * geoRatio)
var score = pairing.main.scoreWeight - dbGeoCost
score = min(score, geoMaxCost)
return score
@@ -311,48 +293,37 @@ sealed class Solver(
// Handicap functions
// Has to be overridden if handicap is not based on rank
open fun handicap(p1: Pairable, p2: Pairable): Int {
open fun HandicapParams.handicap(p1: Pairable, p2: Pairable): Int {
var hd = 0
var pseudoRank1: Int = p1.rank
var pseudoRank2: Int = p2.rank
pseudoRank1 = min(pseudoRank1, pairingParams.handicap.rankThreshold)
pseudoRank2 = min(pseudoRank2, pairingParams.handicap.rankThreshold)
pseudoRank1 = min(pseudoRank1, rankThreshold)
pseudoRank2 = min(pseudoRank2, rankThreshold)
hd = pseudoRank1 - pseudoRank2
return clampHandicap(hd)
return clamp(hd)
}
open fun clampHandicap(inputHd: Int): Int {
var hd = inputHd
if (hd > 0) {
hd -= pairingParams.handicap.correction
hd = min(hd, 0)
}
if (hd < 0) {
hd += pairingParams.handicap.correction
hd = max(hd, 0)
}
open fun HandicapParams.clamp(input: Int): Int {
var hd = input
if (hd >= correction) hd -= correction
if (hd < 0) hd = max(hd + correction, 0)
// Clamp handicap with ceiling
hd = min(hd, pairingParams.handicap.ceiling)
hd = max(hd, -pairingParams.handicap.ceiling)
hd = min(hd, ceiling)
hd = max(hd, -ceiling)
return hd
}
open fun games(black: Pairable, white: Pairable): List<Game> {
// CB TODO team of individuals pairing
return listOf(Game(id = Store.nextGameId, black = black.id, white = white.id, handicap = handicap(black, white)))
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() }
val historyHelper = if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(history, ::computeStandingScore)
else HistoryHelper(history, ::computeStandingScore)
// Decide each pairable group based on the main criterion
private val numberGroups by lazy {
val (mainScoreMin, mainScoreMax) = mainCriterionMinMax()
@@ -401,9 +372,5 @@ sealed class Solver(
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]!!
val Pairable.cums: Double get() = historyHelper.cumScore[id]!!
}

View File

@@ -3,7 +3,7 @@ package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.*
class SwissSolver(round: Int,
history: List<Game>,
history: List<List<Game>>,
pairables: List<Pairable>,
pairingParams: PairingParams,
placementParams: PlacementParams):
@@ -19,7 +19,7 @@ class SwissSolver(round: Int,
}
override fun computeStandingScore(): Map<ID, Double> {
return historyHelper.numberWins
return historyHelper.wins
}
override fun getSpecificCriterionValue(p: Pairable, criterion: Criterion): Double {