added many tournament parameters and generic computing of base criterion.
This commit is contained in:
@@ -10,10 +10,15 @@ import kotlin.math.roundToInt
|
||||
// Pairable
|
||||
|
||||
sealed class Pairable(val id: ID, val name: String, open val rating: Int, open val rank: Int) {
|
||||
companion object {}
|
||||
companion object {
|
||||
val MIN_RANK: Int = -30 // 30k
|
||||
}
|
||||
abstract fun toJson(): Json.Object
|
||||
abstract val club: String?
|
||||
abstract val country: String?
|
||||
open fun nameSeed(): String {
|
||||
return name
|
||||
}
|
||||
val skip = mutableSetOf<Int>() // skipped rounds
|
||||
}
|
||||
|
||||
@@ -71,6 +76,9 @@ class Player(
|
||||
).also {
|
||||
if (skip.isNotEmpty()) it["skip"] = Json.Array(skip)
|
||||
}
|
||||
override fun nameSeed(): String {
|
||||
return name + firstname
|
||||
}
|
||||
}
|
||||
|
||||
fun Player.Companion.fromJson(json: Json.Object, default: Player? = null) = Player(
|
||||
|
@@ -2,47 +2,134 @@ package org.jeudego.pairgoth.model
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.ext.OpenGothaFormat
|
||||
import org.jeudego.pairgoth.model.Pairing.PairingType.*
|
||||
import org.jeudego.pairgoth.model.MacMahon
|
||||
import org.jeudego.pairgoth.model.RoundRobin
|
||||
import org.jeudego.pairgoth.model.Swiss
|
||||
import org.jeudego.pairgoth.pairing.MacMahonSolver
|
||||
import org.jeudego.pairgoth.pairing.SwissSolver
|
||||
import java.util.Random
|
||||
|
||||
sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) {
|
||||
// Below are some constants imported from opengotha
|
||||
/**
|
||||
* Max value for BaAvoidDuplGame.
|
||||
* In order to be compatible with max value of long (8 * 10^18),
|
||||
* with max number of games (8000),
|
||||
* with relative weight of this parameter (1/2)
|
||||
*BA_MAX_AVOIDDUPLGAME should be strictly limited to 5 * 10^14
|
||||
*/
|
||||
private const val BA_MAX_AVOIDDUPLGAME: Long = 500000000000000L // 5e14
|
||||
|
||||
/**
|
||||
* Max value for BaRandom.
|
||||
* Due to internal coding,
|
||||
* BA_MAX_RANDOM should be strictly limited to 2 * 10^9
|
||||
*/
|
||||
private const val BA_MAX_RANDOM: Long = 1000000000L // 2e9
|
||||
private const val BA_MAX_BALANCEWB: Long = 1000000L // 1e6
|
||||
|
||||
private const val MA_MAX_AVOID_MIXING_CATEGORIES: Double = 2e13
|
||||
// Ratio between MA_MAX_MINIMIZE_SCORE_DIFFERENCE and MA_MAX_AVOID_MIXING_CATEGORIES should stay below 1/ nbcat^2
|
||||
private const val MA_MAX_MINIMIZE_SCORE_DIFFERENCE: Double = 1e11
|
||||
private const val MA_MAX_DUDD_WEIGHT: Double = MA_MAX_MINIMIZE_SCORE_DIFFERENCE / 1000; // Draw-ups Draw-downs
|
||||
enum class MA_DUDD {TOP, MID, BOT}
|
||||
|
||||
private const val MA_MAX_MAXIMIZE_SEEDING: Double = MA_MAX_MINIMIZE_SCORE_DIFFERENCE / 20000;
|
||||
|
||||
enum class SeedMethod { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
|
||||
|
||||
sealed class Pairing(val type: PairingType, val pairingParams: PairingParams = PairingParams()) {
|
||||
companion object {}
|
||||
enum class PairingType { SWISS, MACMAHON, ROUNDROBIN }
|
||||
data class Weights(
|
||||
val played: Double = 1_000_000.0, // players already met
|
||||
val group: Double = 100_000.0, // different group
|
||||
val handicap: Double = 50_000.0, // for each handicap stone
|
||||
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 = 500.0, // per color unbalancing
|
||||
val club: Double = 100.0, // same club weight
|
||||
val country: Double = 50.0 // same country
|
||||
enum class PairingType { SWISS, MAC_MAHON, ROUND_ROBIN }
|
||||
data class PairingParams(
|
||||
// Standard NX1 factor ( = Rather N X 1 than 1 X N)
|
||||
val standardNX1Factor: Double = 0.5,
|
||||
// Base criteria
|
||||
val baseAvoidDuplGame: Long = BA_MAX_AVOIDDUPLGAME,
|
||||
val baseRandom: Long = 0,
|
||||
val baseDeterministic: Boolean = true,
|
||||
val baseBalanceWB: Long = BA_MAX_BALANCEWB,
|
||||
|
||||
// Main criteria
|
||||
// TODO move avoidmixingcategories to swiss with category
|
||||
//val maAvoidMixingCategories: Double = MA_MAX_AVOID_MIXING_CATEGORIES,
|
||||
val mainMinimizeScoreDifference: Double = MA_MAX_MINIMIZE_SCORE_DIFFERENCE,
|
||||
|
||||
val maDUDDWeight: Double = MA_MAX_DUDD_WEIGHT,
|
||||
val maCompensateDUDD: Boolean = true,
|
||||
val maDUDDUpperMode: MA_DUDD = MA_DUDD.MID,
|
||||
val maDUDDLowerMode: MA_DUDD = MA_DUDD.MID,
|
||||
|
||||
val maMaximizeSeeding: Double = MA_MAX_MAXIMIZE_SEEDING, // 5 *10^6
|
||||
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,
|
||||
|
||||
// Secondary criteria
|
||||
val seBarThresholdActive: Boolean = true, // Do not apply secondary criteria for players above bar
|
||||
val seRankThreshold: Int = 0, // Do not apply secondary criteria above 1D rank
|
||||
val seNbWinsThresholdActive: Boolean = true, // Do not apply secondary criteria when nbWins >= nbRounds / 2
|
||||
val seDefSecCrit: Double = MA_MAX_AVOID_MIXING_CATEGORIES, // Should be MA_MAX_MINIMIZE_SCORE_DIFFERENCE for MM, MA_MAX_AVOID_MIXING_CATEGORIES for others
|
||||
|
||||
// Geographical params
|
||||
val geo: GeographicalParams = GeographicalParams(avoidSameGeo = seDefSecCrit),
|
||||
|
||||
// Handicap related settings
|
||||
val hd: HandicapParams = HandicapParams(minimizeHandicap = seDefSecCrit),
|
||||
)
|
||||
|
||||
|
||||
abstract fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game>
|
||||
}
|
||||
|
||||
data class GeographicalParams(
|
||||
val avoidSameGeo: Double, // Should be SeDefSecCrit for SwCat and MM, 0 for Swiss
|
||||
val preferMMSDiffRatherThanSameCountry: Int = 1, // Typically = 1
|
||||
val preferMMSDiffRatherThanSameClubsGroup: Int = 2, // Typically = 2
|
||||
val preferMMSDiffRatherThanSameClub: Int = 3, // Typically = 3
|
||||
) {
|
||||
companion object {
|
||||
fun disabled() = GeographicalParams(avoidSameGeo = 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
data class HandicapParams(
|
||||
// minimizeHandicap is a secondary criteria but moved here
|
||||
val minimizeHandicap: Double, // Should be paiSeDefSecCrit for SwCat, 0 for others
|
||||
val basedOnMMS: Boolean = true, // if hdBasedOnMMS is false, hd will be based on rank
|
||||
// When one player in the game has a rank of at least hdNoHdRankThreshold,
|
||||
// then the game will be without handicap
|
||||
val noHdRankThreshold: Int = 0, // 0 is 1d
|
||||
val correction: Int = 1, // Handicap will be decreased by hdCorrection
|
||||
val ceiling: Int = 9, // Possible values are between 0 and 9
|
||||
) {
|
||||
companion object {
|
||||
fun disabled() = HandicapParams(
|
||||
minimizeHandicap = 0.0,
|
||||
basedOnMMS = false,
|
||||
noHdRankThreshold=-30, // 30k
|
||||
ceiling=0)
|
||||
}
|
||||
}
|
||||
|
||||
fun Tournament<*>.historyBefore(round: Int) =
|
||||
if (lastRound() == 0) emptyList()
|
||||
else (0 until round).flatMap { games(round).values }
|
||||
|
||||
class Swiss(
|
||||
var method: Method,
|
||||
var firstRoundMethod: Method = method,
|
||||
): Pairing(SWISS, Weights(
|
||||
handicap = 0.0, // no handicap games anyway
|
||||
club = 0.0,
|
||||
country = 0.0
|
||||
class Swiss(): Pairing(SWISS, PairingParams(
|
||||
maSeedSystem1 = SeedMethod.SPLIT_AND_SLIP,
|
||||
maSeedSystem2 = SeedMethod.SPLIT_AND_SLIP,
|
||||
|
||||
seBarThresholdActive = true, // not relevant
|
||||
seRankThreshold = -30,
|
||||
seNbWinsThresholdActive = true, // not relevant
|
||||
seDefSecCrit = MA_MAX_AVOID_MIXING_CATEGORIES,
|
||||
|
||||
geo = GeographicalParams.disabled(),
|
||||
hd = HandicapParams.disabled(),
|
||||
)) {
|
||||
enum class Method { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
|
||||
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||
val actualMethod = if (round == 1) firstRoundMethod else method
|
||||
return SwissSolver(tournament.historyBefore(round), pairables, weights, actualMethod).pair()
|
||||
return SwissSolver(tournament.historyBefore(round), pairables, pairingParams).pair()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,39 +137,64 @@ class MacMahon(
|
||||
var bar: Int = 0,
|
||||
var minLevel: Int = -30,
|
||||
var reducer: Int = 1
|
||||
): Pairing(MACMAHON) {
|
||||
): Pairing(MAC_MAHON, PairingParams(seDefSecCrit = MA_MAX_MINIMIZE_SCORE_DIFFERENCE)) {
|
||||
val groups = mutableListOf<Int>()
|
||||
|
||||
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||
return MacMahonSolver(tournament.historyBefore(round), pairables, weights, mmBase = minLevel, mmBar = bar, reducer = reducer).pair()
|
||||
return MacMahonSolver(tournament.historyBefore(round), pairables, pairingParams, mmBase = minLevel, mmBar = bar, reducer = reducer).pair()
|
||||
}
|
||||
}
|
||||
|
||||
class RoundRobin: Pairing(ROUNDROBIN) {
|
||||
class RoundRobin: Pairing(ROUND_ROBIN) {
|
||||
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
// Serialization
|
||||
// TODO failing on serialization
|
||||
fun HandicapParams.toJson() = Json.Object(
|
||||
"minimize_hd" to minimizeHandicap,
|
||||
"mms_based" to basedOnMMS,
|
||||
"no_hd_thresh" to noHdRankThreshold,
|
||||
"correction" to correction,
|
||||
"ceiling" to ceiling, )
|
||||
|
||||
fun HandicapParams.fromJson(json: Json.Object) = HandicapParams(
|
||||
minimizeHandicap=json.getDouble("minimize_hd")!!,
|
||||
basedOnMMS=json.getBoolean("mms_based")!!,
|
||||
noHdRankThreshold=json.getInt("no_hd_thresh")!!,
|
||||
correction=json.getInt("correction")!!,
|
||||
ceiling=json.getInt("ceiling")!!,
|
||||
)
|
||||
|
||||
fun GeographicalParams.toJson() = Json.Object(
|
||||
"avoid_same_geo" to avoidSameGeo,
|
||||
"country" to preferMMSDiffRatherThanSameCountry,
|
||||
"club_group" to preferMMSDiffRatherThanSameClubsGroup,
|
||||
"club" to preferMMSDiffRatherThanSameClub,)
|
||||
|
||||
fun GeographicalParams.fromJson(json: Json.Object) = GeographicalParams(
|
||||
avoidSameGeo=json.getDouble("avoid_same_geo")!!,
|
||||
preferMMSDiffRatherThanSameCountry=json.getInt("country")!!,
|
||||
preferMMSDiffRatherThanSameClubsGroup=json.getInt("club_group")!!,
|
||||
preferMMSDiffRatherThanSameClub=json.getInt("club")!!,
|
||||
)
|
||||
|
||||
|
||||
fun Pairing.Companion.fromJson(json: Json.Object) = when (json.getString("type")?.let { Pairing.PairingType.valueOf(it) } ?: badRequest("missing pairing type")) {
|
||||
SWISS -> Swiss(
|
||||
method = json.getString("method")?.let { Swiss.Method.valueOf(it) } ?: badRequest("missing pairing method"),
|
||||
firstRoundMethod = json.getString("firstRoundMethod")?.let { Swiss.Method.valueOf(it) } ?: json.getString("method")!!.let { Swiss.Method.valueOf(it) }
|
||||
)
|
||||
MACMAHON -> MacMahon(
|
||||
SWISS -> Swiss()
|
||||
MAC_MAHON -> MacMahon(
|
||||
bar = json.getInt("bar") ?: 0,
|
||||
minLevel = json.getInt("minLevel") ?: -30,
|
||||
reducer = json.getInt("reducer") ?: 1
|
||||
)
|
||||
ROUNDROBIN -> RoundRobin()
|
||||
ROUND_ROBIN -> RoundRobin()
|
||||
}
|
||||
|
||||
fun Pairing.toJson() = when (this) {
|
||||
is Swiss ->
|
||||
if (method == firstRoundMethod) Json.Object("type" to type.name, "method" to method.name)
|
||||
else Json.Object("type" to type.name, "method" to method.name, "firstRoundMethod" to firstRoundMethod.name)
|
||||
Json.Object("type" to type.name, "geo" to pairingParams.geo.toJson(), "hd" to pairingParams.hd.toJson())
|
||||
is MacMahon -> Json.Object("type" to type.name, "bar" to bar, "minLevel" to minLevel, "reducer" to reducer)
|
||||
is RoundRobin -> Json.Object("type" to type.name)
|
||||
}
|
||||
|
@@ -72,7 +72,7 @@ sealed class Tournament <P: Pairable>(
|
||||
|
||||
// standings criteria
|
||||
val criteria = mutableListOf<Criterion>(
|
||||
if (pairing.type == Pairing.PairingType.MACMAHON) Criterion.MMS else Criterion.NBW,
|
||||
if (pairing.type == Pairing.PairingType.MAC_MAHON) Criterion.MMS else Criterion.NBW,
|
||||
Criterion.SOS,
|
||||
Criterion.SOSOS
|
||||
)
|
||||
|
@@ -21,9 +21,14 @@ open class HistoryHelper(protected val history: List<Game>) {
|
||||
}).toSet()
|
||||
}
|
||||
|
||||
// Returns the number of games played as white
|
||||
// Only count games without handicap
|
||||
private val colorBalance: Map<Int, Int> by lazy {
|
||||
history.flatMap { game ->
|
||||
history.flatMap { game -> if (game.handicap == 0) {
|
||||
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 ->
|
||||
acc + next.second
|
||||
}
|
||||
|
@@ -3,43 +3,27 @@ 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.Swiss.Method.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.math.sign
|
||||
|
||||
class MacMahonSolver(history: List<Game>, pairables: List<Pairable>, weights: Pairing.Weights, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(history, pairables, weights) {
|
||||
class MacMahonSolver(history: List<Game>, pairables: List<Pairable>, pairingParams: Pairing.PairingParams, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(history, pairables, pairingParams) {
|
||||
|
||||
val Pairable.mms get() = mmBase + score
|
||||
|
||||
// CB TODO - configurable criteria
|
||||
override fun mainCriterion(p1: Pairable): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
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 weight(black: Pairable, white: Pairable): Double {
|
||||
var weight = 0.0
|
||||
if (black.played(white)) weight += weights.played
|
||||
if (black.club == white.club) weight += weights.club
|
||||
if (black.country == white.country) weight += weights.country
|
||||
weight += (abs(black.colorBalance + 1) + abs(white.colorBalance - 1)) * weights.color
|
||||
|
||||
// MacMahon specific
|
||||
weight += Math.abs(black.mms - white.mms) * weights.score
|
||||
if (sign(mmBar - black.mms) != sign(mmBar - white.mms)) weight += weights.group
|
||||
|
||||
if (black.mms < mmBar && white.mms < mmBar && abs(black.mms - white.mms) > reducer) {
|
||||
if (black.mms > white.mms) weight = Double.NaN
|
||||
else weight = handicap(black, white) * weights.handicap
|
||||
}
|
||||
return weight
|
||||
}
|
||||
|
||||
override fun handicap(black: Pairable, white: Pairable) =
|
||||
if (black.mms > mmBar || white.mms > mmBar || abs(black.mms - white.mms) < reducer || black.mms > white.mms) 0
|
||||
else (white.mms - black.mms - reducer).roundToInt()
|
||||
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package org.jeudego.pairgoth.pairing
|
||||
|
||||
import org.jeudego.pairgoth.model.Game
|
||||
import org.jeudego.pairgoth.model.Game.Result.*
|
||||
import org.jeudego.pairgoth.model.Pairable
|
||||
import org.jeudego.pairgoth.model.Pairing
|
||||
import org.jeudego.pairgoth.model.TeamTournament
|
||||
@@ -10,27 +9,166 @@ import org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching
|
||||
import org.jgrapht.alg.matching.blossom.v5.ObjectiveSense
|
||||
import org.jgrapht.graph.DefaultWeightedEdge
|
||||
import org.jgrapht.graph.SimpleDirectedWeightedGraph
|
||||
import org.jgrapht.graph.SimpleWeightedGraph
|
||||
import org.jgrapht.graph.builder.GraphBuilder
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
interface HistoryDigester {
|
||||
val colorBalance: Map<Int, Int>
|
||||
val score: Map<Int, Double>
|
||||
val sos: Map<Int, Double>
|
||||
val sosos: Map<Int, Double>
|
||||
val sodos: Map<Int, Double>
|
||||
|
||||
private fun detRandom(max: Long, p1: Pairable, p2: Pairable): Long {
|
||||
var nR: Long = 0
|
||||
var inverse = false
|
||||
|
||||
val seed1 = p1.nameSeed()
|
||||
val seed2 = p2.nameSeed()
|
||||
|
||||
var name1 = seed1
|
||||
var name2 = seed2
|
||||
if (name1 > name2) {
|
||||
name1 = name2.also { name2 = name1 }
|
||||
inverse = true
|
||||
}
|
||||
val s = name1 + name2
|
||||
for (i in s.indices) {
|
||||
val c = s[i]
|
||||
nR += (c.code * (i + 1)).toLong()
|
||||
}
|
||||
nR = nR * 1234567 % (max + 1)
|
||||
if (inverse) nR = max - nR
|
||||
return nR
|
||||
}
|
||||
|
||||
sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val weights: Pairing.Weights) {
|
||||
private fun nonDetRandom(max: Long): Long {
|
||||
if (max == 0L) {
|
||||
return 0
|
||||
}
|
||||
val r = Math.random() * (max + 1)
|
||||
return r.toLong()
|
||||
}
|
||||
|
||||
sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val pairingParams: Pairing.PairingParams) {
|
||||
|
||||
companion object {
|
||||
val rand = Random(/* seed from properties - TODO */)
|
||||
}
|
||||
|
||||
open fun sort(p: Pairable, q: Pairable): Int = 0 // no sort by default
|
||||
abstract fun weight(black: Pairable, white: Pairable): Double
|
||||
open fun handicap(black: Pairable, white: Pairable) = 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)
|
||||
|
||||
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>
|
||||
|
||||
// Base criteria
|
||||
open fun avoidDuplicatingGames(p1: Pairable, p2: Pairable): Long {
|
||||
if (historyHelper.playedTogether(p1, p2)) {
|
||||
return pairingParams.baseAvoidDuplGame
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
open fun applyRandom(p1: Pairable, p2: Pairable): Long {
|
||||
if (pairingParams.baseDeterministic) {
|
||||
return detRandom(pairingParams.baseRandom, p1, p2)
|
||||
} else {
|
||||
return nonDetRandom(pairingParams.baseRandom)
|
||||
}
|
||||
}
|
||||
|
||||
open fun applyBalanceBW(p1: Pairable, p2: Pairable): Long {
|
||||
// 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.baseBalanceWB
|
||||
} else if (wb1 == 0 && abs(wb2) >= 2) {
|
||||
return pairingParams.baseBalanceWB / 2
|
||||
} else if (wb2 == 0 && abs(wb1) >= 2) {
|
||||
return pairingParams.baseBalanceWB / 2
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
open fun applyBaseCriteria(p1: Pairable, p2: Pairable): Long {
|
||||
var score = 0L
|
||||
|
||||
// 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)
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
// Main criteria
|
||||
open fun applyMainCriteria(p1: Pairable, p2: Pairable): Long {
|
||||
var score = 0L;
|
||||
|
||||
// Main criterion 1 avoid mixing category is moved to Swiss with category
|
||||
// TODO
|
||||
|
||||
// Main criterion 2 minimize score difference
|
||||
score += minimizeScoreDifference(p1, p2)
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
open fun minimizeScoreDifference(p1: Pairable, p2: Pairable): Long {
|
||||
var scoCost: Long = 0
|
||||
val scoRange: 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 k: Double = pairingParams.standardNX1Factor
|
||||
scoCost = (pairingParams.mainMinimizeScoreDifference * (1.0 - x) * (1.0 + k * x)) as Long
|
||||
|
||||
return scoCost
|
||||
}
|
||||
|
||||
// Handicap functions
|
||||
open fun handicap(p1: Pairable, p2: Pairable): Int {
|
||||
var hd = 0
|
||||
var pseudoRank1: Int = p1.rank
|
||||
var pseudoRank2: Int = p2.rank
|
||||
|
||||
pseudoRank1 = min(pseudoRank1, pairingParams.hd.noHdRankThreshold)
|
||||
pseudoRank2 = min(pseudoRank2, pairingParams.hd.noHdRankThreshold)
|
||||
hd = pseudoRank1 - pseudoRank2
|
||||
|
||||
return clampHandicap(hd)
|
||||
}
|
||||
|
||||
open fun clampHandicap(input_hd: Int): Int {
|
||||
var hd = input_hd
|
||||
if (hd > 0) {
|
||||
hd -= pairingParams.hd.correction
|
||||
hd = min(hd, 0)
|
||||
}
|
||||
if (hd < 0) {
|
||||
hd += pairingParams.hd.correction
|
||||
hd = max(hd, 0)
|
||||
}
|
||||
// Clamp handicap with ceiling
|
||||
hd = min(hd, pairingParams.hd.ceiling)
|
||||
hd = max(hd, -pairingParams.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)))
|
||||
@@ -58,6 +196,15 @@ sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val weig
|
||||
return result
|
||||
}
|
||||
|
||||
private fun computeGroups(): Pair<Map<Int, Int>, Int> {
|
||||
val (mainScoreMin, mainScoreMax) = mainCriterionMinMax()
|
||||
|
||||
// TODO categories
|
||||
val groups: Map<Int, Int> = pairables.associate { pairable -> Pair(pairable.id, mainCriterion(pairable)) }
|
||||
|
||||
return Pair(groups, mainScoreMax - mainScoreMin)
|
||||
}
|
||||
|
||||
// Calculation parameters
|
||||
|
||||
val n = pairables.size
|
||||
@@ -66,6 +213,10 @@ sealed class Solver(history: List<Game>, val pairables: List<Pairable>, val weig
|
||||
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 numberGroups = groupsResult.second
|
||||
|
||||
// pairables sorted using overloadable sort function
|
||||
private val sortedPairables by lazy {
|
||||
pairables.sortedWith(::sort)
|
||||
|
@@ -3,11 +3,9 @@ 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.Swiss
|
||||
import org.jeudego.pairgoth.model.Swiss.Method.*
|
||||
import kotlin.math.abs
|
||||
|
||||
class SwissSolver(history: List<Game>, pairables: List<Pairable>, weights: Pairing.Weights, val method: Swiss.Method): Solver(history, pairables, weights) {
|
||||
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) {
|
||||
@@ -15,25 +13,11 @@ class SwissSolver(history: List<Game>, pairables: List<Pairable>, weights: Pairi
|
||||
else -> ((q.score - p.score) * 1000).toInt()
|
||||
}
|
||||
|
||||
override fun weight(black: Pairable, white: Pairable): Double {
|
||||
var weight = 0.0
|
||||
if (black.played(white)) weight += weights.played
|
||||
if (black.score != white.score) {
|
||||
val placeWeight =
|
||||
if (black.score > white.score) (black.placeInGroup.second + white.placeInGroup.first) * weights.place
|
||||
else (white.placeInGroup.second + black.placeInGroup.first) * weights.place
|
||||
weight += abs(black.score - white.score) * weights.score + placeWeight
|
||||
} else {
|
||||
weight += when (method) {
|
||||
SPLIT_AND_FOLD ->
|
||||
if (black.placeInGroup.first > white.placeInGroup.first) abs(black.placeInGroup.first - (white.placeInGroup.second - white.placeInGroup.first)) * weights.place
|
||||
else abs(white.placeInGroup.first - (black.placeInGroup.second - black.placeInGroup.first)) * weights.place
|
||||
override fun mainCriterion(p1: Pairable): Int {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
SPLIT_AND_RANDOM -> rand.nextDouble() * black.placeInGroup.second * weights.place
|
||||
SPLIT_AND_SLIP -> abs(abs(black.placeInGroup.first - white.placeInGroup.first) - black.placeInGroup.second) * weights.place
|
||||
}
|
||||
}
|
||||
weight += (abs(black.colorBalance + 1) + abs(white.colorBalance - 1)) * weights.color
|
||||
return weight
|
||||
override fun mainCriterionMinMax(): Pair<Int, Int> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user