diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt new file mode 100644 index 0000000..cc995f0 --- /dev/null +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt @@ -0,0 +1,99 @@ +package org.jeudego.pairgoth.api + +import com.republicate.kson.Json +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.Tournament +import org.jeudego.pairgoth.model.getID +import org.jeudego.pairgoth.model.historyBefore +import org.jeudego.pairgoth.pairing.HistoryHelper +import org.jeudego.pairgoth.pairing.solver.MacMahonSolver +import kotlin.math.max +import kotlin.math.min + +// TODO CB avoid code redundancy with solvers + +fun Tournament<*>.mmBase(pairable: Pairable): Double { + if (pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon") + return min(max(pairable.rank, pairing.mmFloor), pairing.mmBar) + MacMahonSolver.mmsZero + pairable.mmsCorrection +} + +fun Tournament<*>.getSortedPairables(round: Int): List { + val historyHelper = HistoryHelper(historyBefore(round + 1)) { + if (pairing.type == PairingType.SWISS) wins + else pairables.mapValues { + it.value.let { + pairable -> + mmBase(pairable) + + (nbW(pairable) ?: 0.0) + // TODO take tournament parameter into account + (1..round).map { round -> + if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0 else 1 + }.sum() * pairing.pairingParams.main.mmsValueAbsent + } + } + } + val neededCriteria = ArrayList(pairing.placementParams.criteria) + if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW) + if (!neededCriteria.contains(Criterion.RATING)) neededCriteria.add(Criterion.RATING) + val criteria = neededCriteria.map { crit -> + crit.name to when (crit) { + Criterion.NONE -> StandingsHandler.nullMap + Criterion.CATEGORY -> StandingsHandler.nullMap + Criterion.RANK -> pairables.mapValues { it.value.rank } + Criterion.RATING -> pairables.mapValues { it.value.rating } + Criterion.NBW -> historyHelper.wins + Criterion.MMS -> historyHelper.mms + Criterion.STS -> StandingsHandler.nullMap + Criterion.CPS -> StandingsHandler.nullMap + + Criterion.SOSW -> historyHelper.sos + Criterion.SOSWM1 -> historyHelper.sosm1 + Criterion.SOSWM2 -> historyHelper.sosm2 + Criterion.SODOSW -> historyHelper.sodos + Criterion.SOSOSW -> historyHelper.sosos + Criterion.CUSSW -> historyHelper.cumScore + Criterion.SOSM -> historyHelper.sos + Criterion.SOSMM1 -> historyHelper.sosm1 + Criterion.SOSMM2 -> historyHelper.sosm2 + Criterion.SODOSM -> historyHelper.sodos + Criterion.SOSOSM -> historyHelper.sosos + Criterion.CUSSM -> historyHelper.cumScore + + Criterion.SOSTS -> StandingsHandler.nullMap + + Criterion.EXT -> StandingsHandler.nullMap + Criterion.EXR -> StandingsHandler.nullMap + + Criterion.SDC -> StandingsHandler.nullMap + Criterion.DC -> StandingsHandler.nullMap + } + } + val pairables = pairables.values.filter { it.final }.map { it.toMutableJson() } + pairables.forEach { player -> + for (crit in criteria) { + player[crit.first] = crit.second[player.getID()] ?: 0.0 + } + player["results"] = Json.MutableArray(List(round) { "0=" }) + } + val sortedPairables = 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 + }.mapIndexed() { i, obj -> + obj.set("num", i+1) + } + var place = 1 + sortedPairables.groupBy { p -> + Triple(p.getDouble(criteria[0].first) ?: 0.0, p.getDouble(criteria[1].first) ?: 0.0, criteria.getOrNull(2)?.let { p.getDouble(it.first) ?: 0.0 }) + }.forEach { + it.value.forEach { p -> p["place"] = place } + place += it.value.size + } + return sortedPairables +} 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 e28db24..4e798b6 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.Game +import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.toID import org.jeudego.pairgoth.model.toJson @@ -20,7 +21,7 @@ object PairingHandler: PairgothApiHandler { val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) }.toSet() - val unpairables = tournament.pairables.values.filter { !it.final || it.skip.contains(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() + val unpairables = tournament.pairables.values.filter { it.final && it.skip.contains(round) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() val pairables = tournament.pairables.values.filter { it.final && !it.skip.contains(round) && !playing.contains(it.id) }.sortedByDescending { it.rating }.map { it.id }.toJsonArray() val games = tournament.games(round).values.sortedBy { if (it.table == 0) Int.MAX_VALUE else it.table @@ -68,43 +69,74 @@ object PairingHandler: PairgothApiHandler { // only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...) if (round != tournament.lastRound()) badRequest("cannot edit pairings in other rounds but the last") val payload = getObjectPayload(request) - val gameId = payload.getInt("id") ?: badRequest("invalid game id") - val game = tournament.games(round)[gameId] ?: badRequest("invalid game id") - val playing = (tournament.games(round).values).filter { it.id != gameId }.flatMap { - listOf(it.black, it.white) - }.toSet() - if (game.result != Game.Result.UNKNOWN && ( - game.black != payload.getInt("b") || - game.white != payload.getInt("w") || - game.handicap != payload.getInt("h") - )) badRequest("Game already has a result") - game.black = payload.getID("b") ?: badRequest("missing black player id") - game.white = payload.getID("w") ?: badRequest("missing white player id") + if (payload.containsKey("id")) { + val gameId = payload.getInt("id") ?: badRequest("invalid game id") + val game = tournament.games(round)[gameId] ?: badRequest("invalid game id") + val playing = (tournament.games(round).values).filter { it.id != gameId }.flatMap { + listOf(it.black, it.white) + }.toSet() + if (game.result != Game.Result.UNKNOWN && ( + game.black != payload.getInt("b") || + game.white != payload.getInt("w") || + game.handicap != payload.getInt("h") + )) badRequest("Game already has a result") + game.black = payload.getID("b") ?: badRequest("missing black player id") + game.white = payload.getID("w") ?: badRequest("missing white player id") - tournament.recomputeHdAndDUDD(round, game.id) - val previousTable = game.table; - // temporary - //payload.getInt("dudd")?.let { game.drawnUpDown = it } - val black = tournament.pairables[game.black] ?: badRequest("invalid black player id") - val white = tournament.pairables[game.black] ?: badRequest("invalid white player id") - if (!black.final) badRequest("black registration status is not final") - if (!white.final) badRequest("white registration status is not final") - if (black.skip.contains(round)) badRequest("black is not playing this round") - if (white.skip.contains(round)) badRequest("white is not playing this round") - if (playing.contains(black.id)) badRequest("black is already in another game") - if (playing.contains(white.id)) badRequest("white is already in another game") - if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap") - if (payload.containsKey("t")) { - game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number") - } - tournament.dispatchEvent(GameUpdated, Json.Object("round" to round, "game" to game.toJson())) - if (game.table != previousTable && tournament.renumberTables(round, game)) { - val games = tournament.games(round).values.sortedBy { - if (it.table == 0) Int.MAX_VALUE else it.table + tournament.recomputeHdAndDUDD(round, game.id) + val previousTable = game.table; + // temporary + //payload.getInt("dudd")?.let { game.drawnUpDown = it } + val black = tournament.pairables[game.black] ?: badRequest("invalid black player id") + val white = tournament.pairables[game.black] ?: badRequest("invalid white player id") + if (!black.final) badRequest("black registration status is not final") + if (!white.final) badRequest("white registration status is not final") + if (black.skip.contains(round)) badRequest("black is not playing this round") + if (white.skip.contains(round)) badRequest("white is not playing this round") + if (playing.contains(black.id)) badRequest("black is already in another game") + if (playing.contains(white.id)) badRequest("white is already in another game") + if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap") + if (payload.containsKey("t")) { + game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number") } - tournament.dispatchEvent(TablesRenumbered, Json.Object("round" to round, "games" to games.map { it.toJson() }.toCollection(Json.MutableArray()))) + tournament.dispatchEvent(GameUpdated, Json.Object("round" to round, "game" to game.toJson())) + if (game.table != previousTable) { + val sortedPairables = tournament.getSortedPairables(round) + val sortedMap = sortedPairables.associateBy { + it.getID()!! + } + val changed = tournament.renumberTables(round, game) { game -> + val whitePosition = sortedMap[game.white]?.getInt("num") ?: Int.MIN_VALUE + val blackPosition = sortedMap[game.black]?.getInt("num") ?: Int.MIN_VALUE + (whitePosition + blackPosition) + } + if (changed) { + val games = tournament.games(round).values.sortedBy { + if (it.table == 0) Int.MAX_VALUE else it.table + } + tournament.dispatchEvent(TablesRenumbered, Json.Object("round" to round, "games" to games.map { it.toJson() }.toCollection(Json.MutableArray()))) + } + } + return Json.Object("success" to true) + } else { + // without id, it's a table renumbering + val sortedPairables = tournament.getSortedPairables(round) + val sortedMap = sortedPairables.associateBy { + it.getID()!! + } + val changed = tournament.renumberTables(round, null) { game -> + val whitePosition = sortedMap[game.white]?.getInt("num") ?: Int.MIN_VALUE + val blackPosition = sortedMap[game.black]?.getInt("num") ?: Int.MIN_VALUE + (whitePosition + blackPosition) + } + if (changed) { + val games = tournament.games(round).values.sortedBy { + if (it.table == 0) Int.MAX_VALUE else it.table + } + tournament.dispatchEvent(TablesRenumbered, Json.Object("round" to round, "games" to games.map { it.toJson() }.toCollection(Json.MutableArray()))) + } + return Json.Object("success" to true) } - return Json.Object("success" to true) } override fun delete(request: HttpServletRequest): Json { 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 1d1da46..c9fa677 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 @@ -30,88 +30,11 @@ object StandingsHandler: PairgothApiHandler { 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 + pairable.mmsCorrection - } - - // CB avoid code redundancy with solvers - val historyHelper = HistoryHelper(tournament.historyBefore(round + 1)) { - 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 - (1..round).map { round -> - if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0 else 1 - }.sum() * tournament.pairing.pairingParams.main.mmsValueAbsent - } - } - } - val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria) - if (!neededCriteria.contains(NBW)) neededCriteria.add(NBW) - val criteria = neededCriteria.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.mms - 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.filter { it.final }.map { it.toMutableJson() } - pairables.forEach { player -> - for (crit in criteria) { - player[crit.first] = crit.second[player.getID()] ?: 0.0 - } - player["results"] = Json.MutableArray(List(round) { "0=" }) - } - val sortedPairables = 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 - }.mapIndexed() { i, obj -> - obj.set("num", i+1) - } + val sortedPairables = tournament.getSortedPairables(round) val sortedMap = sortedPairables.associateBy { it.getID()!! } - var place = 1 - sortedPairables.groupBy { p -> - Triple(p.getDouble(criteria[0].first) ?: 0.0, p.getDouble(criteria[1].first) ?: 0.0, p.getDouble(criteria[2].first) ?: 0.0) - }.forEach { - it.value.forEach { p -> p["place"] = place } - place += it.value.size - } + for (r in 1..round) { tournament.games(r).values.forEach { game -> val white = if (game.white != 0) sortedMap[game.white] else null @@ -152,6 +75,8 @@ object StandingsHandler: PairgothApiHandler { return when(accept) { "application/json" -> sortedPairables.toJsonArray() "application/egf" -> { + val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria) + if (!neededCriteria.contains(NBW)) neededCriteria.add(NBW) exportToEGFFormat(tournament, sortedPairables, neededCriteria, response.writer) return null } 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 2bf1693..d05fbc6 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 @@ -6,12 +6,10 @@ import com.republicate.kson.toJsonArray //import kotlinx.datetime.LocalDate import java.time.LocalDate import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest -import org.jeudego.pairgoth.pairing.HistoryHelper import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.pairing.solver.SwissSolver import org.jeudego.pairgoth.store.Store import kotlin.math.max -import kotlin.math.min import java.util.* import kotlin.math.roundToInt @@ -98,14 +96,16 @@ sealed class Tournament ( acc } - fun renumberTables(round: Int, pivot: Game? = null): Boolean { + private fun defaultGameOrderBy(game: Game): Int { + val whiteRank = pairables[game.white]?.rating ?: Int.MIN_VALUE + val blackRank = pairables[game.black]?.rating ?: Int.MIN_VALUE + return -(whiteRank + blackRank) + } + + fun renumberTables(round: Int, pivot: Game? = null, orderBY: (Game) -> Int = ::defaultGameOrderBy): Boolean { var changed = false var nextTable = 1 - games(round).values.filter{ game -> pivot?.let { pivot.id != game.id } ?: true }.sortedBy { game -> - val whiteRank = pairables[game.white]?.rating ?: Int.MIN_VALUE - val blackRank = pairables[game.black]?.rating ?: Int.MIN_VALUE - -(2 * whiteRank + 2 * blackRank) / 2 - }.forEach { game -> + games(round).values.filter{ game -> pivot?.let { pivot.id != game.id } ?: true }.sortedBy(orderBY).forEach { game -> if (pivot != null && nextTable == pivot.table) { ++nextTable } diff --git a/view-webapp/src/main/webapp/WEB-INF/translations/fr b/view-webapp/src/main/webapp/WEB-INF/translations/fr index d9841db..85bf587 100644 --- a/view-webapp/src/main/webapp/WEB-INF/translations/fr +++ b/view-webapp/src/main/webapp/WEB-INF/translations/fr @@ -85,6 +85,7 @@ Register Inscrire Registration Inscriptions Rengo with 2 players teams Rengo par équipes de 2 Rengo with 3 players team Rengo par équipes de 3 +Renumber Renuméroter Required field Champs requis Reset Mac Mahon groups Réinitialiser les groupes Mac Mahon Results Résultats diff --git a/view-webapp/src/main/webapp/js/tour-pairing.inc.js b/view-webapp/src/main/webapp/js/tour-pairing.inc.js index 87d4706..a95bc5b 100644 --- a/view-webapp/src/main/webapp/js/tour-pairing.inc.js +++ b/view-webapp/src/main/webapp/js/tour-pairing.inc.js @@ -18,6 +18,15 @@ function unpair(games) { }); } +function renumberTables() { + api.putJson(`tour/${tour_id}/pair/${activeRound}`, {}) + .then(rst => { + if (rst !== 'error') { + document.location.reload(); + } + }); +} + function editGame(game) { let t = game.find('.table'); let w = game.find('.white'); @@ -115,6 +124,9 @@ onLoad(()=>{ } unpair(games); }); + $('#renumber-tables').on('click', e => { + renumberTables(); + }); $('#pairing-form [name]').on('input', e => { $('#update-pairing').removeClass('disabled'); }); diff --git a/view-webapp/src/main/webapp/tour-pairing.inc.html b/view-webapp/src/main/webapp/tour-pairing.inc.html index 8cfcd40..46fb0da 100644 --- a/view-webapp/src/main/webapp/tour-pairing.inc.html +++ b/view-webapp/src/main/webapp/tour-pairing.inc.html @@ -47,6 +47,10 @@ Unpair +
#foreach($game in $games)