From 29013940524df09b034b4d1f63869c9a491f49c9 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Mon, 19 Jun 2023 15:39:46 +0200 Subject: [PATCH] Reeng pairing in progress --- .../jeudego/pairgoth/api/PairingHandler.kt | 3 +- .../jeudego/pairgoth/api/TournamentHandler.kt | 1 - .../org/jeudego/pairgoth/model/Pairing.kt | 419 +++++++++++------- .../org/jeudego/pairgoth/model/Placement.kt | 37 +- .../org/jeudego/pairgoth/model/Tournament.kt | 11 - .../jeudego/pairgoth/pairing/HistoryHelper.kt | 18 +- .../pairgoth/pairing/MacMahonSolver.kt | 24 +- .../org/jeudego/pairgoth/pairing/Solver.kt | 122 +++-- .../jeudego/pairgoth/pairing/SwissSolver.kt | 19 +- 9 files changed, 368 insertions(+), 286 deletions(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index 5b0c7b2..d484279 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -4,6 +4,7 @@ import com.republicate.kson.Json import com.republicate.kson.toJsonArray import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.model.Pairing +import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.toID import org.jeudego.pairgoth.model.toJson @@ -28,7 +29,7 @@ object PairingHandler: PairgothApiHandler { val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val payload = getArrayPayload(request) val allPlayers = payload.size == 1 && payload[0] == "all" - if (!allPlayers && tournament.pairing.type == Pairing.PairingType.SWISS) badRequest("Swiss pairing requires all pairable players") + if (!allPlayers && tournament.pairing.type == PairingType.SWISS) badRequest("Swiss pairing requires all pairable players") val playing = (tournament.games(round).values).flatMap { listOf(it.black, it.white) }.toSet() diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt index fdd7e1a..a1a2187 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -64,7 +64,6 @@ object TournamentHandler: PairgothApiHandler { clear() putAll(tournament.games(round)) } - updated.criteria.addAll(tournament.criteria) Store.replaceTournament(updated) tournament.dispatchEvent(tournamentUpdated, tournament.toJson()) return Json.Object("success" to true) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt index fcce2d4..f37f023 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt @@ -2,200 +2,305 @@ 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.PairingType.* +import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.* import org.jeudego.pairgoth.pairing.MacMahonSolver import org.jeudego.pairgoth.pairing.SwissSolver -// 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 +// base pairing parameters +data class BasePairingParams( + // standard NX1 factor for concavity curves + val nx1: Double = 0.5, + val dupWeight: Double = MAX_AVOIDDUPGAME, + val random: Double = 0.0, + val deterministic: Boolean = true, + val colorBalance: Double = MAX_COLOR_BALANCE +) { + init { + if (nx1 < 0.0 || nx1 > 1.0) throw Error("invalid standardNX1Factor") + if (dupWeight < 0.0 || dupWeight > MAX_AVOIDDUPGAME) throw Error("invalid avoidDuplGame value") + if (random < 0.0 || random > MAX_RANDOM) throw Error("invalid random") + if (colorBalance > 0.0 || colorBalance > MAX_COLOR_BALANCE) throw Error("invalid balanceWB") + } -/** - * 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 + companion object { + 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() + } +} -private const val MA_MAX_AVOID_MIXING_CATEGORIES: Long = 20000000000000L // 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: Long = 100000000000L // 1e11 -private const val MA_MAX_DUDD_WEIGHT: Long = MA_MAX_MINIMIZE_SCORE_DIFFERENCE / 1000; // Draw-ups Draw-downs -enum class MA_DUDD {TOP, MID, BOT} +// main criterium parameters +data class MainCritParams( + // TB - TODO move avoidmixingcategories to swiss with category? + val categoriesWeight: Double = MAX_CATEGORIES_WEIGHT, // opengotha avoidMixingCategories + val scoreWeight: Double = MAX_SCORE_WEIGHT, // opengotha minimizeScoreDifference + val drawUpDownWeight: Double = MAX_DRAW_UP_DOWN_WEIGHT, // opengotha DUDDWeight + val compensateDrawUpDown: Boolean = true, + val drawUpDownUpperMode: DrawUpDown = DrawUpDown.MIDDLE, + val drawUpDownLowerMode: DrawUpDown = DrawUpDown.MIDDLE, + val seedingWeight: Double = MAX_SEEDING_WEIGHT, // 5 *10^6, opengotha maximizeSeeding + val lastRoundForSeedSystem1: Int = 1, + val seedSystem1: SeedMethod = SeedMethod.SPLIT_AND_RANDOM, + val seedSystem2: SeedMethod = SeedMethod.SPLIT_AND_FOLD, + val additionalPlacementCritSystem1: Criterion = Criterion.RATING, + val additionalPlacementCritSystem2: Criterion = Criterion.NONE, +) { + enum class DrawUpDown {TOP, MIDDLE, BOTTOM} + enum class SeedMethod { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP } + companion object { + const val MAX_CATEGORIES_WEIGHT = 20000000000000.0 // 2e13 + // Ratio between MAX_SCORE_WEIGHT and MAX_CATEGORIES_WEIGHT should stay below 1/ nbcat^2 + const val MAX_SCORE_WEIGHT = 100000000000.0 // 1e11 + const val MAX_DRAW_UP_DOWN_WEIGHT = MAX_SCORE_WEIGHT / 1000.0; // Draw-ups Draw-downs + const val MAX_SEEDING_WEIGHT = MAX_SCORE_WEIGHT / 20000.0; + val default = MainCritParams() + } +} -private const val MA_MAX_MAXIMIZE_SEEDING: Long = MA_MAX_MINIMIZE_SCORE_DIFFERENCE / 20000; +// secondary criterium parameters +data class SecondaryCritParams( + val barThresholdActive: Boolean = true, // Do not apply secondary criteria for players above bar + val rankThreshold: Int = 0, // Do not apply secondary criteria above 1D rank + val nbWinsThresholdActive: Boolean = true, // Do not apply secondary criteria when nbWins >= nbRounds / 2 + val defSecCrit: Double = MainCritParams.MAX_CATEGORIES_WEIGHT, // Should be MA_MAX_MINIMIZE_SCORE_DIFFERENCE for MM, MA_MAX_AVOID_MIXING_CATEGORIES for others +) { + companion object { + val default = SecondaryCritParams() + } +} -enum class SeedMethod { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP } +// geographical pairing params +data class GeographicalParams( + val avoidSameGeo: Double = 0.0, // Should be SecondaryCritParams.defSecCrit 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 { + val disabled = GeographicalParams(avoidSameGeo = 0.0) + } +} -sealed class Pairing(val type: PairingType, val pairingParams: PairingParams = PairingParams(), val placementParams: PlacementParams) { +// handicap params +data class HandicapParams( + // minimizeHandicap is a secondary criteria but moved here + val weight: Double = 0.0, // Should be paiSeDefSecCrit for SwCat, 0 for others + val useMMS: Boolean = true, // if useMMS 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 rankThreshold: 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 { + val default = HandicapParams( + weight = 0.0, // default disables handicap + useMMS = false, + rankThreshold = -30, // 30k + ceiling = 0) + } +} + +enum class PairingType { SWISS, MAC_MAHON, ROUND_ROBIN } + +data class PairingParams( + val base: BasePairingParams = BasePairingParams(), + val main: MainCritParams = MainCritParams(), + val secondary: SecondaryCritParams = SecondaryCritParams(), + val geo: GeographicalParams = GeographicalParams(), + val handicap: HandicapParams = HandicapParams() +) + +sealed class Pairing( + val type: PairingType, + val pairingParams: PairingParams, + val placementParams: PlacementParams) { companion object {} - 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: Long = MA_MAX_MINIMIZE_SCORE_DIFFERENCE, - - val maDUDDWeight: Long = MA_MAX_DUDD_WEIGHT, - val maCompensateDUDD: Boolean = true, - val maDUDDUpperMode: MA_DUDD = MA_DUDD.MID, - val maDUDDLowerMode: MA_DUDD = MA_DUDD.MID, - - val maMaximizeSeeding: Long = 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, - val maAdditionalPlacementCritSystem1: PlacementCriterion = PlacementCriterion.RATING, - val maAdditionalPlacementCritSystem2: PlacementCriterion = PlacementCriterion.NULL, - - // 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: Long = 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): List } -data class GeographicalParams( - val avoidSameGeo: Long, // 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 = 0L) - } -} - -data class HandicapParams( - // minimizeHandicap is a secondary criteria but moved here - val minimizeHandicap: Long, // 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 = 0L, - basedOnMMS = false, - noHdRankThreshold=-30, // 30k - ceiling=0) - } -} - -fun Tournament<*>.historyBefore(round: Int) = +private fun Tournament<*>.historyBefore(round: Int) = if (lastRound() == 0) emptyList() else (0 until round).flatMap { games(round).values } -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(), -), PlacementParams(PlacementCriterion.NBW, PlacementCriterion.SOSW, PlacementCriterion.SOSOSW)) { +class Swiss( + pairingParams: PairingParams = PairingParams( + base = BasePairingParams(), + main = MainCritParams( + seedSystem1 = SPLIT_AND_SLIP, + seedSystem2 = SPLIT_AND_SLIP + ), + secondary = SecondaryCritParams( + barThresholdActive = true, + rankThreshold = -30, + nbWinsThresholdActive = true, + defSecCrit = MainCritParams.MAX_CATEGORIES_WEIGHT + ), + geo = GeographicalParams.disabled, + handicap = HandicapParams.default + ), + placementParams: PlacementParams = PlacementParams( + Criterion.NBW, Criterion.SOSW, Criterion.SOSOSW + ) +): Pairing(SWISS, pairingParams, placementParams) { + companion object {} override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { return SwissSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams).pair() } } class MacMahon( - var bar: Int = 0, - var minLevel: Int = -30, - var reducer: Int = 1 -): Pairing(MAC_MAHON, PairingParams(seDefSecCrit = MA_MAX_MINIMIZE_SCORE_DIFFERENCE), - PlacementParams(PlacementCriterion.MMS, PlacementCriterion.SOSM, PlacementCriterion.SOSOSM)) { - val groups = mutableListOf() - + pairingParams: PairingParams = PairingParams( + base = BasePairingParams(), + main = MainCritParams(), + secondary = SecondaryCritParams( + defSecCrit = MainCritParams.MAX_SCORE_WEIGHT + ), + geo = GeographicalParams( + avoidSameGeo = MainCritParams.MAX_SCORE_WEIGHT + ), + handicap = HandicapParams() + ), + placementParams: PlacementParams = PlacementParams( + Criterion.NBW, Criterion.SOSW, Criterion.SOSOSW + ) +): Pairing(MAC_MAHON, pairingParams, placementParams) { + companion object {} override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { - return MacMahonSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams, mmBase = minLevel, mmBar = bar, reducer = reducer).pair() + return MacMahonSolver(round, tournament.historyBefore(round), pairables, pairingParams, placementParams).pair() } } -class RoundRobin: Pairing(ROUND_ROBIN, PairingParams(), PlacementParams(PlacementCriterion.NBW, PlacementCriterion.RATING)) { +class RoundRobin( + pairingParams: PairingParams = PairingParams(), + placementParams: PlacementParams = PlacementParams(Criterion.NBW, Criterion.RATING) +): Pairing(ROUND_ROBIN, pairingParams, placementParams) { override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { 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.getLong("minimize_hd")!!, - basedOnMMS=json.getBoolean("mms_based")!!, - noHdRankThreshold=json.getInt("no_hd_thresh")!!, - correction=json.getInt("correction")!!, - ceiling=json.getInt("ceiling")!!, +fun BasePairingParams.Companion.fromJson(json: Json.Object) = BasePairingParams( + nx1 = json.getDouble("nx1") ?: default.nx1, + dupWeight = json.getDouble("dupWeight") ?: default.dupWeight, + random = json.getDouble("random") ?: default.random, + deterministic = json.getBoolean("deterministic") ?: default.deterministic, + colorBalance = json.getDouble("colorBalanceWeight") ?: default.colorBalance +) + +fun BasePairingParams.toJson() = Json.Object( + "nx1" to nx1, + "dupWeight" to dupWeight, + "random" to random, + "colorBalanceWeight" to colorBalance +) + +fun MainCritParams.Companion.fromJson(json: Json.Object) = MainCritParams( + categoriesWeight = json.getDouble("catWeight") ?: default.categoriesWeight, + scoreWeight = json.getDouble("scoreWeight") ?: default.scoreWeight, + drawUpDownWeight = json.getDouble("upDownWeight") ?: default.drawUpDownWeight, + compensateDrawUpDown = json.getBoolean("upDownCompensate") ?: default.compensateDrawUpDown, + drawUpDownLowerMode = json.getString("upDownLowerMode")?.let { MainCritParams.DrawUpDown.valueOf(it) } ?: default.drawUpDownLowerMode, + drawUpDownUpperMode = json.getString("upDownUpperMode")?.let { MainCritParams.DrawUpDown.valueOf(it) } ?: default.drawUpDownUpperMode, + seedingWeight = json.getDouble("maximizeSeeding") ?: default.seedingWeight, + lastRoundForSeedSystem1 = json.getInt("firstSeedLastRound") ?: default.lastRoundForSeedSystem1, + seedSystem1 = json.getString("firstSeed")?.let { MainCritParams.SeedMethod.valueOf(it) } ?: default.seedSystem1, + seedSystem2 = json.getString("secondSeed")?.let { MainCritParams.SeedMethod.valueOf(it) } ?: default.seedSystem2, + additionalPlacementCritSystem1 = json.getString("firstSeedAddCrit")?.let { Criterion.valueOf(it) } ?: default.additionalPlacementCritSystem1, + additionalPlacementCritSystem2 = json.getString("secondSeedAddCrit")?.let { Criterion.valueOf(it) } ?: default.additionalPlacementCritSystem2 +) + +fun MainCritParams.toJson() = Json.Object( + "catWeight" to categoriesWeight, + "scoreWeight" to scoreWeight, + "upDownWeight" to drawUpDownWeight, + "upDownCompensate" to compensateDrawUpDown, + "upDownLowerMode" to drawUpDownLowerMode, + "upDownUpperMode" to drawUpDownUpperMode, + "maximizeSeeding" to seedingWeight, + "firstSeedLastRound" to lastRoundForSeedSystem1, + "firstSeed" to seedSystem1, + "secondSeed" to seedSystem2, + "firstSeedAddCrit" to additionalPlacementCritSystem1, + "secondSeedAddCrit" to additionalPlacementCritSystem2 +) + +fun SecondaryCritParams.Companion.fromJson(json: Json.Object) = SecondaryCritParams( + barThresholdActive = json.getBoolean("barTreshold") ?: default.barThresholdActive, + rankThreshold = json.getInt("rankTreshold") ?: default.rankThreshold, + nbWinsThresholdActive = json.getBoolean("winsTreshold") ?: default.nbWinsThresholdActive, + defSecCrit = json.getDouble("secWeight") ?: default.defSecCrit +) + +fun SecondaryCritParams.toJson() = Json.Object( + "barTreshold" to barThresholdActive, + "rankTreshold" to rankThreshold, + "winsTreshold" to nbWinsThresholdActive, + "secWeight" to defSecCrit +) + +fun GeographicalParams.Companion.fromJson(json: Json.Object) = GeographicalParams( + avoidSameGeo = json.getDouble("weight") ?: disabled.avoidSameGeo, + preferMMSDiffRatherThanSameCountry = json.getInt("mmsDiffCountry") ?: disabled.preferMMSDiffRatherThanSameCountry, + preferMMSDiffRatherThanSameClubsGroup = json.getInt("mmsDiffClubGroup") ?: disabled.preferMMSDiffRatherThanSameClubsGroup, + preferMMSDiffRatherThanSameClub = json.getInt("mmsDiffClub") ?: disabled.preferMMSDiffRatherThanSameClub ) 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.getLong("avoid_same_geo")!!, - preferMMSDiffRatherThanSameCountry=json.getInt("country")!!, - preferMMSDiffRatherThanSameClubsGroup=json.getInt("club_group")!!, - preferMMSDiffRatherThanSameClub=json.getInt("club")!!, + "weight" to avoidSameGeo, + "mmsDiffCountry" to preferMMSDiffRatherThanSameCountry, + "mmsDiffClubGroup" to preferMMSDiffRatherThanSameClubsGroup, + "mmsDiffClub" to preferMMSDiffRatherThanSameClub ) +fun HandicapParams.Companion.fromJson(json: Json.Object) = HandicapParams( + weight = json.getDouble("weight") ?: default.weight, + useMMS = json.getBoolean("useMMS") ?: default.useMMS, + rankThreshold = json.getInt("treshold") ?: default.rankThreshold, + correction = json.getInt("correction") ?: default.correction, + ceiling = json.getInt("ceiling") ?: default.ceiling +) -fun Pairing.Companion.fromJson(json: Json.Object) = when (json.getString("type")?.let { Pairing.PairingType.valueOf(it) } ?: badRequest("missing pairing type")) { - SWISS -> Swiss() - MAC_MAHON -> MacMahon( - bar = json.getInt("bar") ?: 0, - minLevel = json.getInt("minLevel") ?: -30, - reducer = json.getInt("reducer") ?: 1 - ) - ROUND_ROBIN -> RoundRobin() -} - -fun Pairing.toJson() = when (this) { - is Swiss -> - 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) +fun HandicapParams.toJson() = Json.Object( + "weight" to weight, + "useMMS" to useMMS, + "treshold" to rankThreshold, + "correction" to correction, + "ceiling" to ceiling +) + +fun Pairing.Companion.fromJson(json: Json.Object): Pairing { + // get default values for each type + val type = json.getString("type")?.let { PairingType.valueOf(it) } ?: badRequest("missing pairing type") + val defaultParams = when (type) { + SWISS -> Swiss() + MAC_MAHON -> MacMahon() + ROUND_ROBIN -> RoundRobin() + } + val base = json.getObject("base")?.let { BasePairingParams.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 + val hd = json.getObject("handicap")?.let { HandicapParams.fromJson(it) } ?: defaultParams.pairingParams.handicap + val pairingParams = PairingParams(base, main, secondary, geo, hd) + val placementParams = json.getArray("placement")?.let { PlacementParams.fromJson(it) } ?: defaultParams.placementParams + return when (type) { + SWISS -> Swiss(pairingParams, placementParams) + MAC_MAHON -> MacMahon(pairingParams, placementParams) + ROUND_ROBIN -> RoundRobin(pairingParams, placementParams) + } } +fun Pairing.toJson() = Json.Object( + "type" to type.name, + "base" to pairingParams.base.toJson(), + "main" to pairingParams.main.toJson(), + "secondary" to pairingParams.main.toJson(), + "geo" to pairingParams.geo.toJson(), + "handicap" to pairingParams.handicap.toJson(), + "placement" to placementParams.toJson() +) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Placement.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Placement.kt index a4121c0..eb5f016 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Placement.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Placement.kt @@ -1,7 +1,9 @@ package org.jeudego.pairgoth.model -enum class PlacementCriterion { - NULL, // No ranking/tie-break +import com.republicate.kson.Json + +enum class Criterion { + NONE, // No ranking / tie-break CATEGORY, RANK, @@ -35,24 +37,23 @@ enum class PlacementCriterion { DC, // Direct confrontation } -class PlacementParams(vararg criteria: PlacementCriterion) { - companion object { - const val MAX_NUMBER_OF_CRITERIA: Int = 6 +class PlacementParams(vararg crit: Criterion) { + companion object {} + + val criteria = crit.toList().also { + check() } - private fun addNullCriteria(criteria: Array): ArrayList { - var criteria = arrayListOf(*criteria) - while (criteria.size < MAX_NUMBER_OF_CRITERIA) { - criteria.add(PlacementCriterion.NULL) - } - return criteria + private fun check() { + // throws an exception if criteria are incoherent + // TODO - if (not coherent) throw Error("...") } +} - val criteria = addNullCriteria(criteria) +fun PlacementParams.Companion.fromJson(json: Json.Array) = PlacementParams(*json.map { + Criterion.valueOf(it!! as String) +}.toTypedArray()) - open fun checkWarnings(): String { - // Returns a warning message if criteria are incoherent - // TODO - return "" - } -} \ No newline at end of file +fun PlacementParams.toJson() = Json.Array(*criteria.map { + it.name +}.toTypedArray()) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index d6628aa..ff1ee17 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -36,10 +36,6 @@ sealed class Tournament ( TEAM5(5); } - enum class Criterion { - NBW, MMS, SOS, SOSOS, SODOS - } - // players per id abstract val players: MutableMap @@ -69,13 +65,6 @@ sealed class Tournament ( if (round > games.size + 1) throw Error("invalid round") else mutableMapOf().also { games.add(it) } fun lastRound() = games.size - - // standings criteria - val criteria = mutableListOf( - if (pairing.type == Pairing.PairingType.MAC_MAHON) Criterion.MMS else Criterion.NBW, - Criterion.SOS, - Criterion.SOSOS - ) } // standard tournament of individuals diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt index f4aa60e..2b90725 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/HistoryHelper.kt @@ -4,19 +4,19 @@ import org.jeudego.pairgoth.model.* open class HistoryHelper(protected val history: List, computeScore: () -> Map) { - fun getCriterionValue(p: Pairable, crit: PlacementCriterion): Double { + fun getCriterionValue(p: Pairable, crit: Criterion): Double { // Returns generic criterion // Specific criterion are computed by solvers directly return when (crit) { - PlacementCriterion.NULL -> 0.0 - PlacementCriterion.CATEGORY -> TODO() - PlacementCriterion.RANK -> p.rank.toDouble() - PlacementCriterion.RATING -> p.rating.toDouble() + Criterion.NONE -> 0.0 + Criterion.CATEGORY -> TODO() + Criterion.RANK -> p.rank.toDouble() + Criterion.RATING -> p.rating.toDouble() - PlacementCriterion.EXT -> TODO() - PlacementCriterion.EXR -> TODO() - PlacementCriterion.SDC -> TODO() - PlacementCriterion.DC -> TODO() + Criterion.EXT -> TODO() + Criterion.EXR -> TODO() + Criterion.SDC -> TODO() + Criterion.DC -> TODO() else -> -1.0 } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt index 1aca4d3..cbfaeea 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt @@ -1,14 +1,10 @@ package org.jeudego.pairgoth.pairing import org.jeudego.pairgoth.model.* -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.roundToInt -import kotlin.math.sign -class MacMahonSolver(round: Int, history: List, pairables: List, pairingParams: Pairing.PairingParams, placementParams: PlacementParams, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(round, history, pairables, pairingParams, placementParams) { +class MacMahonSolver(round: Int, history: List, pairables: List, pairingParams: PairingParams, placementParams: PlacementParams): Solver(round, history, pairables, pairingParams, placementParams) { - val Pairable.mms get() = mmBase + nbW // TODO real calculation +// val Pairable.mms get() = mmBase + nbW // TODO real calculation // CB TODO - configurable criteria override fun mainCriterion(p1: Pairable): Int { @@ -23,16 +19,16 @@ class MacMahonSolver(round: Int, history: List, pairables: List, TODO("Not yet implemented") } - override fun getSpecificCriterionValue(p: Pairable, criterion: PlacementCriterion): Double { + override fun getSpecificCriterionValue(p: Pairable, criterion: Criterion): Double { // TODO solve this double/int conflict return when (criterion) { - PlacementCriterion.MMS -> TODO() - PlacementCriterion.SOSM -> p.sos - PlacementCriterion.SOSMM1 -> p.sosm1 - PlacementCriterion.SOSMM2 -> p.sosm2 - PlacementCriterion.SODOSM -> p.sodos - PlacementCriterion.SOSOSM -> p.sosos - PlacementCriterion.CUSSM -> p.cums + Criterion.MMS -> TODO() + Criterion.SOSM -> p.sos + Criterion.SOSMM1 -> p.sosm1 + Criterion.SOSMM2 -> p.sosm2 + Criterion.SODOSM -> p.sodos + Criterion.SOSOSM -> p.sosos + Criterion.CUSSM -> p.cums else -> -1.0 } } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt index b65e41a..4d6525a 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt @@ -1,6 +1,7 @@ package org.jeudego.pairgoth.pairing import org.jeudego.pairgoth.model.* +import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.* import org.jeudego.pairgoth.store.Store import org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching import org.jgrapht.alg.matching.blossom.v5.ObjectiveSense @@ -14,8 +15,7 @@ import kotlin.math.min val DEBUG_EXPORT_WEIGHT = true -private fun detRandom(max: Long, p1: Pairable, p2: Pairable): Long { - var nR: Long = 0 +private fun detRandom(max: Double, p1: Pairable, p2: Pairable): Double { var inverse = false val seed1 = p1.nameSeed() @@ -28,28 +28,25 @@ private fun detRandom(max: Long, p1: Pairable, p2: Pairable): Long { inverse = true } val s = name1 + name2 + var nR = 0.0 for (i in s.indices) { val c = s[i] - nR += (c.code * (i + 1)).toLong() + nR += (c.code * (i + 1)).toDouble() } nR = nR * 1234567 % (max + 1) if (inverse) nR = max - nR return nR } -private fun nonDetRandom(max: Long): Long { - if (max == 0L) { - return 0 - } - val r = Math.random() * (max + 1) - return r.toLong() -} +private fun nonDetRandom(max: Double) = + if (max == 0.0) 0.0 + else Math.random() * (max + 1.0) sealed class Solver( val round: Int, history: List, val pairables: List, - val pairingParams: Pairing.PairingParams, + val pairingParams: PairingParams, val placementParams: PlacementParams) { companion object { @@ -66,28 +63,23 @@ sealed class Solver( } return 0 } - open fun weight(p1: Pairable, p2: Pairable): Double { - var score = 1L // 1 is minimum value because 0 means "no matching allowed" + 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) - score += applyBaseCriteria(p1, p2) - - score += applyMainCriteria(p1, p2) - - score += applySecondaryCriteria(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 // SOS and variants will be computed based on this score abstract fun computeStandingScore(): Map // This function needs to be overridden for criterion specific to the current pairing mode - open fun getSpecificCriterionValue(p1: Pairable, criterion: PlacementCriterion): Double { + open fun getSpecificCriterionValue(p1: Pairable, criterion: Criterion): Double { return -1.0 } - private fun getCriterionValue(p1: Pairable, criterion: PlacementCriterion): Double { + private fun getCriterionValue(p1: Pairable, criterion: Criterion): Double { val genericCritVal = historyHelper.getCriterionValue(p1, criterion) // If the value from the history helper is > 0 it means that it is a generic criterion // Just returns the value @@ -122,8 +114,8 @@ sealed class Solver( return result } - open fun applyBaseCriteria(p1: Pairable, p2: Pairable): Long { - var score = 0L + open fun applyBaseCriteria(p1: Pairable, p2: Pairable): Double { + var score = 0.0 // Base Criterion 1 : Avoid Duplicating Game // Did p1 and p2 already play ? @@ -137,8 +129,8 @@ sealed class Solver( } // Main criteria - open fun applyMainCriteria(p1: Pairable, p2: Pairable): Long { - var score = 0L + open fun applyMainCriteria(p1: Pairable, p2: Pairable): Double { + var score = 0.0 // Main criterion 1 avoid mixing category is moved to Swiss with category // TODO @@ -155,8 +147,8 @@ sealed class Solver( return score } - open fun applySecondaryCriteria(p1: Pairable, p2: Pairable): Long { - var score = 0L + 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 @@ -169,23 +161,23 @@ sealed class Solver( // Weight score computation details // Base criteria - open fun avoidDuplicatingGames(p1: Pairable, p2: Pairable): Long { + open fun avoidDuplicatingGames(p1: Pairable, p2: Pairable): Double { if (p1.played(p2)) { - return 0 // We get no score if pairables already played together + return 0.0 // We get no score if pairables already played together } else { - return pairingParams.baseAvoidDuplGame + return pairingParams.base.dupWeight } } - open fun applyRandom(p1: Pairable, p2: Pairable): Long { - if (pairingParams.baseDeterministic) { - return detRandom(pairingParams.baseRandom, p1, p2) + open fun applyRandom(p1: Pairable, p2: Pairable): Double { + if (pairingParams.base.deterministic) { + return detRandom(pairingParams.base.random, p1, p2) } else { - return nonDetRandom(pairingParams.baseRandom) + return nonDetRandom(pairingParams.base.random) } } - open fun applyBalanceBW(p1: Pairable, p2: Pairable): Long { + 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 @@ -194,58 +186,58 @@ sealed class Solver( val wb1: Int = p1.colorBalance val wb2: Int = p2.colorBalance if (wb1 * wb2 < 0) { - return pairingParams.baseBalanceWB + return pairingParams.base.colorBalance } else if (wb1 == 0 && abs(wb2) >= 2) { - return pairingParams.baseBalanceWB / 2 + return pairingParams.base.colorBalance / 2 } else if (wb2 == 0 && abs(wb1) >= 2) { - return pairingParams.baseBalanceWB / 2 + return pairingParams.base.colorBalance / 2 } } - return 0 + return 0.0 } - open fun minimizeScoreDifference(p1: Pairable, p2: Pairable): Long { - var score = 0L + open fun 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.standardNX1Factor - score = (pairingParams.mainMinimizeScoreDifference * (1.0 - x) * (1.0 + k * x)) as Long + val k: Double = pairingParams.base.nx1 + score = pairingParams.main.scoreWeight * (1.0 - x) * (1.0 + k * x) return score } - fun applySeeding(p1: Pairable, p2: Pairable): Long { - var score = 0L + fun 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.maMaximizeSeeding + val maxSeedingWeight = pairingParams.main.seedingWeight - val currentSeedSystem: SeedMethod = if (round <= pairingParams.maLastRoundForSeedSystem1) - pairingParams.maSeedSystem1 else pairingParams.maSeedSystem2 + val currentSeedSystem: MainCritParams.SeedMethod = if (round <= pairingParams.main.lastRoundForSeedSystem1) + pairingParams.main.seedSystem1 else pairingParams.main.seedSystem2 score += when(currentSeedSystem) { // The best is to get 2 * |Cla1 - Cla2| - groupSize close to 0 - SeedMethod.SPLIT_AND_SLIP -> { - val x = 2 * abs(cla1 - cla2) - groupSize + SPLIT_AND_SLIP -> { + val x = 2.0 * abs(cla1 - cla2) - groupSize maxSeedingWeight - maxSeedingWeight * x / groupSize * x / groupSize } // The best is to get cla1 + cla2 - (groupSize - 1) close to 0 - SeedMethod.SPLIT_AND_FOLD -> { + SPLIT_AND_FOLD -> { val x = cla1 + cla2 - (groupSize - 1) maxSeedingWeight - maxSeedingWeight * x / (groupSize - 1) * x / (groupSize - 1) } - SeedMethod.SPLIT_AND_RANDOM -> { + SPLIT_AND_RANDOM -> { if ((2 * cla1 < groupSize && 2 * cla2 >= groupSize) || (2 * cla1 >= groupSize && 2 * cla2 < groupSize)) { - val randRange = (maxSeedingWeight * 0.2).toLong() + val randRange = maxSeedingWeight * 0.2 val rand = detRandom(randRange, p1, p2) maxSeedingWeight - rand } else { - 0L + 0.0 } } } @@ -260,7 +252,7 @@ sealed class Solver( // TODO understand where it is used } - fun avoidSameGeo(p1: Pairable, p2: Pairable): Long { + fun avoidSameGeo(p1: Pairable, p2: Pairable): Double { val placementScoreRange = numberGroups val geoMaxCost = pairingParams.geo.avoidSameGeo @@ -310,8 +302,8 @@ sealed class Solver( } // The concavity function is applied to geoRatio to get geoCost - val dbGeoCost: Double = geoMaxCost.toDouble() * (1.0 - geoRatio) * (1.0 + pairingParams.standardNX1Factor * geoRatio) - var score: Long = pairingParams.mainMinimizeScoreDifference - dbGeoCost.toLong() + val dbGeoCost: Double = geoMaxCost.toDouble() * (1.0 - geoRatio) * (1.0 + pairingParams.base.nx1 * geoRatio) + var score = pairingParams.main.scoreWeight - dbGeoCost score = min(score, geoMaxCost) return score @@ -324,8 +316,8 @@ sealed class Solver( var pseudoRank1: Int = p1.rank var pseudoRank2: Int = p2.rank - pseudoRank1 = min(pseudoRank1, pairingParams.hd.noHdRankThreshold) - pseudoRank2 = min(pseudoRank2, pairingParams.hd.noHdRankThreshold) + pseudoRank1 = min(pseudoRank1, pairingParams.handicap.rankThreshold) + pseudoRank2 = min(pseudoRank2, pairingParams.handicap.rankThreshold) hd = pseudoRank1 - pseudoRank2 return clampHandicap(hd) @@ -334,16 +326,16 @@ sealed class Solver( open fun clampHandicap(inputHd: Int): Int { var hd = inputHd if (hd > 0) { - hd -= pairingParams.hd.correction + hd -= pairingParams.handicap.correction hd = min(hd, 0) } if (hd < 0) { - hd += pairingParams.hd.correction + hd += pairingParams.handicap.correction hd = max(hd, 0) } // Clamp handicap with ceiling - hd = min(hd, pairingParams.hd.ceiling) - hd = max(hd, -pairingParams.hd.ceiling) + hd = min(hd, pairingParams.handicap.ceiling) + hd = max(hd, -pairingParams.handicap.ceiling) return hd } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt index 61890c7..29dbe4c 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt @@ -1,12 +1,11 @@ package org.jeudego.pairgoth.pairing import org.jeudego.pairgoth.model.* -import kotlin.math.abs class SwissSolver(round: Int, history: List, pairables: List, - pairingParams: Pairing.PairingParams, + pairingParams: PairingParams, placementParams: PlacementParams): Solver(round, history, pairables, pairingParams, placementParams) { @@ -23,16 +22,16 @@ class SwissSolver(round: Int, return historyHelper.numberWins } - override fun getSpecificCriterionValue(p: Pairable, criterion: PlacementCriterion): Double { + override fun getSpecificCriterionValue(p: Pairable, criterion: Criterion): Double { // TODO solve this double/int conflict return when (criterion) { - PlacementCriterion.NBW -> p.nbW - PlacementCriterion.SOSW -> p.sos - PlacementCriterion.SOSWM1 -> p.sosm1 - PlacementCriterion.SOSWM2 -> p.sosm2 - PlacementCriterion.SODOSW -> p.sodos - PlacementCriterion.SOSOSW -> p.sosos - PlacementCriterion.CUSSW -> p.cums + Criterion.NBW -> p.nbW + Criterion.SOSW -> p.sos + Criterion.SOSWM1 -> p.sosm1 + Criterion.SOSWM2 -> p.sosm2 + Criterion.SODOSW -> p.sodos + Criterion.SOSOSW -> p.sosos + Criterion.CUSSW -> p.cums else -> -1.0 } }