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.PairingType
import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.historyBefore
@@ -50,9 +51,9 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
val mmBase = pairable.mmBase()
val score = roundScore(mmBase +
(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
}.sum() * pairing.pairingParams.main.mmsValueAbsent)
} * pairing.pairingParams.main.mmsValueAbsent)
Pair(
if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase
else roundScore(mmBase + round/2),
@@ -100,14 +101,14 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
Criterion.DC -> StandingsHandler.nullMap
}
}
val pairables = pairables.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
pairables.forEach { player ->
val jsonPairables = pairables.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
jsonPairables.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 ->
val sortedPairables = jsonPairables.sortedWith { left, right ->
for (crit in criteria) {
val lval = left.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
}
fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: Int = rounds) {
val sortedMap = sortedPairables.associateBy {
fun Tournament<*>.populateStandings(sortedEntries: List<Json.Object>, round: Int = rounds, individualStandings: Boolean) {
val sortedMap = sortedEntries.associateBy {
it.getID()!!
}
// refresh name, firstname, club and level
val refMap = if (individualStandings) players else pairables
sortedMap.forEach { (id, pairable) ->
val mutable = pairable as Json.MutableObject
pairables[id]?.let {
refMap[id]?.let {
mutable["name"] = it.name
if (it is Player) {
mutable["firstname"] = it.firstname
@@ -150,7 +152,8 @@ fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: I
// fill result
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 black = if (game.black != 0) sortedMap[game.black] else null
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.ID
import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.adjustedTime
import org.jeudego.pairgoth.model.displayRank
@@ -27,10 +28,18 @@ object StandingsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
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)
tournament.populateStandings(sortedPairables, round)
val individualStandings = tournament is TeamTournament &&
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 accept = acceptHeader?.substringBefore(";")
@@ -44,7 +53,7 @@ object StandingsHandler: PairgothApiHandler {
PrintWriter(OutputStreamWriter(response.outputStream, encoding))
}
return when (accept) {
"application/json" -> sortedPairables.toJsonArray()
"application/json" -> sortedEntries.toJsonArray()
"application/egf" -> {
response.contentType = "text/plain;charset=${encoding}"
val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria)
@@ -52,19 +61,19 @@ object StandingsHandler: PairgothApiHandler {
if (neededCriteria.first() == SCOREX) {
neededCriteria.add(1, MMS)
}
exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer)
exportToEGFFormat(tournament, sortedEntries, neededCriteria, writer)
writer.flush()
return null
}
"application/ffg" -> {
response.contentType = "text/plain;charset=${encoding}"
exportToFFGFormat(tournament, sortedPairables, writer)
exportToFFGFormat(tournament, sortedEntries, writer)
writer.flush()
return null
}
"text/csv" -> {
response.contentType = "text/csv;charset=${encoding}"
exportToCSVFormat(tournament, sortedPairables, writer)
exportToCSVFormat(tournament, sortedEntries, writer)
writer.flush()
return null
}

View File

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

View File

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

View File

@@ -184,7 +184,7 @@ unpairable, non disponibles,
supports the implémente le système dappariement
white blanc
White Blanc
white vs. black blanc vs. Noir
white vs. black Blanc vs. Noir
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
yyyymmdd-city aaaammjj-ville

View File

@@ -30,6 +30,9 @@
</div>
</form>
</div>
#if($tour.type.startsWith('TEAM'))
<div class="strong">Team Standings</div>
#end
<div id="standings-container" class="roundbox">
#set($standings = $api.get("tour/${params.id}/standings/$round"))
#if($standings.isObject() && ($standings.error || $standings.message))
@@ -105,6 +108,78 @@
</tbody>
</table>
</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">
#if(!$tour.frozen && $round == $tour.rounds)
<button id="freeze" class="ui orange floating right labeled icon button">