Use directed graph and colors weight

This commit is contained in:
Claude Brisson
2023-05-19 17:14:11 +02:00
parent 1e20247fe9
commit 45873d6014
3 changed files with 34 additions and 19 deletions

View File

@@ -11,9 +11,15 @@ import java.util.Random
// TODO - this is only an early draft // TODO - this is only an early draft
sealed class Pairing(val type: PairingType) { sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) {
companion object {} companion object {}
enum class PairingType { SWISS, MACMAHON, ROUNDROBIN } enum class PairingType { SWISS, MACMAHON, ROUNDROBIN }
data class Weights(
val played: Double = 1_000_000.0, // weight if players already met
val score: Double = 10_000.0, // per difference of score or MMS
val place: Double = 1_000.0, // per difference of expected position for Swiss
val color: Double = 100.0 // per color unbalancing
)
abstract fun pair(tournament: Tournament, round: Int, pairables: List<Pairable>): List<Game> abstract fun pair(tournament: Tournament, round: Int, pairables: List<Pairable>): List<Game>
} }
@@ -28,7 +34,7 @@ class Swiss(
val history = val history =
if (tournament.games.isEmpty()) emptyList() if (tournament.games.isEmpty()) emptyList()
else tournament.games.slice(0 until round).flatMap { it.values } else tournament.games.slice(0 until round).flatMap { it.values }
return SwissSolver(history, pairables, actualMethod).pair() return SwissSolver(history, pairables, weights, actualMethod).pair()
} }
} }

View File

@@ -3,15 +3,17 @@ package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Game.Result.* import org.jeudego.pairgoth.model.Game.Result.*
import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching import org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching
import org.jgrapht.alg.matching.blossom.v5.ObjectiveSense import org.jgrapht.alg.matching.blossom.v5.ObjectiveSense
import org.jgrapht.graph.DefaultWeightedEdge import org.jgrapht.graph.DefaultWeightedEdge
import org.jgrapht.graph.SimpleDirectedWeightedGraph
import org.jgrapht.graph.SimpleWeightedGraph import org.jgrapht.graph.SimpleWeightedGraph
import org.jgrapht.graph.builder.GraphBuilder import org.jgrapht.graph.builder.GraphBuilder
import java.util.* import java.util.*
sealed class Solver(protected val history: List<Game>, protected val pairables: List<Pairable>) { sealed class Solver(val history: List<Game>, val pairables: List<Pairable>, val weights: Pairing.Weights) {
companion object { companion object {
val rand = Random(/* seed from properties - TODO */) val rand = Random(/* seed from properties - TODO */)
@@ -23,12 +25,13 @@ sealed class Solver(protected val history: List<Game>, protected val pairables:
fun pair(): List<Game> { fun pair(): List<Game> {
// check that at this stage, we have an even number of pairables // 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") if (pairables.size % 2 != 0) throw Error("expecting an even number of pairables")
val builder = GraphBuilder(SimpleWeightedGraph<Pairable, DefaultWeightedEdge>(DefaultWeightedEdge::class.java)) val builder = GraphBuilder(SimpleDirectedWeightedGraph<Pairable, DefaultWeightedEdge>(DefaultWeightedEdge::class.java))
for (i in sortedPairables.indices) { for (i in sortedPairables.indices) {
for (j in i + 1 until n) { for (j in i + 1 until n) {
val p = pairables[i] val p = pairables[i]
val q = pairables[j] val q = pairables[j]
builder.addEdge(p, q, weight(p, q)) builder.addEdge(p, q, weight(p, q))
builder.addEdge(q, p, weight(q, p))
} }
} }
val graph = builder.build() val graph = builder.build()
@@ -36,7 +39,6 @@ sealed class Solver(protected val history: List<Game>, protected val pairables:
val solution = matching.matching val solution = matching.matching
val result = solution.map { val result = solution.map {
// CB TODO - choice of colors should be here
Game(Store.nextGameId, graph.getEdgeSource(it).id , graph.getEdgeTarget(it).id) Game(Store.nextGameId, graph.getEdgeSource(it).id , graph.getEdgeTarget(it).id)
} }
return result return result
@@ -81,6 +83,16 @@ sealed class Solver(protected val history: List<Game>, protected val pairables:
}).toSet() }).toSet()
} }
// color balance (nw - nb)
val Pairable.colorBalance: Int get() = _colorBalance[id] ?: 0
private val _colorBalance: Map<Int, Int> by lazy {
history.flatMap { game ->
listOf(Pair(game.white, +1), Pair(game.black, -1))
}.groupingBy { it.first }.fold(0) { acc, next ->
acc + next.second
}
}
// score (number of wins) // score (number of wins)
val Pairable.score: Int get() = _score[id] ?: 0 val Pairable.score: Int get() = _score[id] ?: 0
private val _score: Map<Int, Int> by lazy { private val _score: Map<Int, Int> by lazy {

View File

@@ -2,15 +2,12 @@ package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairing
import org.jeudego.pairgoth.model.Swiss import org.jeudego.pairgoth.model.Swiss
import org.jeudego.pairgoth.model.Swiss.Method.* import org.jeudego.pairgoth.model.Swiss.Method.*
import kotlin.math.abs import kotlin.math.abs
class SwissSolver(history: List<Game>, pairables: List<Pairable>, val method: Swiss.Method): Solver(history, pairables) { class SwissSolver(history: List<Game>, pairables: List<Pairable>, weights: Pairing.Weights, val method: Swiss.Method): Solver(history, pairables, weights) {
val PLAYED_WEIGHT = 1_000_000.0 // weight if players already met
val SCORE_WEIGHT = 10_000.0 // weight per difference of score
val PLACE_WEIGHT = 1_000.0 // weight per difference of place
override fun sort(p: Pairable, q: Pairable): Int = override fun sort(p: Pairable, q: Pairable): Int =
when (p.score) { when (p.score) {
@@ -19,20 +16,20 @@ class SwissSolver(history: List<Game>, pairables: List<Pairable>, val method: Sw
} }
override fun weight(p: Pairable, q: Pairable) = when { override fun weight(p: Pairable, q: Pairable) = when {
p.played(q) -> PLAYED_WEIGHT p.played(q) -> weights.played
p.score != q.score -> { p.score != q.score -> {
val placeWeight = val placeWeight =
if (p.score > q.score) (p.placeInGroup.second + q.placeInGroup.first) * PLACE_WEIGHT if (p.score > q.score) (p.placeInGroup.second + q.placeInGroup.first) * weights.place
else (q.placeInGroup.second + p.placeInGroup.first) * PLACE_WEIGHT else (q.placeInGroup.second + p.placeInGroup.first) * weights.place
abs(p.score - q.score) * SCORE_WEIGHT + placeWeight abs(p.score - q.score) * weights.score + placeWeight
} }
else -> when (method) { else -> when (method) {
SPLIT_AND_FOLD -> SPLIT_AND_FOLD ->
if (p.placeInGroup.first > q.placeInGroup.first) abs(p.placeInGroup.first - (q.placeInGroup.second - q.placeInGroup.first)) * PLACE_WEIGHT if (p.placeInGroup.first > q.placeInGroup.first) abs(p.placeInGroup.first - (q.placeInGroup.second - q.placeInGroup.first)) * weights.place
else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * PLACE_WEIGHT else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * weights.place
SPLIT_AND_RANDOM -> rand.nextDouble() * p.placeInGroup.second * PLACE_WEIGHT SPLIT_AND_RANDOM -> rand.nextDouble() * p.placeInGroup.second * weights.place
SPLIT_AND_SLIP -> abs(abs(p.placeInGroup.first - q.placeInGroup.first) - p.placeInGroup.second) * PLACE_WEIGHT SPLIT_AND_SLIP -> abs(abs(p.placeInGroup.first - q.placeInGroup.first) - p.placeInGroup.second) * weights.place
else -> throw Error("unhandled case") else -> throw Error("unhandled case")
} }
} } + (abs(p.colorBalance + 1) + abs(q.colorBalance - 1)) * weights.color
} }