Tables reordering (and use pseudo-ranks for table level), plus some code reorg
This commit is contained in:
@@ -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
|
||||
}
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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');
|
||||
});
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user