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,6 +69,7 @@ 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)
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 {
@@ -98,7 +100,36 @@ object PairingHandler: PairgothApiHandler {
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)) {
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
}
@@ -106,6 +137,7 @@ object PairingHandler: PairgothApiHandler {
}
return Json.Object("success" to true)
}
}
override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request)

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 {
var changed = false
var nextTable = 1
games(round).values.filter{ game -> pivot?.let { pivot.id != game.id } ?: true }.sortedBy { game ->
private fun defaultGameOrderBy(game: Game): Int {
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 ->
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(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)