From 2378c51a05c675ed6e948434bc0ff160437051b7 Mon Sep 17 00:00:00 2001 From: Theo Barollet Date: Thu, 8 Jun 2023 00:29:11 +0200 Subject: [PATCH] added many tournament parameters and generic computing of base criterion. --- .../org/jeudego/pairgoth/model/Pairable.kt | 10 +- .../org/jeudego/pairgoth/model/Pairing.kt | 184 ++++++++++++++---- .../org/jeudego/pairgoth/model/Tournament.kt | 2 +- .../jeudego/pairgoth/pairing/HistoryHelper.kt | 7 +- .../pairgoth/pairing/MacMahonSolver.kt | 32 +-- .../org/jeudego/pairgoth/pairing/Solver.kt | 173 ++++++++++++++-- .../jeudego/pairgoth/pairing/SwissSolver.kt | 28 +-- 7 files changed, 340 insertions(+), 96 deletions(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt index 711d8c0..ec01b86 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairable.kt @@ -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() // 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( 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 19d422c..3b94dec 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,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): List } +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): List { - 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() override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { - 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): 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.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) } 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 c6b0c0e..d6628aa 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 @@ -72,7 +72,7 @@ sealed class Tournament ( // standings criteria val criteria = mutableListOf( - 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 ) 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 daccd5b..55c18e5 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 @@ -21,9 +21,14 @@ open class HistoryHelper(protected val history: List) { }).toSet() } + // Returns the number of games played as white + // Only count games without handicap private val colorBalance: Map 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 } 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 5f7b647..ae55d14 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 @@ -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, pairables: List, weights: Pairing.Weights, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(history, pairables, weights) { +class MacMahonSolver(history: List, pairables: List, 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 { + 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() - } 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 ec2dc36..c770e73 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,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 - val score: Map - val sos: Map - val sosos: Map - val sodos: Map + +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, val pairables: List, 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, val pairables: List, 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 + + // 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 { // 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, val pairables: List, val weig return result } + private fun computeGroups(): Pair, Int> { + val (mainScoreMin, mainScoreMax) = mainCriterionMinMax() + + // TODO categories + val groups: Map = 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, val pairables: List, 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) 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 368c614..50d024a 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 @@ -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, pairables: List, weights: Pairing.Weights, val method: Swiss.Method): Solver(history, pairables, weights) { +class SwissSolver(history: List, pairables: List, 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, pairables: List, 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 { + TODO("Not yet implemented") } }