From 31411eb85907f5b627b6a3dfe2ff77e87dfd8f28 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sun, 24 Dec 2023 15:45:14 +0100 Subject: [PATCH] Standings page in progress --- .../jeudego/pairgoth/api/StandingsHandler.kt | 88 +++++++++++++++++++ .../kotlin/org/jeudego/pairgoth/model/Defs.kt | 2 +- .../org/jeudego/pairgoth/model/Pairable.kt | 8 +- .../org/jeudego/pairgoth/model/Pairing.kt | 88 +++++++++---------- .../org/jeudego/pairgoth/model/Tournament.kt | 7 +- .../jeudego/pairgoth/pairing/HistoryHelper.kt | 8 +- .../pairgoth/pairing/solver/MacMahonSolver.kt | 8 +- .../org/jeudego/pairgoth/server/ApiServlet.kt | 2 + .../org/jeudego/pairgoth/view/PairgothTool.kt | 37 +++++++- view-webapp/src/main/sass/tour.scss | 33 +++++++ .../src/main/webapp/js/tour-standings.inc.js | 42 +++++++++ .../src/main/webapp/tour-pairing.inc.html | 2 +- .../src/main/webapp/tour-results.inc.html | 2 +- .../src/main/webapp/tour-standings.inc.html | 51 ++++++++++- view-webapp/src/main/webapp/tour.html | 7 +- 15 files changed, 325 insertions(+), 60 deletions(-) create mode 100644 view-webapp/src/main/webapp/js/tour-standings.inc.js diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt index ffbe742..4e3a586 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/StandingsHandler.kt @@ -1,4 +1,92 @@ package org.jeudego.pairgoth.api +import com.republicate.kson.Json +import com.republicate.kson.toJsonArray +import org.jeudego.pairgoth.model.Criterion +import org.jeudego.pairgoth.model.MacMahon +import org.jeudego.pairgoth.model.Pairable +import org.jeudego.pairgoth.model.PairingType +import org.jeudego.pairgoth.model.historyBefore +import org.jeudego.pairgoth.pairing.HistoryHelper +import org.jeudego.pairgoth.pairing.solver.MacMahonSolver +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import kotlin.math.max +import kotlin.math.min + +import org.jeudego.pairgoth.model.Criterion.* +import org.jeudego.pairgoth.model.ID +import org.jeudego.pairgoth.model.getID + object StandingsHandler: PairgothApiHandler { + override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { + val tournament = getTournament(request) + val round = getSubSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("invalid round number") + + fun mmBase(pairable: Pairable): Double { + if (tournament.pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon") + return min(max(pairable.rank, tournament.pairing.mmFloor), tournament.pairing.mmBar) + MacMahonSolver.mmsZero + } + + val historyHelper = HistoryHelper(tournament.historyBefore(round)) { + if (tournament.pairing.type == PairingType.SWISS) wins + else tournament.pairables.mapValues { + it.value.let { + pairable -> mmBase(pairable) + ( nbW(pairable) ?: 0.0) // TODO take tournament parameter into account + } + } + } + val criteria = tournament.pairing.placementParams.criteria.map { crit -> + crit.name to when (crit) { + NONE -> nullMap + CATEGORY -> nullMap + RANK -> tournament.pairables.mapValues { it.value.rank } + RATING -> tournament.pairables.mapValues { it.value.rating } + NBW -> historyHelper.wins + MMS -> historyHelper.scores + STS -> nullMap + CPS -> nullMap + + SOSW -> historyHelper.sos + SOSWM1 -> historyHelper.sosm1 + SOSWM2 -> historyHelper.sosm2 + SODOSW -> historyHelper.sodos + SOSOSW -> historyHelper.sosos + CUSSW -> historyHelper.cumScore + SOSM -> historyHelper.sos + SOSMM1 -> historyHelper.sosm1 + SOSMM2 -> historyHelper.sosm2 + SODOSM -> historyHelper.sodos + SOSOSM -> historyHelper.sosos + CUSSM -> historyHelper.cumScore + + SOSTS -> nullMap + + EXT -> nullMap + EXR -> nullMap + + SDC -> nullMap + DC -> nullMap + } + } + val pairables = tournament.pairables.values.map { it.toMutableJson() } + pairables.forEach { it -> + val player = it as Json.MutableObject + for (crit in criteria) { + player[crit.first] = crit.second[player.getID()] + } + } + return pairables.sortedWith { left, right -> + for (crit in criteria) { + val lval = left.getDouble(crit.first) ?: 0.0 + val rval = right.getDouble(crit.first) ?: 0.0 + val cmp = lval.compareTo(rval) + if (cmp != 0) return@sortedWith -cmp + } + return@sortedWith 0 + + }.toJsonArray() + } + + val nullMap = mapOf() } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt index 740db6f..a46e428 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Defs.kt @@ -7,5 +7,5 @@ typealias ID = Int fun String.toID() = toInt() fun String.toIDOrNull() = toIntOrNull() fun Number.toID() = toInt() -fun Json.Object.getID(key: String) = getInt(key) +fun Json.Object.getID(key: String = "id") = getInt(key) fun Json.Array.getID(index: Int) = getInt(index) \ No newline at end of file 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 29ea3d8..b13c502 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 @@ -13,6 +13,7 @@ sealed class Pairable(val id: ID, val name: String, open val rating: Int, open v val MAX_RANK: Int = 8 // 9D } abstract fun toJson(): Json.Object + abstract fun toMutableJson(): Json.MutableObject abstract val club: String? abstract val country: String? open fun nameSeed(separator: String =" "): String { @@ -29,6 +30,9 @@ object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE) { override fun toJson(): Json.Object { throw Error("bye player should never be serialized") } + override fun toMutableJson(): Json.MutableObject { + throw Error("bye player should never be serialized") + } override val club = "none" override val country = "none" @@ -71,7 +75,7 @@ class Player( companion object // used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...) val externalIds = mutableMapOf() - override fun toJson(): Json.Object = Json.MutableObject( + override fun toMutableJson() = Json.MutableObject( "id" to id, "name" to name, "firstname" to firstname, @@ -85,6 +89,8 @@ class Player( json[dbid.key] = id } } + + override fun toJson(): Json.Object = toMutableJson() override fun nameSeed(separator: String): String { return name + separator + firstname } 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 31bcff4..2a1452e 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 @@ -129,7 +129,7 @@ sealed class Pairing( abstract fun pair(tournament: Tournament<*>, round: Int, pairables: List): List } -private fun Tournament<*>.historyBefore(round: Int) = +internal fun Tournament<*>.historyBefore(round: Int) = if (lastRound() == 1) emptyList() else (1 until round).map { games(it).values.toList() } @@ -212,12 +212,12 @@ class RoundRobin( // Serialization -fun BaseCritParams.Companion.fromJson(json: Json.Object) = BaseCritParams( - nx1 = json.getDouble("nx1") ?: default.nx1, - dupWeight = json.getDouble("dupWeight") ?: default.dupWeight, - random = json.getDouble("random") ?: default.random, - deterministic = json.getBoolean("deterministic") ?: default.deterministic, - colorBalanceWeight = json.getDouble("colorBalanceWeight") ?: default.colorBalanceWeight +fun BaseCritParams.Companion.fromJson(json: Json.Object, localDefault: BaseCritParams? = null) = BaseCritParams( + nx1 = json.getDouble("nx1") ?: localDefault?.nx1 ?: default.nx1, + dupWeight = json.getDouble("dupWeight") ?: localDefault?.dupWeight ?: default.dupWeight, + random = json.getDouble("random") ?: localDefault?.random ?: default.random, + deterministic = json.getBoolean("deterministic") ?: localDefault?.deterministic ?: default.deterministic, + colorBalanceWeight = json.getDouble("colorBalanceWeight") ?: localDefault?.colorBalanceWeight ?: default.colorBalanceWeight ) fun BaseCritParams.toJson() = Json.Object( @@ -227,19 +227,19 @@ fun BaseCritParams.toJson() = Json.Object( "colorBalanceWeight" to colorBalanceWeight ) -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.Companion.fromJson(json: Json.Object, localDefault: MainCritParams? = null) = MainCritParams( + categoriesWeight = json.getDouble("catWeight") ?: localDefault?.categoriesWeight ?: default.categoriesWeight, + scoreWeight = json.getDouble("scoreWeight") ?: localDefault?.scoreWeight ?: default.scoreWeight, + drawUpDownWeight = json.getDouble("upDownWeight") ?: localDefault?.drawUpDownWeight ?: default.drawUpDownWeight, + compensateDrawUpDown = json.getBoolean("upDownCompensate") ?: localDefault?.compensateDrawUpDown ?: default.compensateDrawUpDown, + drawUpDownLowerMode = json.getString("upDownLowerMode")?.let { MainCritParams.DrawUpDown.valueOf(it) } ?: localDefault?.drawUpDownLowerMode ?: default.drawUpDownLowerMode, + drawUpDownUpperMode = json.getString("upDownUpperMode")?.let { MainCritParams.DrawUpDown.valueOf(it) } ?: localDefault?.drawUpDownUpperMode ?: default.drawUpDownUpperMode, + seedingWeight = json.getDouble("maximizeSeeding") ?: localDefault?.seedingWeight ?: default.seedingWeight, + lastRoundForSeedSystem1 = json.getInt("firstSeedLastRound") ?: localDefault?.lastRoundForSeedSystem1 ?: default.lastRoundForSeedSystem1, + seedSystem1 = json.getString("firstSeed")?.let { MainCritParams.SeedMethod.valueOf(it) } ?: localDefault?.seedSystem1 ?: default.seedSystem1, + seedSystem2 = json.getString("secondSeed")?.let { MainCritParams.SeedMethod.valueOf(it) } ?: localDefault?.seedSystem2 ?: default.seedSystem2, + additionalPlacementCritSystem1 = json.getString("firstSeedAddCrit")?.let { Criterion.valueOf(it) } ?: localDefault?.additionalPlacementCritSystem1 ?: default.additionalPlacementCritSystem1, + additionalPlacementCritSystem2 = json.getString("secondSeedAddCrit")?.let { Criterion.valueOf(it) } ?: localDefault?.additionalPlacementCritSystem2 ?: default.additionalPlacementCritSystem2 ) fun MainCritParams.toJson() = Json.Object( @@ -257,11 +257,11 @@ fun MainCritParams.toJson() = Json.Object( "secondSeedAddCrit" to additionalPlacementCritSystem2 ) -fun SecondaryCritParams.Companion.fromJson(json: Json.Object) = SecondaryCritParams( - barThresholdActive = json.getBoolean("barThreshold") ?: default.barThresholdActive, - rankThreshold = json.getInt("rankThreshold") ?: default.rankThreshold, - nbWinsThresholdActive = json.getBoolean("winsThreshold") ?: default.nbWinsThresholdActive, - defSecCrit = json.getDouble("secWeight") ?: default.defSecCrit +fun SecondaryCritParams.Companion.fromJson(json: Json.Object, localDefault: SecondaryCritParams? = null) = SecondaryCritParams( + barThresholdActive = json.getBoolean("barThreshold") ?: localDefault?.barThresholdActive ?: default.barThresholdActive, + rankThreshold = json.getInt("rankThreshold") ?: localDefault?.rankThreshold ?: default.rankThreshold, + nbWinsThresholdActive = json.getBoolean("winsThreshold") ?: localDefault?.nbWinsThresholdActive ?: default.nbWinsThresholdActive, + defSecCrit = json.getDouble("secWeight") ?: localDefault?.defSecCrit ?: default.defSecCrit ) fun SecondaryCritParams.toJson() = Json.Object( @@ -271,11 +271,11 @@ fun SecondaryCritParams.toJson() = Json.Object( "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.Companion.fromJson(json: Json.Object, localDefault: GeographicalParams? = null) = GeographicalParams( + avoidSameGeo = json.getDouble("weight") ?: localDefault?.avoidSameGeo ?: disabled.avoidSameGeo, + preferMMSDiffRatherThanSameCountry = json.getInt("mmsDiffCountry") ?: localDefault?.preferMMSDiffRatherThanSameCountry ?: disabled.preferMMSDiffRatherThanSameCountry, + preferMMSDiffRatherThanSameClubsGroup = json.getInt("mmsDiffClubGroup") ?: localDefault?.preferMMSDiffRatherThanSameClub ?: disabled.preferMMSDiffRatherThanSameClubsGroup, + preferMMSDiffRatherThanSameClub = json.getInt("mmsDiffClub") ?: localDefault?.preferMMSDiffRatherThanSameClub ?: disabled.preferMMSDiffRatherThanSameClub ) fun GeographicalParams.toJson() = Json.Object( @@ -285,12 +285,12 @@ fun GeographicalParams.toJson() = Json.Object( "mmsDiffClub" to preferMMSDiffRatherThanSameClub ) -fun HandicapParams.Companion.fromJson(json: Json.Object, type: PairingType) = HandicapParams( - weight = json.getDouble("weight") ?: default(type).weight, - useMMS = json.getBoolean("useMMS") ?: default(type).useMMS, - rankThreshold = json.getInt("threshold") ?: default(type).rankThreshold, - correction = json.getInt("correction") ?: default(type).correction, - ceiling = json.getInt("ceiling") ?: default(type).ceiling +fun HandicapParams.Companion.fromJson(json: Json.Object, type: PairingType, localDefault: HandicapParams? = null) = HandicapParams( + weight = json.getDouble("weight") ?: localDefault?.weight ?: default(type).weight, + useMMS = json.getBoolean("useMMS") ?: localDefault?.useMMS ?: default(type).useMMS, + rankThreshold = json.getInt("threshold") ?: localDefault?.rankThreshold ?: default(type).rankThreshold, + correction = json.getInt("correction") ?: localDefault?.correction ?: default(type).correction, + ceiling = json.getInt("ceiling") ?: localDefault?.ceiling ?: default(type).ceiling ) fun HandicapParams.toJson() = Json.Object( @@ -301,21 +301,21 @@ fun HandicapParams.toJson() = Json.Object( "ceiling" to ceiling ) -fun Pairing.Companion.fromJson(json: Json.Object): Pairing { +fun Pairing.Companion.fromJson(json: Json.Object, default: Pairing?): Pairing { // get default values for each type - val type = json.getString("type")?.let { PairingType.valueOf(it) } ?: badRequest("missing pairing type") + val type = json.getString("type")?.let { PairingType.valueOf(it) } ?: default?.type ?: badRequest("missing pairing type") val defaultParams = when (type) { SWISS -> Swiss() MAC_MAHON -> MacMahon() ROUND_ROBIN -> RoundRobin() } - val base = json.getObject("base")?.let { BaseCritParams.fromJson(it) } ?: defaultParams.pairingParams.base - val main = json.getObject("main")?.let { MainCritParams.fromJson(it) } ?: defaultParams.pairingParams.main - val secondary = json.getObject("secondary")?.let { SecondaryCritParams.fromJson(it) } ?: defaultParams.pairingParams.secondary - val geo = json.getObject("geo")?.let { GeographicalParams.fromJson(it) } ?: defaultParams.pairingParams.geo - val hd = json.getObject("handicap")?.let { HandicapParams.fromJson(it, type) } ?: defaultParams.pairingParams.handicap + val base = json.getObject("base")?.let { BaseCritParams.fromJson(it, default?.pairingParams?.base) } ?: default?.pairingParams?.base ?: defaultParams.pairingParams.base + val main = json.getObject("main")?.let { MainCritParams.fromJson(it, default?.pairingParams?.main) } ?: default?.pairingParams?.main ?: defaultParams.pairingParams.main + val secondary = json.getObject("secondary")?.let { SecondaryCritParams.fromJson(it, default?.pairingParams?.secondary) } ?: default?.pairingParams?.secondary ?: defaultParams.pairingParams.secondary + val geo = json.getObject("geo")?.let { GeographicalParams.fromJson(it, default?.pairingParams?.geo) } ?: default?.pairingParams?.geo ?: defaultParams.pairingParams.geo + val hd = json.getObject("handicap")?.let { HandicapParams.fromJson(it, type, default?.pairingParams?.handicap) } ?: default?.pairingParams?.handicap ?: defaultParams.pairingParams.handicap val pairingParams = PairingParams(base, main, secondary, geo, hd) - val placementParams = json.getArray("placement")?.let { PlacementParams.fromJson(it) } ?: defaultParams.placementParams + val placementParams = json.getArray("placement")?.let { PlacementParams.fromJson(it) } ?: default?.placementParams ?: defaultParams.placementParams return when (type) { SWISS -> Swiss(pairingParams, placementParams) MAC_MAHON -> MacMahon(pairingParams, placementParams).also { mm -> 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 3f03473..411f313 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 @@ -117,11 +117,12 @@ class TeamTournament( override val rank: Int get() = if (teamPlayers.isEmpty()) super.rank else (teamPlayers.sumOf { player -> player.rank.toDouble() } / players.size).roundToInt() override val club: String? get() = teamPlayers.map { club }.distinct().let { if (it.size == 1) it[0] else null } override val country: String? get() = teamPlayers.map { country }.distinct().let { if (it.size == 1) it[0] else null } - override fun toJson() = Json.Object( + override fun toMutableJson() = Json.MutableObject( "id" to id, "name" to name, "players" to playerIds.toList().toJsonArray() ) + override fun toJson(): Json.Object = toMutableJson() val teamOfIndividuals: Boolean get() = type.individual } @@ -160,7 +161,7 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), - pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing") + pairing = json.getObject("pairing")?.let { Pairing.fromJson(it, default?.pairing) } ?: default?.pairing ?: badRequest("missing pairing") ) else TeamTournament( @@ -178,7 +179,7 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), - pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing") + pairing = json.getObject("pairing")?.let { Pairing.fromJson(it, default?.pairing) } ?: default?.pairing ?: badRequest("missing pairing") ) json["pairables"]?.let { pairables -> 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 7918f0b..3e73e95 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 @@ -3,7 +3,7 @@ package org.jeudego.pairgoth.pairing import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.Game.Result.* -open class HistoryHelper(protected val history: List>, scoresGetter: ()-> Map) { +open class HistoryHelper(protected val history: List>, scoresGetter: HistoryHelper.()-> Map) { private val Game.blackScore get() = when (result) { BLACK, BOTHWIN -> 1.0 @@ -15,7 +15,9 @@ open class HistoryHelper(protected val history: List>, scoresGetter: else -> 0.0 } - private val scores by lazy(scoresGetter) + val scores by lazy { + scoresGetter() + } // Generic helper functions open fun playedTogether(p1: Pairable, p2: Pairable) = paired.contains(Pair(p1.id, p2.id)) @@ -172,7 +174,7 @@ open class HistoryHelper(protected val history: List>, scoresGetter: // CB TODO - a big problem with the current naive implementation is that the team score is -for now- the sum of team members individual scores class TeamOfIndividualsHistoryHelper(history: List>, scoresGetter: () -> Map): - HistoryHelper(history, scoresGetter) { + HistoryHelper(history, { scoresGetter() }) { private fun Pairable.asTeam() = this as TeamTournament.Team diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt index b17ba2e..a6265b3 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/MacMahonSolver.kt @@ -13,9 +13,11 @@ class MacMahonSolver(round: Int, BaseSolver(round, history, pairables, pairingParams, placementParams) { override val scores: Map by lazy { - pairablesMap.mapValues { it.value.let { - pairable -> pairable.mmBase + pairable.nbW // TODO take tournament parameter into account - } } + pairablesMap.mapValues { + it.value.let { + pairable -> pairable.mmBase + pairable.nbW // TODO take tournament parameter into account + } + } } override fun debug(p: Pairable) { diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt index c371e49..5c38a21 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/server/ApiServlet.kt @@ -5,6 +5,7 @@ import org.jeudego.pairgoth.api.ApiHandler import org.jeudego.pairgoth.api.PairingHandler import org.jeudego.pairgoth.api.PlayerHandler import org.jeudego.pairgoth.api.ResultsHandler +import org.jeudego.pairgoth.api.StandingsHandler import org.jeudego.pairgoth.api.TeamHandler import org.jeudego.pairgoth.api.TournamentHandler import org.jeudego.pairgoth.util.Colorizer.blue @@ -87,6 +88,7 @@ class ApiServlet: HttpServlet() { "part" -> PlayerHandler "pair" -> PairingHandler "res" -> ResultsHandler + "standings" -> StandingsHandler "team" -> TeamHandler else -> ApiHandler.badRequest("unknown sub-entity: $subEntity") } diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt index 75a0abf..39c426d 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt @@ -7,5 +7,40 @@ import com.republicate.kson.Json */ class PairgothTool { - public fun toMap(array: Json.Array) = array.map { ser -> ser as Json.Object }.associateBy { it.getLong("id")!! } + fun toMap(array: Json.Array) = array.map { ser -> ser as Json.Object }.associateBy { it.getLong("id")!! } + + fun getCriteria() = mapOf( + "NONE" to "No tie break", // No ranking / tie-break + + "CATEGORY" to "Category", + "RANK" to "Rank", + "RATING" to "Rating", + "NBW" to "Number of wins", // Number win + "MMS" to "Mac Mahon score", // Macmahon score + "STS" to "Strasbourg score", // Strasbourg score + "CPS" to "Cup score", // Cup score + + "SOSW" to "Sum of opponents wins", // Sum of opponents NBW + "SOSWM1" to "Sum of opponents wins minus 1", //-1 + "SOSWM2" to "Sum of opponents wins minus 2", //-2 + "SODOSW" to "Sum of defeated opponents wins", // Sum of defeated opponents NBW + "SOSOSW" to "Sum of opponents SOSW", // Sum of opponent SOS + "CUSSW" to "Cumulative sum of opponents wins", // Cumulative sum of scores (NBW) + + "SOSM" to "Sum of opponents Mac Mahon score", // Sum of opponents McMahon score + "SOSMM1" to "Sum of opponents Mac Mahon score minus 1", // Same as previous group but with McMahon score + "SOSMM2" to "Sum of opponents Mac Mahon score minus 2", + "SODOSM" to "Sum of defeated opponents Mac Mahon score", + "SOSOSM" to "Sum of opponents SOSM", + "CUSSM" to "Cumulative sum of opponents Mac Mahon score", + + "SOSTS" to "Sum of opponents Strasbourg score", // Sum of opponnents Strasbourg score + + "EXT" to "Attempted achievements", // Exploits tentes + "EXR" to "Succeeded achievements", // Exploits reussis + + // For the two criteria below see the user documentation + "SDC" to "Simplified direct confrontation", // Simplified direct confrontation + "DC" to "Direct confrontation", // Direct confrontation + ) } \ No newline at end of file diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index bc119e8..cf43577 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -11,6 +11,11 @@ } } + .active-round-box { + padding: 1em; + font-weight: bold; + } + /* information section */ form { @@ -232,4 +237,32 @@ } } + /* standings section */ + + #standings-params { + display: inline-flex; + flex-flow: row wrap; + justify-content: center; + background-color: #eeeeee; + margin-left: auto; + margin-right: auto; + + .criterium { + position: relative; + cursor: pointer; + select { + position: absolute; + top: 100%; + z-index: 2; + &.active { + display: initial; + } + } + select.active { + } + } + #params-submit { + justify-content: space-around; + } + } } diff --git a/view-webapp/src/main/webapp/js/tour-standings.inc.js b/view-webapp/src/main/webapp/js/tour-standings.inc.js new file mode 100644 index 0000000..fa21e4a --- /dev/null +++ b/view-webapp/src/main/webapp/js/tour-standings.inc.js @@ -0,0 +1,42 @@ +onLoad(() => { + $('.criterium').on('click', e => { + let alreadyOpen = e.target.closest('select'); + if (alreadyOpen) return; + let select = e.target.closest('.criterium').find('select'); + $('.criterium select').removeClass('active'); + select.toggleClass('active'); + }); + document.on('click', e => { + let crit = e.target.closest('.criterium'); + if (!crit) $('.criterium select').removeClass('active'); + }); + $('.criterium select').on('input', e => { + let select = e.target.closest('select'); + let info = select.previousElementSibling; + info.textContent = select.selectedOptions[0].value; + $('.criterium select').removeClass('active'); + $('#params-submit').removeClass('hidden'); + }); + $('#params-form .cancel.button').on('click', e => { + $('.criterium select').removeClass('active').forEach(elem => { + elem.value = elem.data('initial'); + elem.previousElementSibling.textContent = elem.value; + }); + $('#params-submit').addClass('hidden'); + }); + $('#params-form').on('submit', e => { + if (!$('#params-submit').hasClass('hidden')) { + api.putJson(`tour/${tour_id}`, { + pairing: { + placement: $('.criterium select').map(elem => elem.value) + } + }).then(rst => { + if (rst !== 'error') { + document.location.reload(); + } + }) + } + e.preventDefault(); + return false; + }); +}); diff --git a/view-webapp/src/main/webapp/tour-pairing.inc.html b/view-webapp/src/main/webapp/tour-pairing.inc.html index 0cfd1e1..41c641a 100644 --- a/view-webapp/src/main/webapp/tour-pairing.inc.html +++ b/view-webapp/src/main/webapp/tour-pairing.inc.html @@ -15,7 +15,7 @@ #end
-
+
Pairings for round $round diff --git a/view-webapp/src/main/webapp/tour-results.inc.html b/view-webapp/src/main/webapp/tour-results.inc.html index e10385a..0e6267d 100644 --- a/view-webapp/src/main/webapp/tour-results.inc.html +++ b/view-webapp/src/main/webapp/tour-results.inc.html @@ -1,5 +1,5 @@
-
+
Results for round $round diff --git a/view-webapp/src/main/webapp/tour-standings.inc.html b/view-webapp/src/main/webapp/tour-standings.inc.html index 050eacd..9af4f5f 100644 --- a/view-webapp/src/main/webapp/tour-standings.inc.html +++ b/view-webapp/src/main/webapp/tour-standings.inc.html @@ -1,3 +1,52 @@ +#macro(placement $i, $p) + + +#end
- Standings... +
+ Standings after round + + $round + +
+
+
+
+#foreach($placement in $tour.pairing.placement) +
+ #set($num = $foreach.index + 1) + + #placement($num $placement) +
+#end +
+ +
+
+
+#set($standings = $api.get("tour/${params.id}/standings/$round")) +#if($standings.isObject() && ($standings.error || $standings.message)) + #if($standings.error) + #set($error = $standings.error) + #else + #set($error = $standings.message) + #end + + #set($standings = []) +#end +#foreach($line in $standings) +
$line
+#end +
diff --git a/view-webapp/src/main/webapp/tour.html b/view-webapp/src/main/webapp/tour.html index 8d0355d..65f0208 100644 --- a/view-webapp/src/main/webapp/tour.html +++ b/view-webapp/src/main/webapp/tour.html @@ -132,7 +132,11 @@ if (input.tagName === 'SELECT') { let sel = input.selectedOptions; if (sel && sel.length === 1) { - info.textContent = sel[0].textContent; + let txt = sel[0].textContent + if (input.hasClass('short-value')) { + txt = txt.replace(/ - .*$/, ''); + } + info.textContent = txt; } } else { if (input.attr('name') === 'location' && $('input[name="online"]')[0].checked) { @@ -169,6 +173,7 @@ #include('/js/tour-registration.inc.js') #include('/js/tour-pairing.inc.js') #include('/js/tour-results.inc.js') +#include('/js/tour-standings.inc.js')