Display individual standings below team standings

This commit is contained in:
Claude Brisson
2025-06-11 11:03:58 +02:00
parent be18f159be
commit 17bb013feb
6 changed files with 162 additions and 18 deletions

View File

@@ -7,6 +7,7 @@ import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Pairable import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.historyBefore import org.jeudego.pairgoth.model.historyBefore
@@ -50,9 +51,9 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
val mmBase = pairable.mmBase() val mmBase = pairable.mmBase()
val score = roundScore(mmBase + val score = roundScore(mmBase +
(nbW(pairable) ?: 0.0) + (nbW(pairable) ?: 0.0) +
(1..round).map { round -> (1..round).sumOf { round ->
if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0.0 else 1.0 if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0.0 else 1.0
}.sum() * pairing.pairingParams.main.mmsValueAbsent) } * pairing.pairingParams.main.mmsValueAbsent)
Pair( Pair(
if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase
else roundScore(mmBase + round/2), else roundScore(mmBase + round/2),
@@ -100,14 +101,14 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
Criterion.DC -> StandingsHandler.nullMap Criterion.DC -> StandingsHandler.nullMap
} }
} }
val pairables = pairables.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() } val jsonPairables = pairables.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
pairables.forEach { player -> jsonPairables.forEach { player ->
for (crit in criteria) { for (crit in criteria) {
player[crit.first] = crit.second[player.getID()] ?: 0.0 player[crit.first] = crit.second[player.getID()] ?: 0.0
} }
player["results"] = Json.MutableArray(List(round) { "0=" }) player["results"] = Json.MutableArray(List(round) { "0=" })
} }
val sortedPairables = pairables.sortedWith { left, right -> val sortedPairables = jsonPairables.sortedWith { left, right ->
for (crit in criteria) { for (crit in criteria) {
val lval = left.getDouble(crit.first) ?: 0.0 val lval = left.getDouble(crit.first) ?: 0.0
val rval = right.getDouble(crit.first) ?: 0.0 val rval = right.getDouble(crit.first) ?: 0.0
@@ -129,15 +130,16 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
return sortedPairables return sortedPairables
} }
fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: Int = rounds) { fun Tournament<*>.populateStandings(sortedEntries: List<Json.Object>, round: Int = rounds, individualStandings: Boolean) {
val sortedMap = sortedPairables.associateBy { val sortedMap = sortedEntries.associateBy {
it.getID()!! it.getID()!!
} }
// refresh name, firstname, club and level // refresh name, firstname, club and level
val refMap = if (individualStandings) players else pairables
sortedMap.forEach { (id, pairable) -> sortedMap.forEach { (id, pairable) ->
val mutable = pairable as Json.MutableObject val mutable = pairable as Json.MutableObject
pairables[id]?.let { refMap[id]?.let {
mutable["name"] = it.name mutable["name"] = it.name
if (it is Player) { if (it is Player) {
mutable["firstname"] = it.firstname mutable["firstname"] = it.firstname
@@ -150,7 +152,8 @@ fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: I
// fill result // fill result
for (r in 1..round) { for (r in 1..round) {
games(r).values.forEach { game -> val roundGames = if (individualStandings) individualGames(r) else games(r)
roundGames.values.forEach { game ->
val white = if (game.white != 0) sortedMap[game.white] else null val white = if (game.white != 0) sortedMap[game.white] else null
val black = if (game.black != 0) sortedMap[game.black] else null val black = if (game.black != 0) sortedMap[game.black] else null
val whiteNum = white?.getInt("num") ?: 0 val whiteNum = white?.getInt("num") ?: 0
@@ -186,3 +189,56 @@ fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: I
} }
} }
} }
fun TeamTournament.getSortedTeamMembers(round: Int, includePreliminary: Boolean = false): List<Json.Object> {
val teamGames = historyBefore(round + 1)
val individualHistory = teamGames.map { roundTeamGames ->
roundTeamGames.flatMap { game -> individualGames[game.id]?.toList() ?: listOf() }
}
val historyHelper = HistoryHelper(individualHistory) {
pairables.mapValues {
Pair(0.0, wins[it.key] ?: 0.0)
}
}
val neededCriteria = mutableListOf(Criterion.NBW, Criterion.RATING)
val criteria = neededCriteria.map { crit ->
crit.name to when (crit) {
Criterion.NBW -> historyHelper.wins
Criterion.RANK -> pairables.mapValues { it.value.rank }
Criterion.RATING -> pairables.mapValues { it.value.rating }
else -> null
}
}
val jsonPlayers = players.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
jsonPlayers.forEach { player ->
for (crit in criteria) {
player[crit.first] = crit.second?.get(player.getID()) ?: 0.0
}
player["results"] = Json.MutableArray(List(round) { "0=" })
}
val sortedPlayers = jsonPlayers.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
sortedPlayers.groupBy { p ->
Triple(
criteria.getOrNull(0)?.first?.let { crit -> p.getDouble(crit) ?: 0.0 } ?: 0.0,
criteria.getOrNull(1)?.first?.let { crit -> p.getDouble(crit) ?: 0.0 } ?: 0.0,
criteria.getOrNull(2)?.first?.let { crit -> p.getDouble(crit) ?: 0.0 } ?: 0.0
)
}.forEach {
it.value.forEach { p -> p["place"] = place }
place += it.value.size
}
return sortedPlayers
}

View File

@@ -6,6 +6,7 @@ import org.jeudego.pairgoth.model.Criterion
import org.jeudego.pairgoth.model.Criterion.* import org.jeudego.pairgoth.model.Criterion.*
import org.jeudego.pairgoth.model.ID import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.TeamTournament
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
@@ -27,10 +28,18 @@ object StandingsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: tournament.rounds val round = getSubSelector(request)?.toIntOrNull() ?: tournament.rounds
val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false val includePreliminary = request.getParameter("include_preliminary")?.toBoolean() ?: false
val sortedPairables = tournament.getSortedPairables(round, includePreliminary) val individualStandings = tournament is TeamTournament &&
tournament.populateStandings(sortedPairables, round) tournament.type.individual &&
request.getParameter("individual_standings")?.toBoolean() == true
val sortedEntries = if (individualStandings) {
tournament.getSortedTeamMembers(round)
} else {
tournament.getSortedPairables(round, includePreliminary)
}
tournament.populateStandings(sortedEntries, round, individualStandings)
val acceptHeader = request.getHeader("Accept") as String? val acceptHeader = request.getHeader("Accept") as String?
val accept = acceptHeader?.substringBefore(";") val accept = acceptHeader?.substringBefore(";")
@@ -44,7 +53,7 @@ object StandingsHandler: PairgothApiHandler {
PrintWriter(OutputStreamWriter(response.outputStream, encoding)) PrintWriter(OutputStreamWriter(response.outputStream, encoding))
} }
return when (accept) { return when (accept) {
"application/json" -> sortedPairables.toJsonArray() "application/json" -> sortedEntries.toJsonArray()
"application/egf" -> { "application/egf" -> {
response.contentType = "text/plain;charset=${encoding}" response.contentType = "text/plain;charset=${encoding}"
val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria) val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria)
@@ -52,19 +61,19 @@ object StandingsHandler: PairgothApiHandler {
if (neededCriteria.first() == SCOREX) { if (neededCriteria.first() == SCOREX) {
neededCriteria.add(1, MMS) neededCriteria.add(1, MMS)
} }
exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer) exportToEGFFormat(tournament, sortedEntries, neededCriteria, writer)
writer.flush() writer.flush()
return null return null
} }
"application/ffg" -> { "application/ffg" -> {
response.contentType = "text/plain;charset=${encoding}" response.contentType = "text/plain;charset=${encoding}"
exportToFFGFormat(tournament, sortedPairables, writer) exportToFFGFormat(tournament, sortedEntries, writer)
writer.flush() writer.flush()
return null return null
} }
"text/csv" -> { "text/csv" -> {
response.contentType = "text/csv;charset=${encoding}" response.contentType = "text/csv;charset=${encoding}"
exportToCSVFormat(tournament, sortedPairables, writer) exportToCSVFormat(tournament, sortedEntries, writer)
writer.flush() writer.flush()
return null return null
} }

View File

@@ -263,7 +263,7 @@ class TeamTournament(
override fun individualGames(round: Int): Map<ID, Game> { override fun individualGames(round: Int): Map<ID, Game> {
val teamGames = games(round) val teamGames = games(round)
return if (type.individual) { return if (type.individual) {
return teamGames.values.flatMap { game -> teamGames.values.flatMap { game ->
if (game.white == 0 || game.black == 0 ) listOf() if (game.white == 0 || game.black == 0 ) listOf()
else individualGames[game.id]?.toList() ?: listOf() else individualGames[game.id]?.toList() ?: listOf()
}.associateBy { it.id } }.associateBy { it.id }

View File

@@ -56,6 +56,10 @@
font-style: italic; font-style: italic;
} }
.strong {
font-weight: bold;
}
/* header, center, footer */ /* header, center, footer */
#header { #header {

View File

@@ -184,7 +184,7 @@ unpairable, non disponibles,
supports the implémente le système dappariement supports the implémente le système dappariement
white blanc white blanc
White Blanc White Blanc
white vs. black blanc vs. Noir white vs. black Blanc vs. Noir
confirmed. confirmé(s). confirmed. confirmé(s).
Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us La connexion à cette instance est réservée aux acteurs de la FFG et à quelques personnes extérieures, à notre discrétion. Envoyez-nous Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us La connexion à cette instance est réservée aux acteurs de la FFG et à quelques personnes extérieures, à notre discrétion. Envoyez-nous
yyyymmdd-city aaaammjj-ville yyyymmdd-city aaaammjj-ville

View File

@@ -30,6 +30,9 @@
</div> </div>
</form> </form>
</div> </div>
#if($tour.type.startsWith('TEAM'))
<div class="strong">Team Standings</div>
#end
<div id="standings-container" class="roundbox"> <div id="standings-container" class="roundbox">
#set($standings = $api.get("tour/${params.id}/standings/$round")) #set($standings = $api.get("tour/${params.id}/standings/$round"))
#if($standings.isObject() && ($standings.error || $standings.message)) #if($standings.isObject() && ($standings.error || $standings.message))
@@ -105,6 +108,78 @@
</tbody> </tbody>
</table> </table>
</div> </div>
#if($tour.type.startsWith('TEAM'))
<div class="strong">Individual Standings</div>
<div id="individual-standings-container" class="roundbox">
#set($indvstandings = $api.get("tour/${params.id}/standings/$round?individual_standings=true"))
#if($indvstandings.isObject() && ($indvstandings.error || $indvstandings.message))
#if($indvstandings.error)
#set($error = $indvstandings.error)
#else
#set($error = $indvstandings.message)
#end
<script type="text/javascript">
onLoad(() => {
showError("$error")
});
</script>
#set($indvstandings = [])
#end
#set($indvsmap = {})
#foreach($part in $indvstandings)
#set($indvsmap[$part.num] = $part)
#end
<table id="individual-standings-table" class="ui striped table">
<thead>
<th>Num</th>
<th>Plc</th>
<th>Name</th>
<th>Rank</th>
<th>Ctr</th>
<th>Nbw</th>
#foreach($r in [1..$round])
<th>R$r</th>
#end
#set($indvcriteres = ['NBW'])
#foreach($crit in $indvcriteres)
<th>$crit</th>
#end
</thead>
<tbody>
#foreach($part in $indvstandings)
<tr data-id="$part.id">
<td>$part.num</td>
<td>$part.place</td>
<td>$esc.html($part.name)#if($part.firstname) $esc.html($part.firstname)#end</td>
<td data-sort="$part.rank">#rank($part.rank)</td>
<td>#if($part.country)$part.country#end</td>
<td>$number.format('0.#', $part.NBW)</td>
#set($mx = $round - 1)
#foreach($r in [0..$mx])
#set($rst = $part.results[$r])
#set($opp_num = $math.toLong($rst))
#if($opp_num)
#set($opponent = $!indvsmap[$opp_num])
#else
#set($opponent = false)
#end
#if($rst.contains('+'))
#set($rst = "<b>$rst</b>")
#elseif($rst.contains('-'))
#set($rst = "<i>$rst</i>")
#end
<td class="nobreak game-result" #if($opponent)title="$esc.html($opponent.name)#if($opponent.firstname) $esc.html($opponent.firstname)#end #rank($opponent.rank)#if($opponent.country) $opponent.country#end"#end>$rst</td>
#end
#foreach($crit in $indvcriteres)
#set($value = "$number.format('0.#', $part[$crit])")
<td data-sort="$value">$value.replace('.5', '½')</td>
#end
</tr>
#end
</tbody>
</table>
</div>
#end
<div class="right form-actions"> <div class="right form-actions">
#if(!$tour.frozen && $round == $tour.rounds) #if(!$tour.frozen && $round == $tour.rounds)
<button id="freeze" class="ui orange floating right labeled icon button"> <button id="freeze" class="ui orange floating right labeled icon button">