Implement standings freezing

This commit is contained in:
Claude Brisson
2024-08-20 11:48:29 +02:00
parent 15257028c0
commit bae30c7465
11 changed files with 98 additions and 54 deletions

View File

@@ -25,7 +25,7 @@ interface ApiHandler {
notImplemented() notImplemented()
} }
fun put(request: HttpServletRequest, response: HttpServletResponse): Json { fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
notImplemented() notImplemented()
} }

View File

@@ -3,6 +3,7 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.model.Criterion import org.jeudego.pairgoth.model.Criterion
import org.jeudego.pairgoth.model.DatabaseId import org.jeudego.pairgoth.model.DatabaseId
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.MacMahon import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.Pairable.Companion.MIN_RANK import org.jeudego.pairgoth.model.Pairable.Companion.MIN_RANK
@@ -34,6 +35,10 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
else ceil(score - epsilon) else ceil(score - epsilon)
} }
if (frozen != null) {
return ArrayList(frozen!!.map { it -> it as Json.Object })
}
// CB TODO - factorize history helper creation between here and solver classes // CB TODO - factorize history helper creation between here and solver classes
val historyHelper = HistoryHelper(historyBefore(round + 1)) { val historyHelper = HistoryHelper(historyBefore(round + 1)) {
if (pairing.type == PairingType.SWISS) wins.mapValues { Pair(0.0, it.value) } if (pairing.type == PairingType.SWISS) wins.mapValues { Pair(0.0, it.value) }
@@ -116,5 +121,50 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
it.value.forEach { p -> p["place"] = place } it.value.forEach { p -> p["place"] = place }
place += it.value.size place += it.value.size
} }
return sortedPairables return sortedPairables
} }
fun Tournament<*>.populateResultsArray(sortedPairables: List<Json.Object>, round: Int = rounds) {
// fill result
val sortedMap = sortedPairables.associateBy {
it.getID()!!
}
for (r in 1..round) {
games(r).values.forEach { game ->
val white = if (game.white != 0) sortedMap[game.white] else null
val black = if (game.black != 0) sortedMap[game.black] else null
val whiteNum = white?.getInt("num") ?: 0
val blackNum = black?.getInt("num") ?: 0
val whiteColor = if (black == null) "" else "w"
val blackColor = if (white == null) "" else "b"
val handicap = if (game.handicap == 0) "" else "${game.handicap}"
assert(white != null || black != null)
if (white != null) {
val mark = when (game.result) {
Game.Result.UNKNOWN -> "?"
Game.Result.BLACK, Game.Result.BOTHLOOSE -> "-"
Game.Result.WHITE, Game.Result.BOTHWIN -> "+"
Game.Result.JIGO, Game.Result.CANCELLED -> "="
}
val results = white.getArray("results") as Json.MutableArray
results[r - 1] =
if (blackNum == 0) "0$mark"
else "$blackNum$mark/$whiteColor$handicap"
}
if (black != null) {
val mark = when (game.result) {
Game.Result.UNKNOWN -> "?"
Game.Result.BLACK, Game.Result.BOTHWIN -> "+"
Game.Result.WHITE, Game.Result.BOTHLOOSE -> "-"
Game.Result.JIGO, Game.Result.CANCELLED -> "="
}
val results = black.getArray("results") as Json.MutableArray
results[r - 1] =
if (whiteNum == 0) "0$mark"
else "$whiteNum$mark/$blackColor$handicap"
}
}
}
}

View File

@@ -3,7 +3,6 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.api.TournamentHandler.dispatchEvent
import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
@@ -66,7 +65,7 @@ object PairingHandler: PairgothApiHandler {
return ret return ret
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json { override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
// only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...) // only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...)

View File

@@ -31,7 +31,7 @@ object PlayerHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to player.id) return Json.Object("success" to true, "id" to player.id)
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json { override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
val player = tournament.players[id] ?: badRequest("invalid player id") val player = tournament.players[id] ?: badRequest("invalid player id")

View File

@@ -18,7 +18,7 @@ object ResultsHandler: PairgothApiHandler {
return games.map { it.toJson() }.toJsonArray() return games.map { it.toJson() }.toJsonArray()
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json { override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val payload = getObjectPayload(request) val payload = getObjectPayload(request)

View File

@@ -2,27 +2,23 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.PairingHandler.dispatchEvent
import org.jeudego.pairgoth.model.Criterion import org.jeudego.pairgoth.model.Criterion
import org.jeudego.pairgoth.model.Criterion.* import org.jeudego.pairgoth.model.Criterion.*
import org.jeudego.pairgoth.model.Game.Result.* import org.jeudego.pairgoth.model.Game.Result.*
import org.jeudego.pairgoth.model.ID import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.adjustedTime import org.jeudego.pairgoth.model.adjustedTime
import org.jeudego.pairgoth.model.displayRank import org.jeudego.pairgoth.model.displayRank
import org.jeudego.pairgoth.model.getID 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 java.io.PrintWriter import java.io.PrintWriter
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import kotlin.math.max
import kotlin.math.min
import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.* import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.WebappManager import org.jeudego.pairgoth.server.WebappManager
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
@@ -35,46 +31,8 @@ object StandingsHandler: PairgothApiHandler {
val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false
val sortedPairables = tournament.getSortedPairables(round, includePreliminary) val sortedPairables = tournament.getSortedPairables(round, includePreliminary)
val sortedMap = sortedPairables.associateBy { tournament.populateResultsArray(sortedPairables, round)
it.getID()!!
}
for (r in 1..round) {
tournament.games(r).values.forEach { game ->
val white = if (game.white != 0) sortedMap[game.white] else null
val black = if (game.black != 0) sortedMap[game.black] else null
val whiteNum = white?.getInt("num") ?: 0
val blackNum = black?.getInt("num") ?: 0
val whiteColor = if (black == null) "" else "w"
val blackColor = if (white == null) "" else "b"
val handicap = if (game.handicap == 0) "" else "${game.handicap}"
assert(white != null || black != null)
if (white != null) {
val mark = when (game.result) {
UNKNOWN -> "?"
BLACK, BOTHLOOSE -> "-"
WHITE, BOTHWIN -> "+"
JIGO, CANCELLED -> "="
}
val results = white.getArray("results") as Json.MutableArray
results[r - 1] =
if (blackNum == 0) "0$mark"
else "$blackNum$mark/$whiteColor$handicap"
}
if (black != null) {
val mark = when (game.result) {
UNKNOWN -> "?"
BLACK, BOTHWIN -> "+"
WHITE, BOTHLOOSE -> "-"
JIGO, CANCELLED -> "="
}
val results = black.getArray("results") as Json.MutableArray
results[r - 1] =
if (whiteNum == 0) "0$mark"
else "$whiteNum$mark/$blackColor$handicap"
}
}
}
val acceptHeader = request.getHeader("Accept") as String? val acceptHeader = request.getHeader("Accept") as String?
val accept = acceptHeader?.substringBefore(";") val accept = acceptHeader?.substringBefore(";")
val acceptEncoding = acceptHeader?.substringAfter(";charset=", "utf-8") ?: "utf-8" val acceptEncoding = acceptHeader?.substringAfter(";charset=", "utf-8") ?: "utf-8"
@@ -268,6 +226,14 @@ ${
} }
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val sortedPairables = tournament.getSortedPairables(tournament.rounds)
tournament.frozen = sortedPairables.toJsonArray()
tournament.dispatchEvent(Event.TournamentUpdated, request, tournament.toJson())
return Json.Object("status" to "ok")
}
private val numFormat = DecimalFormat("###0.#") private val numFormat = DecimalFormat("###0.#")
private val frDate: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy") private val frDate: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
} }

View File

@@ -29,7 +29,7 @@ object TeamHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to team.id) return Json.Object("success" to true, "id" to team.id)
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json { override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament") if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")

View File

@@ -34,6 +34,7 @@ object TournamentHandler: PairgothApiHandler {
// additional attributes for the webapp // additional attributes for the webapp
json["stats"] = tour.stats() json["stats"] = tour.stats()
json["teamSize"] = tour.type.playersNumber json["teamSize"] = tour.type.playersNumber
json["frozen"] = tour.frozen != null
} }
} }
} ?: badRequest("no tournament with id #${id}") } ?: badRequest("no tournament with id #${id}")
@@ -61,7 +62,7 @@ object TournamentHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to tournament.id) return Json.Object("success" to true, "id" to tournament.id)
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json { override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
// CB TODO - some checks are needed here (cannot lower rounds number if games have been played in removed rounds, for instance) // CB TODO - some checks are needed here (cannot lower rounds number if games have been played in removed rounds, for instance)
val tournament = getTournament(request) val tournament = getTournament(request)
val payload = getObjectPayload(request) val payload = getObjectPayload(request)

View File

@@ -52,6 +52,9 @@ sealed class Tournament <P: Pairable>(
protected val _pairables = mutableMapOf<ID, P>() protected val _pairables = mutableMapOf<ID, P>()
val pairables: Map<ID, Pairable> get() = _pairables val pairables: Map<ID, Pairable> get() = _pairables
// frozen standings
var frozen: Json.Array? = null
// pairing // pairing
fun pair(round: Int, pairables: List<Pairable>): List<Game> { fun pair(round: Int, pairables: List<Pairable>): List<Game> {
// Minimal check on round number. // Minimal check on round number.
@@ -335,7 +338,7 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
tournament.teams[team.getID("id")!!] = tournament.teamFromJson(team) tournament.teams[team.getID("id")!!] = tournament.teamFromJson(team)
} }
} }
(json["games"] as Json.Array?)?.forEachIndexed { i, arr -> json.getArray("games")?.forEachIndexed { i, arr ->
val round = i + 1 val round = i + 1
val tournamentGames = tournament.games(round) val tournamentGames = tournament.games(round)
val games = arr as Json.Array val games = arr as Json.Array
@@ -344,6 +347,9 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
tournamentGames[game.getID("id")!!] = Game.fromJson(game) tournamentGames[game.getID("id")!!] = Game.fromJson(game)
} }
} }
json.getArray("frozen")?.also {
tournament.frozen = it
}
return tournament return tournament
} }
@@ -374,5 +380,8 @@ fun Tournament<*>.toFullJson(): Json.Object {
json["teams"] = Json.Array(teams.values.map { it.toJson() }) json["teams"] = Json.Array(teams.values.map { it.toJson() })
} }
json["games"] = Json.Array((1..lastRound()).mapTo(Json.MutableArray()) { round -> games(round).values.mapTo(Json.MutableArray()) { it.toJson() } }); json["games"] = Json.Array((1..lastRound()).mapTo(Json.MutableArray()) { round -> games(round).values.mapTo(Json.MutableArray()) { it.toJson() } });
if (frozen != null) {
json["frozen"] = frozen
}
return json return json
} }

View File

@@ -24,6 +24,16 @@ function publishHtml() {
close_modal(); close_modal();
} }
function freeze() {
api.put(`tour/${tour_id}/standings/${activeRound}`, {}
).then(resp => {
if (resp.ok) {
document.location.reload();
}
else throw "freeze error"
}).catch(err => showError(err));
}
onLoad(() => { onLoad(() => {
new Tablesort($('#standings-table')[0]); new Tablesort($('#standings-table')[0]);
$('.criterium').on('click', e => { $('.criterium').on('click', e => {
@@ -86,4 +96,7 @@ onLoad(() => {
$('.publish-html').on('click', e => { $('.publish-html').on('click', e => {
publishHtml(); publishHtml();
}); });
$('#freeze').on('click', e => {
freeze()
});
}); });

View File

@@ -105,6 +105,12 @@
</table> </table>
</div> </div>
<div class="right form-actions"> <div class="right form-actions">
#if(!$tour.frozen && $round == $tour.rounds)
<button id="freeze" class="ui orange floating right labeled icon button">
<i class="snowflake plane outline icon"></i>
Freeze
</button>
#end
<button id="publish" class="ui yellow floating right labeled icon button"> <button id="publish" class="ui yellow floating right labeled icon button">
<i class="paper plane outline icon"></i> <i class="paper plane outline icon"></i>
Publish Publish