Table renumbering ; various bugfixes and enhancements

This commit is contained in:
Claude Brisson
2024-01-22 11:25:20 +01:00
parent fdf39612ff
commit 354c7cc748
13 changed files with 88 additions and 39 deletions

View File

@@ -16,7 +16,7 @@ interface PairgothApiHandler: ApiHandler {
fun Tournament<*>.dispatchEvent(event: Event, data: Json? = null) { fun Tournament<*>.dispatchEvent(event: Event, data: Json? = null) {
Event.dispatch(event, Json.Object("tournament" to id, "data" to data)) Event.dispatch(event, Json.Object("tournament" to id, "data" to data))
// when storage is not in memory, the tournament has to be persisted // when storage is not in memory, the tournament has to be persisted
if (event != Event.tournamentAdded && event != Event.tournamentDeleted) if (event != Event.TournamentAdded && event != Event.TournamentDeleted)
Store.replaceTournament(this) Store.replaceTournament(this)
} }

View File

@@ -7,7 +7,6 @@ import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID import org.jeudego.pairgoth.model.toID
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
@@ -59,7 +58,7 @@ object PairingHandler: PairgothApiHandler {
} }
val games = tournament.pair(round, pairables) val games = tournament.pair(round, pairables)
val ret = games.map { it.toJson() }.toJsonArray() val ret = games.map { it.toJson() }.toJsonArray()
tournament.dispatchEvent(gamesAdded, Json.Object("round" to round, "games" to ret)) tournament.dispatchEvent(GamesAdded, Json.Object("round" to round, "games" to ret))
return ret return ret
} }
@@ -78,6 +77,7 @@ object PairingHandler: PairgothApiHandler {
game.black = payload.getID("b") ?: badRequest("missing black player id") game.black = payload.getID("b") ?: badRequest("missing black player id")
game.white = payload.getID("w") ?: badRequest("missing white player id") game.white = payload.getID("w") ?: badRequest("missing white player id")
tournament.recomputeDUDD(round, game.id) tournament.recomputeDUDD(round, game.id)
val previousTable = game.table;
// temporary // temporary
payload.getInt("dudd")?.let { game.drawnUpDown = it } payload.getInt("dudd")?.let { game.drawnUpDown = it }
val black = tournament.pairables[game.black] ?: badRequest("invalid black player id") val black = tournament.pairables[game.black] ?: badRequest("invalid black player id")
@@ -90,10 +90,15 @@ object PairingHandler: PairgothApiHandler {
if (playing.contains(white.id)) badRequest("white is already in another game") if (playing.contains(white.id)) badRequest("white is already in another game")
if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap") if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap")
if (payload.containsKey("t")) { if (payload.containsKey("t")) {
// TODO CB - update *all* tables numbers accordingly
game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number") game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number")
} }
tournament.dispatchEvent(gameUpdated, Json.Object("round" to round, "game" to game.toJson())) tournament.dispatchEvent(GameUpdated, Json.Object("round" to round, "game" to game.toJson()))
if (game.table != previousTable && tournament.renumberTables(round, game)) {
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) return Json.Object("success" to true)
} }
@@ -111,7 +116,7 @@ object PairingHandler: PairgothApiHandler {
payload.forEach { payload.forEach {
val id = (it as Number).toInt() val id = (it as Number).toInt()
val game = tournament.games(round)[id] ?: throw Error("invalid game id") val game = tournament.games(round)[id] ?: throw Error("invalid game id")
if (game.result != Game.Result.UNKNOWN) { if (game.result != Game.Result.UNKNOWN && game.black != 0 && game.white != 0) {
ApiHandler.logger.error("cannot unpair game id ${game.id}: it has a result") ApiHandler.logger.error("cannot unpair game id ${game.id}: it has a result")
// we'll only skip it // we'll only skip it
// throw Error("cannot unpair ") // throw Error("cannot unpair ")
@@ -120,7 +125,7 @@ object PairingHandler: PairgothApiHandler {
} }
} }
} }
tournament.dispatchEvent(gamesDeleted, Json.Object("round" to round, "games" to payload)) tournament.dispatchEvent(GamesDeleted, Json.Object("round" to round, "games" to payload))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -5,7 +5,6 @@ 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.model.Player import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
@@ -25,7 +24,7 @@ object PlayerHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val player = Player.fromJson(payload) val player = Player.fromJson(payload)
tournament.players[player.id] = player tournament.players[player.id] = player
tournament.dispatchEvent(playerAdded, player.toJson()) tournament.dispatchEvent(PlayerAdded, player.toJson())
return Json.Object("success" to true, "id" to player.id) return Json.Object("success" to true, "id" to player.id)
} }
@@ -36,7 +35,7 @@ object PlayerHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val updated = Player.fromJson(payload, player) val updated = Player.fromJson(payload, player)
tournament.players[updated.id] = updated tournament.players[updated.id] = updated
tournament.dispatchEvent(playerUpdated, player.toJson()) tournament.dispatchEvent(PlayerUpdated, player.toJson())
return Json.Object("success" to true) return Json.Object("success" to true)
} }
@@ -44,7 +43,7 @@ object PlayerHandler: PairgothApiHandler {
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")
tournament.players.remove(id) ?: badRequest("invalid player id") tournament.players.remove(id) ?: badRequest("invalid player id")
tournament.dispatchEvent(playerDeleted, Json.Object("id" to id)) tournament.dispatchEvent(PlayerDeleted, Json.Object("id" to id))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -24,7 +24,7 @@ object ResultsHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id") val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id")
game.result = Game.Result.fromSymbol(payload.getChar("result") ?: badRequest("missing result")) game.result = Game.Result.fromSymbol(payload.getChar("result") ?: badRequest("missing result"))
tournament.dispatchEvent(Event.resultUpdated, Json.Object("round" to round, "data" to game)) tournament.dispatchEvent(Event.ResultUpdated, Json.Object("round" to round, "data" to game))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -4,7 +4,6 @@ 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.model.TeamTournament import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
@@ -26,7 +25,7 @@ object TeamHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val team = tournament.teamFromJson(payload) val team = tournament.teamFromJson(payload)
tournament.teams[team.id] = team tournament.teams[team.id] = team
tournament.dispatchEvent(teamAdded, team.toJson()) tournament.dispatchEvent(TeamAdded, team.toJson())
return Json.Object("success" to true, "id" to team.id) return Json.Object("success" to true, "id" to team.id)
} }
@@ -38,7 +37,7 @@ object TeamHandler: PairgothApiHandler {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
val updated = tournament.teamFromJson(payload, team) val updated = tournament.teamFromJson(payload, team)
tournament.teams[updated.id] = updated tournament.teams[updated.id] = updated
tournament.dispatchEvent(teamUpdated, team.toJson()) tournament.dispatchEvent(TeamUpdated, team.toJson())
return Json.Object("success" to true) return Json.Object("success" to true)
} }
@@ -47,7 +46,7 @@ object TeamHandler: PairgothApiHandler {
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 team selector") val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid team selector")
tournament.teams.remove(id) ?: badRequest("invalid team id") tournament.teams.remove(id) ?: badRequest("invalid team id")
tournament.dispatchEvent(teamDeleted, Json.Object("id" to id)) tournament.dispatchEvent(TeamDeleted, Json.Object("id" to id))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -11,7 +11,6 @@ import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import org.jeudego.pairgoth.server.ApiServlet import org.jeudego.pairgoth.server.ApiServlet
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.Event.* import org.jeudego.pairgoth.server.Event.*
import org.w3c.dom.Element import org.w3c.dom.Element
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
@@ -44,7 +43,7 @@ object TournamentHandler: PairgothApiHandler {
else -> badRequest("missing or invalid payload") else -> badRequest("missing or invalid payload")
} }
Store.addTournament(tournament) Store.addTournament(tournament)
tournament.dispatchEvent(tournamentAdded, tournament.toJson()) tournament.dispatchEvent(TournamentAdded, tournament.toJson())
return Json.Object("success" to true, "id" to tournament.id) return Json.Object("success" to true, "id" to tournament.id)
} }
@@ -64,14 +63,14 @@ object TournamentHandler: PairgothApiHandler {
clear() clear()
putAll(tournament.games(round)) putAll(tournament.games(round))
} }
updated.dispatchEvent(tournamentUpdated, updated.toJson()) updated.dispatchEvent(TournamentUpdated, updated.toJson())
return Json.Object("success" to true) return Json.Object("success" to true)
} }
override fun delete(request: HttpServletRequest): Json { override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request) val tournament = getTournament(request)
Store.deleteTournament(tournament) Store.deleteTournament(tournament)
tournament.dispatchEvent(tournamentDeleted, Json.Object("id" to tournament.id)) tournament.dispatchEvent(TournamentDeleted, Json.Object("id" to tournament.id))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
} }

View File

@@ -90,6 +90,23 @@ sealed class Tournament <P: Pairable>(
val blackplayer = solver.pairables.find { p-> p.id == game.black }!! val blackplayer = solver.pairables.find { p-> p.id == game.black }!!
game.drawnUpDown = solver.dudd(blackplayer, whiteplayer) game.drawnUpDown = solver.dudd(blackplayer, whiteplayer)
} }
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 ->
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 ->
if (pivot != null && nextTable == pivot.table) {
++nextTable
}
changed = changed || game.table != nextTable
game.table = nextTable++
}
return changed
}
} }
// standard tournament of individuals // standard tournament of individuals

View File

@@ -4,19 +4,20 @@ import info.macias.sse.events.MessageEvent
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
enum class Event { enum class Event {
tournamentAdded, TournamentAdded,
tournamentUpdated, TournamentUpdated,
tournamentDeleted, TournamentDeleted,
playerAdded, PlayerAdded,
playerUpdated, PlayerUpdated,
playerDeleted, PlayerDeleted,
teamAdded, TeamAdded,
teamUpdated, TeamUpdated,
teamDeleted, TeamDeleted,
gamesAdded, GamesAdded,
gamesDeleted, GamesDeleted,
gameUpdated, GameUpdated,
resultUpdated, ResultUpdated,
TablesRenumbered
; ;
companion object { companion object {

View File

@@ -45,4 +45,17 @@ class PairgothTool {
"SDC" to "Simplified direct confrontation", // Simplified direct confrontation "SDC" to "Simplified direct confrontation", // Simplified direct confrontation
"DC" to "Direct confrontation", // Direct confrontation "DC" to "Direct confrontation", // Direct confrontation
) )
fun getResultsStats(games: Collection<Json.Object>): Json.Object {
var total = 0
var known = 0
games
.filter{ it.getInt("b")!! != 0 && it.getInt("w")!! != 0 }
.map { it -> it.getString("r") }
.forEach {
++total
if ("?" != it) ++known
}
return Json.Object("total" to total, "known" to known)
}
} }

View File

@@ -425,6 +425,9 @@
font-size: 1rem !important; font-size: 1rem !important;
line-height: 1.1rem !important; line-height: 1.1rem !important;
min-width: 60vw; min-width: 60vw;
&::before {
top: 0;
}
} }
} }

View File

@@ -145,5 +145,6 @@ the configuration guide le guide de configuration
to à to à
unpairable players joueurs non disponibles unpairable players joueurs non disponibles
version 0.1 supports the version 0.1 supporte le système dappariement version 0.1 supports the version 0.1 supporte le système dappariement
white blanc
white vs. black blanc vs. Noir white vs. black blanc vs. Noir
confirmed. confirmé(s). confirmed. confirmé(s).

View File

@@ -1,4 +1,4 @@
function setResult(id, result) { function setResult(id, result, previous) {
api.putJson(`tour/${tour_id}/res/${activeRound}`, { id: id, result: result }) api.putJson(`tour/${tour_id}/res/${activeRound}`, { id: id, result: result })
.then(res => { .then(res => {
if (res !== 'error') { if (res !== 'error') {
@@ -19,6 +19,16 @@ function setResult(id, result) {
let resultCell = row.find('td.result'); let resultCell = row.find('td.result');
resultCell.text(dispResult).data('result', result); resultCell.text(dispResult).data('result', result);
standingsUpToDate = false; standingsUpToDate = false;
if (previous === '?') {
let indicator = $('#known')[0];
let known = parseInt(indicator.innerText);
indicator.innerText = ++known;
} else if (result === '?') {
let indicator = $('#known')[0];
let known = parseInt(indicator.innerText);
indicator.innerText = --known;
}
} }
}) })
} }
@@ -36,9 +46,9 @@ onLoad(()=>{
$('#results-table .result').on('click', e => { $('#results-table .result').on('click', e => {
let cell = e.target.closest('.result'); let cell = e.target.closest('.result');
let gameId = e.target.closest('tr').data('id'); let gameId = e.target.closest('tr').data('id');
let result = cell.data('result'); let oldResult = cell.data('result');
let index = results.indexOf(result); let index = results.indexOf(oldResult);
result = results[(index + 1)%results.length]; let newResult = results[(index + 1)%results.length];
setResult(gameId, result); setResult(gameId, newResult, oldResult);
}); });
}); });

View File

@@ -4,6 +4,8 @@
<button class="ui floating choose-round prev-round button">&laquo;</button> <button class="ui floating choose-round prev-round button">&laquo;</button>
<span class="active-round">$round</span> <span class="active-round">$round</span>
<button class="ui floating choose-round next-round button">&raquo;</button> <button class="ui floating choose-round next-round button">&raquo;</button>
#set($stats = $utils.getResultsStats($games))
<span class="norbeak"><span id="known">$stats.known</span> / $stats.total</span>
</div> </div>
<div id="results-list" class="roundbox"> <div id="results-list" class="roundbox">
<table id="results-table" class="ui celled striped table"> <table id="results-table" class="ui celled striped table">
@@ -23,7 +25,7 @@
<td>#$game.t</td> <td>#$game.t</td>
<td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name $white.firstname"><span>#if($white)$white.name $white.firstname #rank($white.rank)#{else}BIP#end</span></td> <td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name $white.firstname"><span>#if($white)$white.name $white.firstname #rank($white.rank)#{else}BIP#end</span></td>
<td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name $black.firstname"><span>#if($black)$black.name $black.firstname #rank($black.rank)#{else}BIP#end</span></td> <td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name $black.firstname"><span>#if($black)$black.name $black.firstname #rank($black.rank)#{else}BIP#end</span></td>
<td class="result centered" data-result="$game.r">$dispRst[$game.r]</td> <td class="result centered" data-sort="$game.r" data-result="$game.r">$dispRst[$game.r]</td>
</tr> </tr>
#end #end
#end #end