Tables reordering (and use pseudo-ranks for table level), plus some code reorg

This commit is contained in:
Claude Brisson
2024-01-31 11:29:10 +01:00
parent 0f9afbcc65
commit b8f8d45e57
7 changed files with 195 additions and 122 deletions

View File

@@ -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<Json.Object> {
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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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 <P: Pairable>(
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
}

View File

@@ -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

View File

@@ -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');
});

View File

@@ -47,6 +47,10 @@
<i class="angle double left icon"></i>
Unpair
</button>
<button id="renumber-tables" class="ui right labeled icon floating button">
<i class="sync alternate icon"></i>
Renumber
</button>
</div>
<div id="paired" class="multi-select" title="white vs. black">
#foreach($game in $games)