Merge branch 'master' into 'translations'

# Conflicts:
#   view-webapp/src/main/webapp/WEB-INF/translations/kr
This commit is contained in:
Quentin RENDU
2024-09-03 14:22:22 +00:00
46 changed files with 2545 additions and 514 deletions

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.jeudego.pairgoth</groupId> <groupId>org.jeudego.pairgoth</groupId>
<artifactId>engine-parent</artifactId> <artifactId>engine-parent</artifactId>
<version>0.14</version> <version>0.15</version>
</parent> </parent>
<artifactId>api-webapp</artifactId> <artifactId>api-webapp</artifactId>
@@ -159,6 +159,11 @@
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>3.12.0</version> <version>3.12.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.8</version>
</dependency>
<dependency> <dependency>
<groupId>commons-io</groupId> <groupId>commons-io</groupId>
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>

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

@@ -2,10 +2,9 @@ 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.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.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
@@ -16,11 +15,11 @@ import kotlin.math.ceil
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.round
// TODO CB avoid code redundancy with solvers // TODO CB avoid code redundancy with solvers
fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> { fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = false): List<Json.Object> {
fun Pairable.mmBase(): Double { fun Pairable.mmBase(): Double {
if (pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon") if (pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon")
@@ -31,9 +30,14 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
val epsilon = 0.00001 val epsilon = 0.00001
// Note: this works for now because we only have .0 and .5 fractional parts // Note: this works for now because we only have .0 and .5 fractional parts
return if (pairing.pairingParams.main.roundDownScore) floor(score + epsilon) return if (pairing.pairingParams.main.roundDownScore) floor(score + epsilon)
else ceil(score - epsilon) else round(2 * score) / 2
} }
if (frozen != null) {
return ArrayList(frozen!!.map { it -> it as Json.Object })
}
// 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) }
else pairables.mapValues { else pairables.mapValues {
@@ -42,7 +46,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
val score = roundScore(mmBase + val score = roundScore(mmBase +
(nbW(pairable) ?: 0.0) + (nbW(pairable) ?: 0.0) +
(1..round).map { round -> (1..round).map { round ->
if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0 else 1 if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0.0 else 1.0
}.sum() * pairing.pairingParams.main.mmsValueAbsent) }.sum() * pairing.pairingParams.main.mmsValueAbsent)
Pair( Pair(
if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase
@@ -55,6 +59,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
val neededCriteria = ArrayList(pairing.placementParams.criteria) val neededCriteria = ArrayList(pairing.placementParams.criteria)
if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW) if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW)
if (!neededCriteria.contains(Criterion.RATING)) neededCriteria.add(Criterion.RATING) if (!neededCriteria.contains(Criterion.RATING)) neededCriteria.add(Criterion.RATING)
if (type == Tournament.Type.INDIVIDUAL && pairing.type == PairingType.MAC_MAHON && !neededCriteria.contains(Criterion.MMS)) neededCriteria.add(Criterion.MMS)
val criteria = neededCriteria.map { crit -> val criteria = neededCriteria.map { crit ->
crit.name to when (crit) { crit.name to when (crit) {
Criterion.NONE -> StandingsHandler.nullMap Criterion.NONE -> StandingsHandler.nullMap
@@ -63,6 +68,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
Criterion.RATING -> pairables.mapValues { it.value.rating } Criterion.RATING -> pairables.mapValues { it.value.rating }
Criterion.NBW -> historyHelper.wins Criterion.NBW -> historyHelper.wins
Criterion.MMS -> historyHelper.mms Criterion.MMS -> historyHelper.mms
Criterion.SCOREX -> historyHelper.scoresX
Criterion.STS -> StandingsHandler.nullMap Criterion.STS -> StandingsHandler.nullMap
Criterion.CPS -> StandingsHandler.nullMap Criterion.CPS -> StandingsHandler.nullMap
@@ -71,7 +77,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
Criterion.SOSWM2 -> historyHelper.sosm2 Criterion.SOSWM2 -> historyHelper.sosm2
Criterion.SODOSW -> historyHelper.sodos Criterion.SODOSW -> historyHelper.sodos
Criterion.SOSOSW -> historyHelper.sosos Criterion.SOSOSW -> historyHelper.sosos
Criterion.CUSSW -> historyHelper.cumScore Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else historyHelper.cumScore
Criterion.SOSM -> historyHelper.sos Criterion.SOSM -> historyHelper.sos
Criterion.SOSMM1 -> historyHelper.sosm1 Criterion.SOSMM1 -> historyHelper.sosm1
Criterion.SOSMM2 -> historyHelper.sosm2 Criterion.SOSMM2 -> historyHelper.sosm2
@@ -88,10 +94,10 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
Criterion.DC -> StandingsHandler.nullMap Criterion.DC -> StandingsHandler.nullMap
} }
} }
val pairables = pairables.values.filter { it.final }.map { it.toDetailedJson() } val pairables = pairables.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
pairables.forEach { player -> pairables.forEach { player ->
for (crit in criteria) { for (crit in criteria) {
player[crit.first] = (crit.second[player.getID()] ?: 0.0).toInt() player[crit.first] = crit.second[player.getID()] ?: 0.0
} }
player["results"] = Json.MutableArray(List(round) { "0=" }) player["results"] = Json.MutableArray(List(round) { "0=" })
} }
@@ -113,5 +119,61 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
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<*>.populateFrozenStandings(sortedPairables: List<Json.Object>, round: Int = rounds) {
val sortedMap = sortedPairables.associateBy {
it.getID()!!
}
// refresh name, firstname, club and level
sortedMap.forEach { (id, player) ->
val mutable = player as Json.MutableObject
val live = players[id]!!
mutable["name"] = live.name
mutable["firstname"] = live.firstname
mutable["club"] = live.club
mutable["rating"] = live.rating
mutable["rank"] = live.rank
}
// fill result
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

@@ -4,6 +4,7 @@ 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.Game import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Tournament
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
@@ -38,6 +39,7 @@ object PairingHandler: PairgothApiHandler {
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played") if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
val payload = getArrayPayload(request) val payload = getArrayPayload(request)
if (payload.isEmpty()) badRequest("nobody to pair") if (payload.isEmpty()) badRequest("nobody to pair")
// CB TODO - change convention to empty array for all players
val allPlayers = payload.size == 1 && payload[0] == "all" val allPlayers = payload.size == 1 && payload[0] == "all"
//if (!allPlayers && tournament.pairing.type == PairingType.SWISS) badRequest("Swiss pairing requires all pairable players") //if (!allPlayers && tournament.pairing.type == PairingType.SWISS) badRequest("Swiss pairing requires all pairable players")
val playing = (tournament.games(round).values).flatMap { val playing = (tournament.games(round).values).flatMap {
@@ -57,16 +59,18 @@ object PairingHandler: PairgothApiHandler {
} ?: badRequest("invalid pairable id: #$id") } ?: badRequest("invalid pairable id: #$id")
} }
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, request, Json.Object("round" to round, "games" to ret)) tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret))
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...)
if (round != tournament.lastRound()) badRequest("cannot edit pairings in other rounds but the last") // TODO - check in next line commented out: following founds can exist, but be empty...
// if (round != tournament.lastRound()) badRequest("cannot edit pairings in other rounds but the last")
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
if (payload.containsKey("id")) { if (payload.containsKey("id")) {
val gameId = payload.getInt("id") ?: badRequest("invalid game id") val gameId = payload.getInt("id") ?: badRequest("invalid game id")
@@ -97,38 +101,41 @@ object PairingHandler: PairgothApiHandler {
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")) {
game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number") game.table = payload.getString("t")?.toIntOrNull() ?: badRequest("invalid table number")
game.forcedTable = true
} }
tournament.dispatchEvent(GameUpdated, request, Json.Object("round" to round, "game" to game.toJson())) tournament.dispatchEvent(GameUpdated, request, Json.Object("round" to round, "game" to game.toJson()))
if (game.table != previousTable) { if (game.table != previousTable) {
val sortedPairables = tournament.getSortedPairables(round) val tableWasOccupied = ( tournament.games(round).values.find { g -> g != game && g.table == game.table } != null )
val sortedMap = sortedPairables.associateBy { if (tableWasOccupied) {
it.getID()!! // some renumbering is necessary
} renumberTables(request, tournament, round, game)
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, request,
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)
} else { } else {
// without id, it's a table renumbering // without id, it's a table renumbering
if (payload.containsKey("excludeTables")) {
val tablesExclusion = payload.getString("excludeTables") ?: badRequest("missing 'excludeTables'")
TournamentHandler.validateTablesExclusion(tablesExclusion)
while (tournament.tablesExclusion.size < round) tournament.tablesExclusion.add("")
tournament.tablesExclusion[round - 1] = tablesExclusion
tournament.dispatchEvent(TournamentUpdated, request, tournament.toJson())
}
renumberTables(request, tournament, round)
return Json.Object("success" to true)
}
}
private fun renumberTables(request: HttpServletRequest, tournament: Tournament<*>, round: Int, pivot: Game? = null) {
val sortedPairables = tournament.getSortedPairables(round) val sortedPairables = tournament.getSortedPairables(round)
val sortedMap = sortedPairables.associateBy { val sortedMap = sortedPairables.associateBy {
it.getID()!! it.getID()!!
} }
val changed = tournament.renumberTables(round, null) { game -> val changed = tournament.renumberTables(round, pivot) { gm ->
val whitePosition = sortedMap[game.white]?.getInt("num") ?: Int.MIN_VALUE val whitePosition = sortedMap[gm.white]?.getInt("num") ?: Int.MIN_VALUE
val blackPosition = sortedMap[game.black]?.getInt("num") ?: Int.MIN_VALUE val blackPosition = sortedMap[gm.black]?.getInt("num") ?: Int.MIN_VALUE
(whitePosition + blackPosition) (whitePosition + blackPosition)
} }
if (changed) { if (changed) {
@@ -137,11 +144,13 @@ object PairingHandler: PairgothApiHandler {
} }
tournament.dispatchEvent( tournament.dispatchEvent(
TablesRenumbered, request, TablesRenumbered, request,
Json.Object("round" to round, "games" to games.map { it.toJson() }.toCollection(Json.MutableArray())) Json.Object(
"round" to round,
"games" to games.map { it.toJson() }.toCollection(Json.MutableArray())
)
) )
} }
return Json.Object("success" to true)
}
} }
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json { override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {

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")
@@ -46,7 +46,7 @@ object PlayerHandler: PairgothApiHandler {
if (round <= tournament.lastRound()) { if (round <= tournament.lastRound()) {
val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) } val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) }
if (playing.contains(id)) { if (playing.contains(id)) {
throw badRequest("player is playing in round #$round") badRequest("player is playing in round #$round")
} }
} }
} }

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

@@ -4,76 +4,35 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
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.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.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
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.Normalizer
import java.util.*
import kotlin.collections.ArrayList
object StandingsHandler: PairgothApiHandler { 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() ?: ApiHandler.badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: tournament.rounds
val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false
val sortedPairables = tournament.getSortedPairables(round) val sortedPairables = tournament.getSortedPairables(round, includePreliminary)
val sortedMap = sortedPairables.associateBy { tournament.populateFrozenStandings(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"
@@ -91,6 +50,9 @@ object StandingsHandler: PairgothApiHandler {
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)
if (!neededCriteria.contains(NBW)) neededCriteria.add(NBW) if (!neededCriteria.contains(NBW)) neededCriteria.add(NBW)
if (neededCriteria.first() == SCOREX) {
neededCriteria.add(1, MMS)
}
exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer) exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer)
writer.flush() writer.flush()
return null return null
@@ -161,13 +123,18 @@ ${
"${ "${
player.getString("num")!!.padStart(4, ' ') player.getString("num")!!.padStart(4, ' ')
} ${ } ${
"${player.getString("name")} ${player.getString("firstname") ?: ""}".padEnd(30, ' ').take(30) "${
player.getString("name")?.toSnake(true)
} ${
player.getString("firstname")?.toSnake() ?: ""
}".padEnd(30, ' ').take(30)
} ${ } ${
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ') displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
} ${ } ${
player.getString("country")?.uppercase() ?: "" player.getString("country")?.uppercase() ?: ""
} ${ } ${
(player.getString("club") ?: "").padStart(4).take(4) (player.getString("club") ?: "").toSnake().padStart(4).take(4)
} ${ } ${
criteria.joinToString(" ") { numFormat.format(player.getDouble(it.name)!!).let { if (it.contains('.')) it else "$it " }.padStart(7, ' ') } criteria.joinToString(" ") { numFormat.format(player.getDouble(it.name)!!).let { if (it.contains('.')) it else "$it " }.padStart(7, ' ') }
} ${ } ${
@@ -181,6 +148,24 @@ ${
writer.println(ret) writer.println(ret)
} }
private fun String.toSnake(upper: Boolean = false): String {
val sanitized = sanitizeISO()
val parts = sanitized.trim().split(Regex("(?:\\s|\\xA0)+"))
val snake = parts.joinToString("_") { part ->
if (upper) part.uppercase(Locale.ROOT)
else part.capitalize()
}
return snake
}
private fun String.sanitizeISO(): String {
val ret = Normalizer.normalize(this, Normalizer.Form.NFD)
return ret.replace(Regex("\\p{M}"), "")
// some non accented letters give problems in ISO, there may be other
.replace('Ð', 'D')
.replace('ø', 'o')
}
private fun exportToFFGFormat(tournament: Tournament<*>, lines: List<Json.Object>, writer: PrintWriter) { private fun exportToFFGFormat(tournament: Tournament<*>, lines: List<Json.Object>, writer: PrintWriter) {
val version = WebappManager.properties.getProperty("version")!! val version = WebappManager.properties.getProperty("version")!!
val ret = val ret =
@@ -209,14 +194,14 @@ ${
"${ "${
player.getString("num")!!.padStart(4, ' ') player.getString("num")!!.padStart(4, ' ')
} ${ } ${
"${player.getString("name")} ${player.getString("firstname") ?: ""}".padEnd(24, ' ').take(24) "${player.getString("name")?.toSnake(true)} ${player.getString("firstname")?.toSnake() ?: ""}".padEnd(24, ' ').take(24)
} ${ } ${
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ') displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
} ${ } ${
player.getString("ffg") ?: " " player.getString("ffg") ?: " "
} ${ } ${
if (player.getString("country") == "FR") if (player.getString("country") == "FR")
(player.getString("club") ?: "").padEnd(4).take(4) (player.getString("club") ?: "").toSnake().padEnd(4).take(4)
else else
(player.getString("country") ?: "").padEnd(4).take(4) (player.getString("country") ?: "").padEnd(4).take(4)
} ${ } ${
@@ -264,6 +249,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

@@ -2,9 +2,11 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import com.republicate.kson.toJsonObject import com.republicate.kson.toJsonObject
import com.republicate.kson.toMutableJsonObject
import org.jeudego.pairgoth.api.ApiHandler.Companion.PAYLOAD_KEY import org.jeudego.pairgoth.api.ApiHandler.Companion.PAYLOAD_KEY
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.ext.OpenGotha import org.jeudego.pairgoth.ext.OpenGotha
import org.jeudego.pairgoth.model.BaseCritParams
import org.jeudego.pairgoth.model.TeamTournament import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
@@ -34,6 +36,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,12 +64,61 @@ 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).toMutableJsonObject()
// disallow changing type // disallow changing type
if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed") if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed")
// specific handling for 'excludeTables'
if (payload.containsKey("excludeTables")) {
val tablesExclusion = payload.getString("excludeTables") ?: badRequest("missing 'excludeTables'")
validateTablesExclusion(tablesExclusion)
val round = payload.getInt("round") ?: badRequest("missing 'round'")
while (tournament.tablesExclusion.size < round) tournament.tablesExclusion.add("")
tournament.tablesExclusion[round - 1] = tablesExclusion
tournament.dispatchEvent(TournamentUpdated, request, tournament.toJson())
} else {
// translate client-side conventions to actual parameters
val base = payload.getObject("pairing")?.getObject("base") as Json.MutableObject?
if (base != null) {
base.getString("randomness")?.let { randomness ->
when (randomness) {
"none" -> {
base["random"] = 0.0
base["deterministic"] = true
}
"deterministic" -> {
base["random"] = BaseCritParams.MAX_RANDOM
base["deterministic"] = true
}
"non-deterministic" -> {
base["random"] = BaseCritParams.MAX_RANDOM
base["deterministic"] = false
}
else -> badRequest("invalid randomness parameter: $randomness")
}
}
base.getBoolean("colorBalance")?.let { colorBalance ->
base["colorBalanceWeight"] =
if (colorBalance) BaseCritParams.MAX_COLOR_BALANCE
else 0.0
}
}
val main = payload.getObject("pairing")?.getObject("main") as Json.MutableObject?
if (main != null) {
main.getBoolean("firstSeedAddRating")?.let { firstSeedAddRating ->
main["firstSeedAddCrit"] =
if (firstSeedAddRating) "RATING"
else "NONE"
}
main.getBoolean("secondSeedAddRating")?.let { secondSeedAddRating ->
main["secondSeedAddCrit"] =
if (secondSeedAddRating) "RATING"
else "NONE"
}
}
// prepare updated tournament version
val updated = Tournament.fromJson(payload, tournament) val updated = Tournament.fromJson(payload, tournament)
// copy players, games, criteria (this copy should be provided by the Tournament class - CB TODO) // copy players, games, criteria (this copy should be provided by the Tournament class - CB TODO)
updated.players.putAll(tournament.players) updated.players.putAll(tournament.players)
@@ -78,13 +130,20 @@ object TournamentHandler: PairgothApiHandler {
putAll(tournament.games(round)) putAll(tournament.games(round))
} }
updated.dispatchEvent(TournamentUpdated, request, updated.toJson()) updated.dispatchEvent(TournamentUpdated, request, updated.toJson())
}
return Json.Object("success" to true) return Json.Object("success" to true)
} }
internal fun validateTablesExclusion(exclusion: String) {
if (!tablesExclusionValidator.matches(exclusion)) badRequest("invalid tables exclusion pattern")
}
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json { override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
val tournament = getTournament(request) val tournament = getTournament(request)
getStore(request).deleteTournament(tournament) getStore(request).deleteTournament(tournament)
tournament.dispatchEvent(TournamentDeleted, request, Json.Object("id" to tournament.id)) tournament.dispatchEvent(TournamentDeleted, request, Json.Object("id" to tournament.id))
return Json.Object("success" to true) return Json.Object("success" to true)
} }
private val tablesExclusionValidator = Regex("^(?:(?:\\s+|,)*\\d+(?:-\\d+)?)*$")
} }

View File

@@ -2,6 +2,7 @@ package org.jeudego.pairgoth.ext
import jakarta.xml.bind.JAXBContext import jakarta.xml.bind.JAXBContext
import jakarta.xml.bind.JAXBElement import jakarta.xml.bind.JAXBElement
import org.apache.commons.text.StringEscapeUtils
import java.time.LocalDate import java.time.LocalDate
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.opengotha.TournamentType import org.jeudego.pairgoth.opengotha.TournamentType
@@ -35,7 +36,8 @@ object OpenGotha {
else -> throw Error("Invalid seed system: $str") else -> throw Error("Invalid seed system: $str")
} }
private fun String.titlecase(locale: Locale = Locale.ROOT) = lowercase(locale).replaceFirstChar { it.titlecase(locale) } private fun String.titleCase(locale: Locale = Locale.ROOT) = lowercase(locale).replaceFirstChar { it.titlecase(locale) }
private fun String.escapeXML() = StringEscapeUtils.escapeXml11(this)
private fun MainCritParams.SeedMethod.format() = toString().replace("_", "") private fun MainCritParams.SeedMethod.format() = toString().replace("_", "")
@@ -225,7 +227,7 @@ object OpenGotha {
player as Player player as Player
}.joinToString("\n") { player -> }.joinToString("\n") { player ->
"""<Player agaExpirationDate="" agaId="" club="${ """<Player agaExpirationDate="" agaId="" club="${
player.club player.club.escapeXML()
}" country="${ }" country="${
player.country player.country
}" egfPin="${ }" egfPin="${
@@ -233,11 +235,11 @@ object OpenGotha {
}" ffgLicence="${ }" ffgLicence="${
player.externalIds[DatabaseId.FFG] ?: "" player.externalIds[DatabaseId.FFG] ?: ""
}" ffgLicenceStatus="" firstName="${ }" ffgLicenceStatus="" firstName="${
player.firstname player.firstname.escapeXML()
}" grade="${ }" grade="${
player.displayRank() player.displayRank()
}" name="${ }" name="${
player.name player.name.escapeXML()
}" participating="${ }" participating="${
(1..20).map { (1..20).map {
if (player.skip.contains(it)) 0 else 1 if (player.skip.contains(it)) 0 else 1
@@ -255,7 +257,6 @@ object OpenGotha {
} }
</Players> </Players>
<Games> <Games>
// TODO - table number is not any more kinda random like this
${(1..tournament.lastRound()).map { tournament.games(it) }.flatMapIndexed { index, games -> ${(1..tournament.lastRound()).map { tournament.games(it) }.flatMapIndexed { index, games ->
games.values.mapNotNull { game -> games.values.mapNotNull { game ->
if (game.black == 0 || game.white == 0) null if (game.black == 0 || game.white == 0) null
@@ -264,9 +265,11 @@ object OpenGotha {
}.joinToString("\n") { (round, game) -> }.joinToString("\n") { (round, game) ->
"""<Game blackPlayer="${ """<Game blackPlayer="${
(tournament.pairables[game.black]!! as Player).let { black -> (tournament.pairables[game.black]!! as Player).let { black ->
"${black.name.replace(" ", "")}${black.firstname.replace(" ", "")}".uppercase(Locale.ENGLISH) // Use Locale.ENGLISH to transform é to É "${black.name.replace(" ", "")}${black.firstname.replace(" ", "")}".uppercase(Locale.ENGLISH).escapeXML() // Use Locale.ENGLISH to transform é to É
} }
}" handicap="0" knownColor="true" result="${ }" handicap="${
game.handicap
}" knownColor="true" result="${
when (game.result) { when (game.result) {
Game.Result.UNKNOWN, Game.Result.CANCELLED -> "RESULT_UNKNOWN" Game.Result.UNKNOWN, Game.Result.CANCELLED -> "RESULT_UNKNOWN"
Game.Result.BLACK -> "RESULT_BLACKWINS" Game.Result.BLACK -> "RESULT_BLACKWINS"
@@ -281,7 +284,7 @@ object OpenGotha {
game.table game.table
}" whitePlayer="${ }" whitePlayer="${
(tournament.pairables[game.white]!! as Player).let { white -> (tournament.pairables[game.white]!! as Player).let { white ->
"${white.name}${white.firstname}".uppercase(Locale.ENGLISH) // Use Locale.ENGLISH to transform é to É "${white.name.replace(" ", "")}${white.firstname.replace(" ", "")}".uppercase(Locale.ENGLISH).escapeXML() // Use Locale.ENGLISH to transform é to É
} }
}"/>""" }"/>"""
} }
@@ -304,12 +307,27 @@ object OpenGotha {
} }
</ByePlayer> </ByePlayer>
<TournamentParameterSet> <TournamentParameterSet>
<GeneralParameterSet bInternet="${tournament.online}" basicTime="${tournament.timeSystem.mainTime / 60}" beginDate="${tournament.startDate}" canByoYomiTime="${tournament.timeSystem.byoyomi}" complementaryTimeSystem="${when(tournament.timeSystem.type) { <GeneralParameterSet bInternet="${
tournament.online
}" basicTime="${
tournament.timeSystem.mainTime / 60
}" beginDate="${
tournament.startDate
}" canByoYomiTime="${
tournament.timeSystem.byoyomi
}" complementaryTimeSystem="${
when(tournament.timeSystem.type) {
TimeSystem.TimeSystemType.SUDDEN_DEATH -> "SUDDENDEATH" TimeSystem.TimeSystemType.SUDDEN_DEATH -> "SUDDENDEATH"
TimeSystem.TimeSystemType.JAPANESE -> "STDBYOYOMI" TimeSystem.TimeSystemType.JAPANESE -> "STDBYOYOMI"
TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI" TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI"
TimeSystem.TimeSystemType.FISCHER -> "FISCHER" TimeSystem.TimeSystemType.FISCHER -> "FISCHER"
} }" director="${tournament.director}" endDate="${tournament.endDate}" fischerTime="${tournament.timeSystem.increment}" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="${ } }" director="${
tournament.director.escapeXML()
}" endDate="${
tournament.endDate
}" fischerTime="${
tournament.timeSystem.increment
}" genCountNotPlayedGamesAsHalfPoint="false" genMMBar="${
displayRank( displayRank(
if (tournament.pairing is MacMahon) tournament.pairing.mmBar else 8 if (tournament.pairing is MacMahon) tournament.pairing.mmBar else 8
).uppercase(Locale.ROOT) ).uppercase(Locale.ROOT)
@@ -321,18 +339,94 @@ object OpenGotha {
(tournament.pairing.pairingParams.main.mmsValueAbsent * 2).roundToInt() (tournament.pairing.pairingParams.main.mmsValueAbsent * 2).roundToInt()
}" genMMS2ValueBye="2" genMMZero="30K" genNBW2ValueAbsent="0" genNBW2ValueBye="2" genRoundDownNBWMMS="${ }" genMMS2ValueBye="2" genMMZero="30K" genNBW2ValueAbsent="0" genNBW2ValueBye="2" genRoundDownNBWMMS="${
tournament.pairing.pairingParams.main.roundDownScore tournament.pairing.pairingParams.main.roundDownScore
}" komi="${tournament.komi}" location="${tournament.location}" name="${tournament.name}" nbMovesCanTime="${tournament.timeSystem.stones}" numberOfCategories="1" numberOfRounds="${tournament.rounds}" shortName="${tournament.shortName}" size="${tournament.gobanSize}" stdByoYomiTime="${tournament.timeSystem.byoyomi}"/> }" komi="${
<HandicapParameterSet hdBasedOnMMS="${tournament.pairing.pairingParams.handicap.useMMS}" hdCeiling="${tournament.pairing.pairingParams.handicap.ceiling}" hdCorrection="${tournament.pairing.pairingParams.handicap.correction}" hdNoHdRankThreshold="${displayRank(tournament.pairing.pairingParams.handicap.rankThreshold)}"/> tournament.komi
}" location="${
tournament.location.escapeXML()
}" name="${
tournament.name.escapeXML()
}" nbMovesCanTime="${
tournament.timeSystem.stones
}" numberOfCategories="1" numberOfRounds="${
tournament.rounds
}" shortName="${
tournament.shortName
}" size="${
tournament.gobanSize
}" stdByoYomiTime="${
tournament.timeSystem.byoyomi
}"/>
<HandicapParameterSet hdBasedOnMMS="${
tournament.pairing.pairingParams.handicap.useMMS
}" hdCeiling="${
tournament.pairing.pairingParams.handicap.ceiling
}" hdCorrection="${
tournament.pairing.pairingParams.handicap.correction
}" hdNoHdRankThreshold="${
displayRank(tournament.pairing.pairingParams.handicap.rankThreshold)
}"/>
<PlacementParameterSet> <PlacementParameterSet>
<PlacementCriteria> <PlacementCriteria>
${ ${
(0..5).map { (0..5).map {
"""<PlacementCriterion name="${tournament.pairing.placementParams.criteria.getOrNull(it)?.name ?: "NULL"}" number="${it + 1}"/>""" """<PlacementCriterion name="${
tournament.pairing.placementParams.criteria.getOrNull(it)?.name ?: "NULL"
}" number="${it + 1}"/>"""
} }
} }
</PlacementCriteria> </PlacementCriteria>
</PlacementParameterSet> </PlacementParameterSet>
<PairingParameterSet paiBaAvoidDuplGame="${tournament.pairing.pairingParams.base.dupWeight.toLong()}" paiBaBalanceWB="${tournament.pairing.pairingParams.base.colorBalanceWeight.toLong()}" paiBaDeterministic="${tournament.pairing.pairingParams.base.deterministic}" paiBaRandom="${tournament.pairing.pairingParams.base.random.toLong()}" paiMaAdditionalPlacementCritSystem1="${tournament.pairing.pairingParams.main.additionalPlacementCritSystem1.toString().titlecase()}" paiMaAdditionalPlacementCritSystem2="${tournament.pairing.pairingParams.main.additionalPlacementCritSystem2.toString().titlecase()}" paiMaAvoidMixingCategories="${tournament.pairing.pairingParams.main.categoriesWeight.toLong()}" paiMaCompensateDUDD="${tournament.pairing.pairingParams.main.compensateDrawUpDown}" paiMaDUDDLowerMode="${tournament.pairing.pairingParams.main.drawUpDownLowerMode.toString().substring(0, 3)}" paiMaDUDDUpperMode="${tournament.pairing.pairingParams.main.drawUpDownUpperMode.toString().substring(0, 3)}" paiMaDUDDWeight="${tournament.pairing.pairingParams.main.drawUpDownWeight.toLong()}" paiMaLastRoundForSeedSystem1="${tournament.pairing.pairingParams.main.lastRoundForSeedSystem1}" paiMaMaximizeSeeding="${tournament.pairing.pairingParams.main.seedingWeight.toLong()}" paiMaMinimizeScoreDifference="${tournament.pairing.pairingParams.main.scoreWeight.toLong()}" paiMaSeedSystem1="${tournament.pairing.pairingParams.main.seedSystem1.format()}" paiMaSeedSystem2="${tournament.pairing.pairingParams.main.seedSystem2.format()}" paiSeAvoidSameGeo="${tournament.pairing.pairingParams.geo.avoidSameGeo.toLong()}" paiSeBarThresholdActive="${tournament.pairing.pairingParams.secondary.barThresholdActive}" paiSeDefSecCrit="${tournament.pairing.pairingParams.secondary.defSecCrit.toLong()}" paiSeMinimizeHandicap="${tournament.pairing.pairingParams.handicap.weight.toLong()}" paiSeNbWinsThresholdActive="${tournament.pairing.pairingParams.secondary.nbWinsThresholdActive}" paiSePreferMMSDiffRatherThanSameClub="${tournament.pairing.pairingParams.geo.preferMMSDiffRatherThanSameClub}" paiSePreferMMSDiffRatherThanSameCountry="${tournament.pairing.pairingParams.geo.preferMMSDiffRatherThanSameCountry}" paiSeRankThreshold="${displayRank(tournament.pairing.pairingParams.secondary.rankSecThreshold).uppercase()}" paiStandardNX1Factor="${tournament.pairing.pairingParams.base.nx1}"/> <PairingParameterSet paiBaAvoidDuplGame="${
tournament.pairing.pairingParams.base.dupWeight.toLong()
}" paiBaBalanceWB="${
tournament.pairing.pairingParams.base.colorBalanceWeight.toLong()
}" paiBaDeterministic="${
tournament.pairing.pairingParams.base.deterministic
}" paiBaRandom="${
tournament.pairing.pairingParams.base.random.toLong()
}" paiMaAdditionalPlacementCritSystem1="${
tournament.pairing.pairingParams.main.additionalPlacementCritSystem1.toString().titleCase()
}" paiMaAdditionalPlacementCritSystem2="${
tournament.pairing.pairingParams.main.additionalPlacementCritSystem2.toString().titleCase()
}" paiMaAvoidMixingCategories="${
tournament.pairing.pairingParams.main.categoriesWeight.toLong()
}" paiMaCompensateDUDD="${
tournament.pairing.pairingParams.main.compensateDrawUpDown
}" paiMaDUDDLowerMode="${
tournament.pairing.pairingParams.main.drawUpDownLowerMode.toString().substring(0, 3)
}" paiMaDUDDUpperMode="${
tournament.pairing.pairingParams.main.drawUpDownUpperMode.toString().substring(0, 3)
}" paiMaDUDDWeight="${
tournament.pairing.pairingParams.main.drawUpDownWeight.toLong()
}" paiMaLastRoundForSeedSystem1="${
tournament.pairing.pairingParams.main.lastRoundForSeedSystem1
}" paiMaMaximizeSeeding="${
tournament.pairing.pairingParams.main.seedingWeight.toLong()
}" paiMaMinimizeScoreDifference="${
tournament.pairing.pairingParams.main.scoreWeight.toLong()
}" paiMaSeedSystem1="${
tournament.pairing.pairingParams.main.seedSystem1.format()
}" paiMaSeedSystem2="${
tournament.pairing.pairingParams.main.seedSystem2.format()
}" paiSeAvoidSameGeo="${
tournament.pairing.pairingParams.geo.avoidSameGeo.toLong()
}" paiSeBarThresholdActive="${
tournament.pairing.pairingParams.secondary.barThresholdActive
}" paiSeDefSecCrit="${
tournament.pairing.pairingParams.secondary.defSecCrit.toLong()
}" paiSeMinimizeHandicap="${
tournament.pairing.pairingParams.handicap.weight.toLong()
}" paiSeNbWinsThresholdActive="${
tournament.pairing.pairingParams.secondary.nbWinsThresholdActive
}" paiSePreferMMSDiffRatherThanSameClub="${
tournament.pairing.pairingParams.geo.preferMMSDiffRatherThanSameClub
}" paiSePreferMMSDiffRatherThanSameCountry="${
tournament.pairing.pairingParams.geo.preferMMSDiffRatherThanSameCountry
}" paiSeRankThreshold="${
displayRank(tournament.pairing.pairingParams.secondary.rankSecThreshold).uppercase()
}" paiStandardNX1Factor="${
tournament.pairing.pairingParams.base.nx1
}"/>
<DPParameterSet displayClCol="true" displayCoCol="true" displayIndGamesInMatches="true" displayNPPlayers="false" displayNumCol="true" displayPlCol="true" gameFormat="short" playerSortType="name" showByePlayer="true" showNotFinallyRegisteredPlayers="true" showNotPairedPlayers="true" showNotParticipatingPlayers="false" showPlayerClub="true" showPlayerCountry="false" showPlayerGrade="true"/> <DPParameterSet displayClCol="true" displayCoCol="true" displayIndGamesInMatches="true" displayNPPlayers="false" displayNumCol="true" displayPlCol="true" gameFormat="short" playerSortType="name" showByePlayer="true" showNotFinallyRegisteredPlayers="true" showNotPairedPlayers="true" showNotParticipatingPlayers="false" showPlayerClub="true" showPlayerCountry="false" showPlayerGrade="true"/>
<PublishParameterSet exportToLocalFile="true" htmlAutoScroll="false" print="false"/> <PublishParameterSet exportToLocalFile="true" htmlAutoScroll="false" print="false"/>
</TournamentParameterSet> </TournamentParameterSet>

View File

@@ -11,8 +11,10 @@ data class Game(
var black: ID, var black: ID,
var handicap: Int = 0, var handicap: Int = 0,
var result: Result = UNKNOWN, var result: Result = UNKNOWN,
var drawnUpDown: Int = 0 // counted for white (black gets the opposite) var drawnUpDown: Int = 0, // counted for white (black gets the opposite)
var forcedTable: Boolean = false
) { ) {
companion object {} companion object {}
enum class Result(val symbol: Char) { enum class Result(val symbol: Char) {
UNKNOWN('?'), UNKNOWN('?'),
@@ -36,15 +38,17 @@ data class Game(
// serialization // serialization
fun Game.toJson() = Json.Object( fun Game.toJson() = Json.MutableObject(
"id" to id, "id" to id,
"t" to table, "t" to table,
"w" to white, "w" to white,
"b" to black, "b" to black,
"h" to handicap, "h" to handicap,
"r" to "${result.symbol}", "r" to "${result.symbol}"
"dd" to drawnUpDown ).also { game ->
) if (drawnUpDown != 0) game["dd"] = drawnUpDown
if (forcedTable) game["ft"] = true
}
fun Game.Companion.fromJson(json: Json.Object) = Game( fun Game.Companion.fromJson(json: Json.Object) = Game(
id = json.getID("id") ?: throw Error("missing game id"), id = json.getID("id") ?: throw Error("missing game id"),
@@ -53,5 +57,6 @@ fun Game.Companion.fromJson(json: Json.Object) = Game(
black = json.getID("b") ?: throw Error("missing black player"), black = json.getID("b") ?: throw Error("missing black player"),
handicap = json.getInt("h") ?: 0, handicap = json.getInt("h") ?: 0,
result = json.getChar("r")?.let { Game.Result.fromSymbol(it) } ?: UNKNOWN, result = json.getChar("r")?.let { Game.Result.fromSymbol(it) } ?: UNKNOWN,
drawnUpDown = json.getInt("dd") ?: 0 drawnUpDown = json.getInt("dd") ?: 0,
forcedTable = json.getBoolean("ft") ?: false
) )

View File

@@ -12,6 +12,7 @@ enum class Criterion {
MMS, // Macmahon score MMS, // Macmahon score
STS, // Strasbourg score STS, // Strasbourg score
CPS, // Cup score CPS, // Cup score
SCOREX, // CB TODO - I'm adding this one for the congress, didn't find its name in OG after a quick check, needs a deeper investigation
SOSW, // Sum of opponents NBW SOSW, // Sum of opponents NBW
SOSWM1, //-1 SOSWM1, //-1

View File

@@ -6,12 +6,12 @@ import com.republicate.kson.toJsonArray
//import kotlinx.datetime.LocalDate //import kotlinx.datetime.LocalDate
import java.time.LocalDate import java.time.LocalDate
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver import org.jeudego.pairgoth.api.ApiHandler.Companion.logger
import org.jeudego.pairgoth.pairing.solver.SwissSolver
import org.jeudego.pairgoth.store.nextPlayerId import org.jeudego.pairgoth.store.nextPlayerId
import org.jeudego.pairgoth.store.nextTournamentId import org.jeudego.pairgoth.store.nextTournamentId
import kotlin.math.max import kotlin.math.max
import java.util.* import java.util.*
import java.util.regex.Pattern
import kotlin.math.roundToInt import kotlin.math.roundToInt
sealed class Tournament <P: Pairable>( sealed class Tournament <P: Pairable>(
@@ -30,7 +30,8 @@ sealed class Tournament <P: Pairable>(
val pairing: Pairing, val pairing: Pairing,
val rules: Rules = Rules.FRENCH, val rules: Rules = Rules.FRENCH,
val gobanSize: Int = 19, val gobanSize: Int = 19,
val komi: Double = 7.5 val komi: Double = 7.5,
val tablesExclusion: MutableList<String> = mutableListOf()
) { ) {
companion object {} companion object {}
enum class Type(val playersNumber: Int, val individual: Boolean = true) { enum class Type(val playersNumber: Int, val individual: Boolean = true) {
@@ -51,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.
@@ -113,11 +117,17 @@ sealed class Tournament <P: Pairable>(
} }
} }
fun usedTables(round: Int): BitSet = fun usedTables(round: Int): BitSet {
games(round).values.map { it.table }.fold(BitSet()) { acc, table -> val assigned = games(round).values.map { it.table }.fold(BitSet()) { acc, table ->
acc.set(table) acc.set(table)
acc acc
} }
val excluded = excludedTables(round)
for (table in excluded) {
assigned.set(table)
}
return assigned
}
private fun defaultGameOrderBy(game: Game): Int { private fun defaultGameOrderBy(game: Game): Int {
val whiteRank = pairables[game.white]?.rating ?: Int.MIN_VALUE val whiteRank = pairables[game.white]?.rating ?: Int.MIN_VALUE
@@ -128,9 +138,19 @@ sealed class Tournament <P: Pairable>(
fun renumberTables(round: Int, pivot: Game? = null, orderBY: (Game) -> Int = ::defaultGameOrderBy): Boolean { fun renumberTables(round: Int, pivot: Game? = null, orderBY: (Game) -> Int = ::defaultGameOrderBy): Boolean {
var changed = false var changed = false
var nextTable = 1 var nextTable = 1
games(round).values.filter{ game -> pivot?.let { pivot.id != game.id } ?: true }.sortedBy(orderBY).forEach { game -> val excluded = excludedTables(round)
val forcedTablesGames = games(round).values.filter { game -> game.forcedTable && (pivot == null || game != pivot && game.table != pivot.table) }
val forcedTables = forcedTablesGames.map { game -> game.table }.toSet()
val excludedAndForced = excluded union forcedTables
games(round).values
.filter { game -> pivot?.let { pivot.id != game.id } ?: true }
.filter { game -> !forcedTablesGames.contains(game) }
.sortedBy(orderBY)
.forEach { game ->
while (excludedAndForced.contains(nextTable)) ++nextTable
if (pivot != null && nextTable == pivot.table) { if (pivot != null && nextTable == pivot.table) {
++nextTable ++nextTable
while (excludedAndForced.contains(nextTable)) ++nextTable
} }
if (game.table != 0) { if (game.table != 0) {
changed = changed || game.table != nextTable changed = changed || game.table != nextTable
@@ -151,6 +171,22 @@ sealed class Tournament <P: Pairable>(
"ready" to (games.getOrNull(index)?.values?.count { it.result != Game.Result.UNKNOWN } ?: 0) "ready" to (games.getOrNull(index)?.values?.count { it.result != Game.Result.UNKNOWN } ?: 0)
) )
}.toJsonArray() }.toJsonArray()
fun excludedTables(round: Int): Set<Int> {
if (round > tablesExclusion.size) return emptySet()
val excluded = mutableSetOf<Int>()
val parser = Regex("(\\d+)(?:-(\\d+))?")
parser.findAll(tablesExclusion[round - 1]).forEach { match ->
val left = match.groupValues[1].toInt()
val right = match.groupValues[2].let { if (it.isEmpty()) left else it.toInt() }
var t = left
do {
excluded.add(t)
++t
} while (t <= right)
}
return excluded
}
} }
// standard tournament of individuals // standard tournament of individuals
@@ -170,8 +206,9 @@ class StandardTournament(
pairing: Pairing, pairing: Pairing,
rules: Rules = Rules.FRENCH, rules: Rules = Rules.FRENCH,
gobanSize: Int = 19, gobanSize: Int = 19,
komi: Double = 7.5 komi: Double = 7.5,
): Tournament<Player>(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { tablesExclusion: MutableList<String> = mutableListOf()
): Tournament<Player>(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi, tablesExclusion) {
override val players get() = _pairables override val players get() = _pairables
} }
@@ -192,8 +229,9 @@ class TeamTournament(
pairing: Pairing, pairing: Pairing,
rules: Rules = Rules.FRENCH, rules: Rules = Rules.FRENCH,
gobanSize: Int = 19, gobanSize: Int = 19,
komi: Double = 7.5 komi: Double = 7.5,
): Tournament<TeamTournament.Team>(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { tablesExclusion: MutableList<String> = mutableListOf()
): Tournament<TeamTournament.Team>(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi, tablesExclusion) {
companion object { companion object {
private val epsilon = 0.0001 private val epsilon = 0.0001
} }
@@ -267,7 +305,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19,
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"),
pairing = json.getObject("pairing")?.let { Pairing.fromJson(it, default?.pairing) } ?: default?.pairing ?: badRequest("missing pairing") pairing = json.getObject("pairing")?.let { Pairing.fromJson(it, default?.pairing) } ?: default?.pairing ?: badRequest("missing pairing"),
tablesExclusion = json.getArray("tablesExclusion")?.map { item -> item as String }?.toMutableList() ?: default?.tablesExclusion ?: mutableListOf()
) )
else else
TeamTournament( TeamTournament(
@@ -286,7 +325,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19, gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19,
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"), timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"), rounds = json.getInt("rounds") ?: default?.rounds ?: badRequest("missing rounds"),
pairing = json.getObject("pairing")?.let { Pairing.fromJson(it, default?.pairing) } ?: default?.pairing ?: badRequest("missing pairing") pairing = json.getObject("pairing")?.let { Pairing.fromJson(it, default?.pairing) } ?: default?.pairing ?: badRequest("missing pairing"),
tablesExclusion = json.getArray("tablesExclusion")?.map { item -> item as String }?.toMutableList() ?: default?.tablesExclusion ?: mutableListOf()
) )
json.getArray("players")?.forEach { obj -> json.getArray("players")?.forEach { obj ->
val pairable = obj as Json.Object val pairable = obj as Json.Object
@@ -298,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
@@ -307,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
} }
@@ -327,7 +370,14 @@ fun Tournament<*>.toJson() = Json.MutableObject(
"timeSystem" to timeSystem.toJson(), "timeSystem" to timeSystem.toJson(),
"rounds" to rounds, "rounds" to rounds,
"pairing" to pairing.toJson() "pairing" to pairing.toJson()
) ).also { tour ->
if (tablesExclusion.isNotEmpty()) {
tour["tablesExclusion"] = tablesExclusion.toJsonArray()
}
if (frozen != null) {
tour["frozen"] = frozen
}
}
fun Tournament<*>.toFullJson(): Json.Object { fun Tournament<*>.toFullJson(): Json.Object {
val json = toJson() val json = toJson()
@@ -336,5 +386,11 @@ 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 (tablesExclusion.isNotEmpty()) {
json["tablesExclusion"] = tablesExclusion.toJsonArray()
}
if (frozen != null) {
json["frozen"] = frozen
}
return json return json
} }

View File

@@ -13,6 +13,7 @@ abstract class BasePairingHelper(
) { ) {
abstract val scores: Map<ID, Pair<Double, Double>> abstract val scores: Map<ID, Pair<Double, Double>>
abstract val scoresX: Map<ID, Double>
val historyHelper = val historyHelper =
if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper( if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(
history history
@@ -47,7 +48,7 @@ abstract class BasePairingHelper(
// Decide each pairable group based on the main criterion // Decide each pairable group based on the main criterion
protected val groupsCount get() = 1 + (mainLimits.second - mainLimits.first).toInt() protected val groupsCount get() = 1 + (mainLimits.second - mainLimits.first).toInt()
private val _groups by lazy { private val _groups by lazy {
pairables.associate { pairable -> Pair(pairable.id, pairable.main.toInt()) } pairables.associate { pairable -> Pair(pairable.id, (pairable.main * 2).toInt() / 2) }
} }
// place (among sorted pairables) // place (among sorted pairables)

View File

@@ -3,7 +3,10 @@ package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.* import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.model.Game.Result.* import org.jeudego.pairgoth.model.Game.Result.*
open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter: HistoryHelper.()-> Map<ID, Pair<Double, Double>>) { open class HistoryHelper(
protected val history: List<List<Game>>,
// scoresGetter() returns Pair(absentSosValueForOthers, score) where score is nbw for Swiss, mms for MM, ...
scoresGetter: HistoryHelper.()-> Map<ID, Pair<Double, Double>>) {
// List of all the pairables ID present in the history // List of all the pairables ID present in the history
val allPairables = history.flatten() val allPairables = history.flatten()
@@ -24,6 +27,12 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
scoresGetter() scoresGetter()
} }
val scoresX by lazy {
scoresGetter().mapValues { entry ->
entry.value.first + (wins[entry.key] ?: 0.0)
}
}
// Generic helper functions // Generic helper functions
open fun playedTogether(p1: Pairable, p2: Pairable) = paired.contains(Pair(p1.id, p2.id)) open fun playedTogether(p1: Pairable, p2: Pairable) = paired.contains(Pair(p1.id, p2.id))
open fun colorBalance(p: Pairable) = colorBalance[p.id] open fun colorBalance(p: Pairable) = colorBalance[p.id]

View File

@@ -480,14 +480,15 @@ sealed class BaseSolver(
val epsilon = 0.00001 val epsilon = 0.00001
// Note: this works for now because we only have .0 and .5 fractional parts // Note: this works for now because we only have .0 and .5 fractional parts
return if (pairing.main.roundDownScore) floor(score + epsilon) return if (pairing.main.roundDownScore) floor(score + epsilon)
else ceil(score - epsilon) else round(2 * score) / 2
} }
open fun HandicapParams.clamp(input: Int): Int { open fun HandicapParams.clamp(input: Int): Int {
var hd = input var hd = input
// TODO - validate that "correction" is >= 0 (or modify the UI and the following code to handle the <0 case) // TODO - validate that "correction" is >= 0 (or modify the UI and the following code to handle the <0 case)
if (hd >= correction) hd -= correction if (hd >= correction) hd -= correction
else if (hd < 0) hd = max(hd + correction, 0) // TODO - Following line seems buggy... Get rid of it! What as the purpose?!
// else if (hd < 0) hd = max(hd + correction, 0)
else hd = 0 else hd = 0
// Clamp handicap with ceiling // Clamp handicap with ceiling
hd = min(hd, ceiling) hd = min(hd, ceiling)

View File

@@ -33,6 +33,15 @@ class MacMahonSolver(round: Int,
} }
} }
override val scoresX: Map<ID, Double> by lazy {
require (mmBar > mmFloor) { "MMFloor is higher than MMBar" }
pairablesMap.mapValues {
it.value.let { pairable ->
roundScore(pairable.mmBase + pairable.nbW)
}
}
}
override fun computeWeightForBye(p: Pairable): Double{ override fun computeWeightForBye(p: Pairable): Double{
return 2*scores[p.id]!!.second return 2*scores[p.id]!!.second
} }
@@ -76,6 +85,7 @@ class MacMahonSolver(round: Int,
val Pairable.mmBase: Double get() = min(max(rank, mmFloor), mmBar) + mmsZero + mmsCorrection val Pairable.mmBase: Double get() = min(max(rank, mmFloor), mmBar) + mmsZero + mmsCorrection
// mms: current Mac-Mahon score of the pairable // mms: current Mac-Mahon score of the pairable
val Pairable.mms: Double get() = scores[id]?.second ?: 0.0 val Pairable.mms: Double get() = scores[id]?.second ?: 0.0
val Pairable.scoreX: Double get() = scoresX[id] ?: 0.0
// CB TODO - configurable criteria // CB TODO - configurable criteria
val mainScoreMin = mmFloor + PLA_SMMS_SCORE_MIN - Pairable.MIN_RANK val mainScoreMin = mmFloor + PLA_SMMS_SCORE_MIN - Pairable.MIN_RANK
@@ -83,6 +93,7 @@ class MacMahonSolver(round: Int,
override val mainLimits get() = Pair(mainScoreMin.toDouble(), mainScoreMax.toDouble()) override val mainLimits get() = Pair(mainScoreMin.toDouble(), mainScoreMax.toDouble())
override fun evalCriterion(pairable: Pairable, criterion: Criterion) = when (criterion) { override fun evalCriterion(pairable: Pairable, criterion: Criterion) = when (criterion) {
Criterion.MMS -> pairable.mms Criterion.MMS -> pairable.mms
Criterion.SCOREX -> pairable.scoreX
Criterion.SOSM -> pairable.sos Criterion.SOSM -> pairable.sos
Criterion.SOSOSM -> pairable.sosos Criterion.SOSOSM -> pairable.sosos
Criterion.SOSMM1 -> pairable.sosm1 Criterion.SOSMM1 -> pairable.sosm1

View File

@@ -20,8 +20,7 @@ class SwissSolver(round: Int,
historyHelper.wins.mapValues { historyHelper.wins.mapValues {
Pair(0.0, it.value) } Pair(0.0, it.value) }
} }
// override val scoresX: Map<ID, Double> get() = scores.mapValues { it.value.second }
// get() by lazy { historyHelper.wins }
override val mainLimits = Pair(0.0, round - 1.0) override val mainLimits = Pair(0.0, round - 1.0)
} }

View File

@@ -235,7 +235,7 @@ class ApiServlet: HttpServlet() {
// 2) there will be other content types: .tou, .h9, .html // 2) there will be other content types: .tou, .h9, .html
if (!isJson(accept) && if (!isJson(accept) &&
(!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+"))) && (!isXml(accept) || !request.requestURI.matches(Regex("/api/tour/\\d+"))) &&
(!accept.startsWith("application/ffg") && !accept.startsWith("application/egf") && !accept.startsWith("text/csv") || !request.requestURI.matches(Regex("/api/tour/\\d+/standings/\\d+"))) (!accept.startsWith("application/ffg") && !accept.startsWith("application/egf") && !accept.startsWith("text/csv") || !request.requestURI.matches(Regex("/api/tour/\\d+/standings(?:/\\d+)?")))
) throw ApiException( ) throw ApiException(
HttpServletResponse.SC_BAD_REQUEST, HttpServletResponse.SC_BAD_REQUEST,

View File

@@ -139,7 +139,11 @@ class FileStore(pathStr: String): Store {
entry.toFile() entry.toFile()
}.firstOrNull() }.firstOrNull()
}?.let { file -> }?.let { file ->
val dest = path.resolve(filename + "-${timestamp}").toFile() val history = path.resolve("history").toFile()
if (!history.exists() && !history.mkdir()) {
throw Error("cannot create 'history' sub-directory")
}
val dest = path.resolve("history/${filename}-${timestamp}").toFile()
if (dest.exists()) { if (dest.exists()) {
// it means the user performed several actions in the same second... // it means the user performed several actions in the same second...
// drop the last occurrence // drop the last occurrence
@@ -157,6 +161,10 @@ class FileStore(pathStr: String): Store {
val filename = tournament.filename() val filename = tournament.filename()
val file = path.resolve(filename).toFile() val file = path.resolve(filename).toFile()
if (!file.exists()) throw Error("File $filename does not exist") if (!file.exists()) throw Error("File $filename does not exist")
file.renameTo(path.resolve(filename + "-${timestamp}").toFile()) val history = path.resolve("history").toFile()
if (!history.exists() && !history.mkdir()) {
throw Error("cannot create 'history' sub-directory")
}
file.renameTo(path.resolve("history/${filename}-${timestamp}").toFile())
} }
} }

View File

@@ -162,7 +162,7 @@ class BasicTests: TestBase() {
assertEquals(aTournamentID, resp.getInt("id"), "First tournament should have id #$aTournamentID") assertEquals(aTournamentID, resp.getInt("id"), "First tournament should have id #$aTournamentID")
// filter out "id", and also "komi", "rules" and "gobanSize" which were provided by default // filter out "id", and also "komi", "rules" and "gobanSize" which were provided by default
// also filter out "pairing", which is filled by all default values // also filter out "pairing", which is filled by all default values
val cmp = Json.Object(*resp.entries.filter { it.key !in listOf("id", "komi", "rules", "gobanSize", "pairing") }.map { Pair(it.key, it.value) }.toTypedArray()) val cmp = Json.Object(*resp.entries.filter { it.key !in listOf("id", "komi", "rules", "gobanSize", "pairing", "frozen") }.map { Pair(it.key, it.value) }.toTypedArray())
val expected = aTournament.entries.filter { it.key != "pairing" }.map { Pair(it.key, it.value) }.toMap().toMutableJsonObject().also { map -> val expected = aTournament.entries.filter { it.key != "pairing" }.map { Pair(it.key, it.value) }.toMap().toMutableJsonObject().also { map ->
map["stats"] = Json.Array( map["stats"] = Json.Array(
Json.Object("participants" to 0, "paired" to 0, "games" to 0, "ready" to 0), Json.Object("participants" to 0, "paired" to 0, "games" to 0, "ready" to 0),
@@ -170,7 +170,7 @@ class BasicTests: TestBase() {
) )
map["teamSize"] = 1 map["teamSize"] = 1
} }
assertEquals(expected.toString(), cmp.toString(), "tournament differs") assertEquals(expected.entries.sortedBy { it.key }.map { Pair(it.key, it.value) }.toJsonObject().toString(), cmp.entries.sortedBy { it.key }.map { Pair(it.key, it.value) }.toJsonObject().toString(), "tournament differs")
} }
@Test @Test
@@ -203,8 +203,8 @@ class BasicTests: TestBase() {
var games = TestAPI.post("/api/tour/$aTournamentID/pair/1", Json.Array("all")).asArray() var games = TestAPI.post("/api/tour/$aTournamentID/pair/1", Json.Array("all")).asArray()
aTournamentGameID = (games[0] as Json.Object).getInt("id") aTournamentGameID = (games[0] as Json.Object).getInt("id")
val possibleResults = setOf( val possibleResults = setOf(
"""[{"id":$aTournamentGameID,"t":1,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"?","dd":0}]""", """[{"id":$aTournamentGameID,"t":1,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"?"}]""",
"""[{"id":$aTournamentGameID,"t":1,"w":$anotherPlayerID,"b":$aPlayerID,"h":0,"r":"?","dd":0}]""" """[{"id":$aTournamentGameID,"t":1,"w":$anotherPlayerID,"b":$aPlayerID,"h":0,"r":"?"}]"""
) )
assertTrue(possibleResults.contains(games.toString()), "pairing differs") assertTrue(possibleResults.contains(games.toString()), "pairing differs")
games = TestAPI.get("/api/tour/$aTournamentID/res/1").asArray()!! games = TestAPI.get("/api/tour/$aTournamentID/res/1").asArray()!!
@@ -219,8 +219,8 @@ class BasicTests: TestBase() {
assertTrue(resp.getBoolean("success") == true, "expecting success") assertTrue(resp.getBoolean("success") == true, "expecting success")
val games = TestAPI.get("/api/tour/$aTournamentID/res/1") val games = TestAPI.get("/api/tour/$aTournamentID/res/1")
val possibleResults = setOf( val possibleResults = setOf(
"""[{"id":$aTournamentGameID,"t":1,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"b","dd":0}]""", """[{"id":$aTournamentGameID,"t":1,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"b"}]""",
"""[{"id":$aTournamentGameID,"t":1,"w":$anotherPlayerID,"b":$aPlayerID,"h":0,"r":"b","dd":0}]""" """[{"id":$aTournamentGameID,"t":1,"w":$anotherPlayerID,"b":$aPlayerID,"h":0,"r":"b"}]"""
) )
assertTrue(possibleResults.contains(games.toString()), "results differ") assertTrue(possibleResults.contains(games.toString()), "results differ")
} }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.jeudego.pairgoth</groupId> <groupId>org.jeudego.pairgoth</groupId>
<artifactId>engine-parent</artifactId> <artifactId>engine-parent</artifactId>
<version>0.14</version> <version>0.15</version>
</parent> </parent>
<artifactId>application</artifactId> <artifactId>application</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>

View File

@@ -26,13 +26,19 @@ Authentication: `none`, `sesame` for a shared unique password, `oauth` for email
auth = none auth = none
``` ```
When running in client or server mode, if `auth` is not `none`, the following extra property is needed:
```
auth.shared_secret = <16 ascii characters string>
```
## webapp connector ## webapp connector
Pairgoth webapp connector configuration. Pairgoth webapp connector configuration.
``` ```
webapp.protocol = http webapp.protocol = http
webapp.interface = localhost webapp.host = localhost
webapp.port = 8080 webapp.port = 8080
webapp.context = / webapp.context = /
webapp.external.url = http://localhost:8080 webapp.external.url = http://localhost:8080
@@ -44,7 +50,7 @@ Pairgoth API connector configuration.
``` ```
api.protocol = http api.protocol = http
api.interface = localhost api.host = localhost
api.port = 8085 api.port = 8085
api.context = /api api.context = /api
api.external.url = http://localhost:8085/api api.external.url = http://localhost:8085/api
@@ -79,3 +85,35 @@ Logging configuration.
logger.level = info logger.level = info
logger.format = [%level] %ip [%logger] %message logger.format = [%level] %ip [%logger] %message
``` ```
## ratings
Ratings configuration. `<ratings>` stands for `egf` or `ffg` in the following.
### freeze ratings date
If the following property is given:
```
ratings.<ratings>.file = ...
```
then the given ratings file will be used (it must use the Pairgoth ratings json format). If not, the corresponding ratings will be automatically downloaded and stored into `ratings/EGF-yyyymmdd.json` or `ratings/FFG-yyyymmdd.json`.
The typical use case, for a big tournament lasting several days or a congress, is to let Pairgoth download the latest expected ratings, then to add this property to freeze the ratings at a specific date.
### enable or disable ratings
Whether to display the EGF or FFG ratings button in the Add Player popup:
```
ratings.<ratings>.enable = true | false
```
Whether to show the ratings player IDs on the registration page:
```
ratings.<ratings>.show = true | false
```
For a tournament in France, both are true for `ffg` by default, false otherwise.

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.jeudego.pairgoth</groupId> <groupId>org.jeudego.pairgoth</groupId>
<artifactId>engine-parent</artifactId> <artifactId>engine-parent</artifactId>
<version>0.14</version> <version>0.15</version>
</parent> </parent>
<artifactId>pairgoth-common</artifactId> <artifactId>pairgoth-common</artifactId>

View File

@@ -5,7 +5,7 @@
<groupId>org.jeudego.pairgoth</groupId> <groupId>org.jeudego.pairgoth</groupId>
<artifactId>engine-parent</artifactId> <artifactId>engine-parent</artifactId>
<version>0.14</version> <version>0.15</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<!-- CB: Temporary add my repository, while waiting for SSE Java server module author to incorporate my PR or for me to fork it --> <!-- CB: Temporary add my repository, while waiting for SSE Java server module author to incorporate my PR or for me to fork it -->

View File

@@ -7,7 +7,7 @@
<parent> <parent>
<groupId>org.jeudego.pairgoth</groupId> <groupId>org.jeudego.pairgoth</groupId>
<artifactId>engine-parent</artifactId> <artifactId>engine-parent</artifactId>
<version>0.14</version> <version>0.15</version>
</parent> </parent>
<artifactId>view-webapp</artifactId> <artifactId>view-webapp</artifactId>

View File

@@ -2,6 +2,7 @@ package org.jeudego.pairgoth.view
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.ratings.RatingsManager import org.jeudego.pairgoth.ratings.RatingsManager
import org.jeudego.pairgoth.web.WebappManager
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
@@ -26,6 +27,7 @@ class PairgothTool {
"RATING" to "Rating", "RATING" to "Rating",
"NBW" to "Number of wins", // Number win "NBW" to "Number of wins", // Number win
"MMS" to "Mac Mahon score", // Macmahon score "MMS" to "Mac Mahon score", // Macmahon score
"SCOREX" to "Score X", // Score X
// TODO "STS" to "Strasbourg score", // Strasbourg score // TODO "STS" to "Strasbourg score", // Strasbourg score
// TODO "CPS" to "Cup score", // Cup score // TODO "CPS" to "Cup score", // Cup score
@@ -74,6 +76,11 @@ class PairgothTool {
} }
} }
fun getMmsPlayersMap(pairables: Collection<Json.Object>) =
pairables.associate { part ->
Pair(part.getLong("id"), part.getDouble("MMS")?.toLong())
}
fun removeBye(games: Collection<Json.Object>) = fun removeBye(games: Collection<Json.Object>) =
games.filter { games.filter {
it.getInt("b")!! != 0 && it.getInt("w")!! != 0 it.getInt("b")!! != 0 && it.getInt("w")!! != 0
@@ -104,4 +111,8 @@ class PairgothTool {
}.toSet() }.toSet()
return players.filter { p -> !teamed.contains(p.getLong("id")) } return players.filter { p -> !teamed.contains(p.getLong("id")) }
} }
// EGF ratings
fun displayRatings(ratings: String, country: String): Boolean = WebappManager.properties.getProperty("ratings.${ratings}.enable")?.toBoolean() ?: (ratings.lowercase() != "ffg") || country.lowercase() == "fr"
fun showRatings(ratings: String, country: String): Boolean = WebappManager.properties.getProperty("ratings.${ratings}.enable")?.toBoolean() ?: (ratings.lowercase() != "ffg") || country.lowercase() == "fr"
} }

View File

@@ -281,8 +281,13 @@
vertical-align: baseline; vertical-align: baseline;
} }
.ui.striped.table>tbody>tr:nth-child(2n),.ui.striped.table>tr:nth-child(2n) { .ui.striped.table > tbody > tr:nth-child(2n), .ui.striped.table > tr:nth-child(2n) {
background-color: rgba(0,0,50,.1) background-color: inherit;
//background-color: rgba(0,0,50,.1)
}
.ui.striped.table > tbody > tr:nth-child(2n of :not(.filtered)), .ui.striped.table > tr:nth-child(2n of :not(.filtered)) {
background-color: rgba(0, 0, 50, 0.1);
} }
.form-actions { .form-actions {
@@ -303,7 +308,7 @@
} }
.hidden { .hidden {
display: none; display: none !important;
} }
.roundbox { .roundbox {
@@ -518,6 +523,21 @@
cursor: pointer; cursor: pointer;
} }
@media screen {
#players-list {
font-size: smaller;
}
.multi-select .listitem {
font-size: smaller;
}
#results-list {
font-size: smaller;
}
#standings-container {
font-size: smaller;
}
}
@media print { @media print {
body { body {
@@ -545,7 +565,8 @@
margin-top: 0.1em !important; margin-top: 0.1em !important;
} }
#header, #logo, #lang, .steps, #filter-box, #reglist-mode, #footer, #unpairables, #pairing-buttons, button, #standings-params, #logout, .pairing-stats, .result-sheets, .toggle, #overview { /* TODO - plenty of those elements could just use the .noprint class */
#header, #logo, #lang, .steps, #filter-box, #reglist-mode, #footer, #unpairables, #pairing-buttons, button, #standings-params, #logout, .pairing-stats, .result-sheets, .toggle, #overview, .tables-exclusion, .button, .noprint {
display: none !important; display: none !important;
} }
@@ -675,9 +696,10 @@
display: none; display: none;
} }
#players-list tr > :first-child { /* should final/preliminary column be printed? */
display: none; /* #players-list tr > :first-child { */
} /* display: none; */
/* } */
#players-list #players .participation .ui.label { #players-list #players .participation .ui.label {
background: none; background: none;
@@ -726,6 +748,9 @@
#standings-table { #standings-table {
font-size: 0.70rem; font-size: 0.70rem;
} }
.title-popup {
display: none;
}
} }
} }

View File

@@ -87,6 +87,8 @@
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
margin: 0 1em; margin: 0 1em;
align-items: baseline;
gap: 0.5em;
} }
#players-list { #players-list {
@@ -377,7 +379,7 @@
gap: 1em; gap: 1em;
max-width: max(10em, 20vw); max-width: max(10em, 20vw);
} }
#unpairables { #unpairables, #previous_games {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
min-height: 10vh; min-height: 10vh;
@@ -405,6 +407,18 @@
margin-top: 0.2em; margin-top: 0.2em;
} }
.bottom-pairing-actions {
margin-top: 0.2em;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
gap: 0.2em;
}
.tables-exclusion {
margin-top: 0.2em;
}
/* results section */ /* results section */
#results-filter { #results-filter {
@@ -472,6 +486,23 @@
max-width: 95vw; max-width: 95vw;
} }
#standings-table thead tr th {
z-index: 10;
}
td.game-result {
position: relative;
.title-popup {
position: absolute;
top: 90%;
background: silver;
padding: 4px;
left: 10%;
white-space: nowrap;
z-index: 2;
}
}
.ui.steps { .ui.steps {
margin-top: 0.2em; margin-top: 0.2em;
.step { .step {

View File

@@ -5,6 +5,7 @@
<tool key="translate" class="org.jeudego.pairgoth.view.TranslationTool"/> <tool key="translate" class="org.jeudego.pairgoth.view.TranslationTool"/>
<tool key="strings" class="org.apache.commons.lang3.StringUtils"/> <tool key="strings" class="org.apache.commons.lang3.StringUtils"/>
<tool key="utils" class="org.jeudego.pairgoth.view.PairgothTool"/> <tool key="utils" class="org.jeudego.pairgoth.view.PairgothTool"/>
<tool key="number" locale="en_US"/>
<!-- <!--
<tool key="number" format="#0.00"/> <tool key="number" format="#0.00"/>
<tool key="date" locale="fr_FR" format="yyyy-MM-dd"/> <tool key="date" locale="fr_FR" format="yyyy-MM-dd"/>

View File

@@ -227,11 +227,19 @@ Secondary parameters 부가 설정
Geographical parameters 지리적 설정 Geographical parameters 지리적 설정
Handicap parameters 핸디캡 설정 Handicap parameters 핸디캡 설정
deterministic randomness 결정적 무작위성 deterministic randomness 결정적 무작위성
<<<<<<< e8943b690eca2a284ab2fabd0d014fb77981af21
Randomness: 무작위성 Randomness: 무작위성
none 없음 none 없음
deterministic 결정적 deterministic 결정적
non-deterministic 비결정론적 non-deterministic 비결정론적
balance white and black 점 차이를 선호 balance white and black 점 차이를 선호
=======
Randomness 무작위성
none 없음
deterministic 없음
non-deterministic 비결정론적
balance white and black
>>>>>>> 5528e07f8e0cdda908847577340c595e9d2df8aa
Round Round
down 내림 down 내림
up 올림 up 올림

View File

@@ -194,6 +194,12 @@ function downloadFile(blob, filename) {
document.body.removeChild(link); document.body.removeChild(link);
} }
function isTouchDevice() {
return (('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0));
}
onLoad(() => { onLoad(() => {
$('button.close').on('click', e => { $('button.close').on('click', e => {
close_modal(); close_modal();
@@ -342,4 +348,28 @@ onLoad(() => {
let dialog = e.target.closest('.popup'); let dialog = e.target.closest('.popup');
if (!dialog) close_modal(); if (!dialog) close_modal();
}); });
if (isTouchDevice()) {
$("[title]").on('click', e => {
let item = e.target.closest('[title]');
let title = item.getAttribute('title');
let popup = item.find('.title-popup')
if (popup.length === 0) {
item.insertAdjacentHTML('beforeend', `<span class="title-popup">${title}</span>`);
} else {
item.removeChild(popup[0]);
}
});
}
}); });
// Element.clearChildren method
if( typeof Element.prototype.clearChildren === 'undefined' ) {
Object.defineProperty(Element.prototype, 'clearChildren', {
configurable: true,
enumerable: false,
value: function() {
while(this.firstChild) this.removeChild(this.lastChild);
}
});
}

View File

@@ -224,17 +224,17 @@ onLoad(() => {
let tour = { let tour = {
pairing: { pairing: {
base: { base: {
deterministic: form.val('deterministic'), randomness: form.val('randomness'),
colorBalanceWeight: form.val('colorBalance') ? 1000000.0 : 0.0 // TODO use client side boolean colorBalance: form.val('colorBalance')
}, },
main: { main: {
mmsValueAbsent: form.val('mmsValueAbsent'), mmsValueAbsent: form.val('mmsValueAbsent'),
roundDownScore: form.val('roundDownScore'), roundDownScore: form.val('roundDownScore'),
sosValueAbsentUseBase: form.val('sosValueAbsentUseBase'), sosValueAbsentUseBase: form.val('sosValueAbsentUseBase'),
firstSeedLastRound: form.val('firstSeedLastRound'), firstSeedLastRound: form.val('firstSeedLastRound'),
firstSeedAddCrit: form.val('firstSeedAddRating') ? 'RATING' : 'NONE', // TODO use client side boolean firstSeedAddRating: form.val('firstSeedAddRating'),
firstSeed: form.val('firstSeed'), firstSeed: form.val('firstSeed'),
secondSeedAddCrit: form.val('secondSeedAddRating') ? 'RATING' : 'NONE', // TODO use client side boolean secondSeedAddRating: form.val('secondSeedAddRating'),
secondSeed: form.val('secondSeed'), secondSeed: form.val('secondSeed'),
upDownCompensate: form.val('upDownCompensate'), upDownCompensate: form.val('upDownCompensate'),
upDownUpperMode: form.val('upDownUpperMode'), upDownUpperMode: form.val('upDownUpperMode'),
@@ -281,7 +281,7 @@ onLoad(() => {
$('select[name="pairing"]').on('change', e => { $('select[name="pairing"]').on('change', e => {
let pairing = e.target.value.toLowerCase(); let pairing = e.target.value.toLowerCase();
if (pairing === 'mms') $('#tournament-infos .mms').removeClass('hidden'); if (pairing === 'mac_mahon') $('#tournament-infos .mms').removeClass('hidden');
else $('#tournament-infos .mms').addClass('hidden'); else $('#tournament-infos .mms').addClass('hidden');
if (pairing === 'swiss') $('#tournament-infos .swiss').removeClass('hidden'); if (pairing === 'swiss') $('#tournament-infos .swiss').removeClass('hidden');
else $('#tournament-infos .swiss').addClass('hidden'); else $('#tournament-infos .swiss').addClass('hidden');

View File

@@ -1,12 +1,31 @@
let focused = undefined; let focused = undefined;
function pair(parts) { function pair(parts) {
let doWork = () => {
api.postJson(`tour/${tour_id}/pair/${activeRound}`, parts) api.postJson(`tour/${tour_id}/pair/${activeRound}`, parts)
.then(rst => { .then(rst => {
if (rst !== 'error') { if (rst !== 'error') {
document.location.reload(); document.location.reload();
} }
}); });
}
let tablesExclusionControl = $('#exclude-tables');
let value = tablesExclusionControl[0].value;
let origValue = tablesExclusionControl.data('orig');
if (value === origValue) {
// tables exclusion value did not change
doWork();
} else {
// tables exclusion value has change, we must save it first
api.putJson(`tour/${tour_id}`, { round: activeRound, excludeTables: value })
.then(rst => {
if (rst !== 'error') {
doWork();
}
});
}
} }
function unpair(games) { function unpair(games) {
@@ -19,7 +38,14 @@ function unpair(games) {
} }
function renumberTables() { function renumberTables() {
api.putJson(`tour/${tour_id}/pair/${activeRound}`, {}) let payload = {}
let tablesExclusionControl = $('#exclude-tables');
let value = tablesExclusionControl[0].value;
let origValue = tablesExclusionControl.data('orig');
if (value !== origValue) {
payload['excludeTables'] = value;
}
api.putJson(`tour/${tour_id}/pair/${activeRound}`, payload)
.then(rst => { .then(rst => {
if (rst !== 'error') { if (rst !== 'error') {
document.location.reload(); document.location.reload();
@@ -28,6 +54,7 @@ function renumberTables() {
} }
function editGame(game) { function editGame(game) {
// CB TODO - those should be data attributes of the parent game tag
let t = game.find('.table'); let t = game.find('.table');
let w = game.find('.white'); let w = game.find('.white');
let b = game.find('.black'); let b = game.find('.black');
@@ -35,6 +62,7 @@ function editGame(game) {
let form = $('#pairing-form')[0]; let form = $('#pairing-form')[0];
form.val('id', game.data('id')); form.val('id', game.data('id'));
form.val('prev-table', t.data('value'));
form.val('t', t.data('value')); form.val('t', t.data('value'));
form.val('w', w.data('id')); form.val('w', w.data('id'));
$('#edit-pairing-white').text(w.text()); $('#edit-pairing-white').text(w.text());
@@ -80,12 +108,38 @@ function updatePairable() {
}); });
} }
function showOpponents(player) {
let id = player.data('id');
let games = $(`#standings-table tbody tr[data-id="${id}"] .game-result`)
if (games.length) {
let title = `${$('#previous_games_prefix').text()}${player.innerText.replace('\n', ' ')}${$('#previous_games_postfix').text()}`;
$('#unpairables').addClass('hidden');
$('#previous_games')[0].setAttribute('title', title);
$('#previous_games')[0].clearChildren();
$('#previous_games').removeClass('hidden');
for (let r = 0; r < activeRound; ++r) {
let game = games[r]
let opponent = game.getAttribute('title');
if (!opponent) opponent = '';
let result = game.text().replace(/^\d+/, '');
let listitem = `<div data-id="${id}" class="listitem"><span>R${r+1}</span><span>${opponent}</span><span>${result}</span></div>`
$('#previous_games')[0].insertAdjacentHTML('beforeend', listitem);
}
}
}
function hideOpponents() {
$('#unpairables').removeClass('hidden');
$('#previous_games').addClass('hidden');
}
onLoad(()=>{ onLoad(()=>{
// note - this handler is also in use for lists on Mac Mahon super groups and teams pages // note - this handler is also in use for lists on Mac Mahon super groups and teams pages
$('.listitem').on('click', e => { $('.listitem').on('click', e => {
let listitem = e.target.closest('.listitem'); let listitem = e.target.closest('.listitem');
let box = e.target.closest('.multi-select'); let box = e.target.closest('.multi-select');
if (e.shiftKey && typeof(focused) !== 'undefined') { let focusedBox = focused ? focused.closest('.multi-select') : undefined;
if (e.shiftKey && typeof(focused) !== 'undefined' && box.getAttribute('id') === focusedBox.getAttribute('id')) {
let from = focused.index('.listitem'); let from = focused.index('.listitem');
let to = listitem.index('.listitem'); let to = listitem.index('.listitem');
if (from > to) { if (from > to) {
@@ -102,10 +156,12 @@ onLoad(()=>{
if (e.detail === 1) { if (e.detail === 1) {
// single click // single click
focused = listitem.toggleClass('selected').attr('draggable', listitem.hasClass('selected')); focused = listitem.toggleClass('selected').attr('draggable', listitem.hasClass('selected'));
if (box.getAttribute('id') === 'pairables') showOpponents(focused)
} else if (listitem.closest('#pairing-lists')) { } else if (listitem.closest('#pairing-lists')) {
// on pairing page // on pairing page
if (listitem.closest('#paired')) { if (listitem.closest('#paired')) {
// double click // double click
hideOpponents()
focused = listitem.attr('draggable', listitem.hasClass('selected')); focused = listitem.attr('draggable', listitem.hasClass('selected'));
editGame(focused); editGame(focused);
} else if (listitem.closest('#pairables')) { } else if (listitem.closest('#pairables')) {
@@ -162,6 +218,12 @@ onLoad(()=>{
b: form.val('b'), b: form.val('b'),
h: form.val('h') h: form.val('h')
} }
let prevTable = form.val('prev-table');
if (prevTable !== game.t && $(`.t[data-table="${game.t}"]`).length > 0) {
if (!confirm(`This change will trigger a tables renumbering because the destination table #${game.t} is not empty. Proceed?`)) {
return;
}
}
api.putJson(`tour/${tour_id}/pair/${activeRound}`, game) api.putJson(`tour/${tour_id}/pair/${activeRound}`, game)
.then(game => { .then(game => {
if (game !== 'error') { if (game !== 'error') {
@@ -169,10 +231,11 @@ onLoad(()=>{
} }
}); });
}); });
$('.multi-select').on('dblclick', e => { document.on('dblclick', e => {
let box = e.target.closest('.multi-select');
if (!e.target.closest('.listitem')) { if (!e.target.closest('.listitem')) {
box.find('.listitem').removeClass('selected'); $('.listitem').removeClass('selected');
focused = undefined;
hideOpponents()
} }
}); });
$('#update-pairable').on('click', e => { $('#update-pairable').on('click', e => {

View File

@@ -70,6 +70,13 @@ onLoad(()=>{
let newResult = results[(index + 1)%results.length]; let newResult = results[(index + 1)%results.length];
setResult(gameId, newResult, oldResult); setResult(gameId, newResult, oldResult);
}); });
$('#results-table .result').on('dblclick', e => {
let cell = e.target.closest('.result');
let gameId = e.target.closest('tr').data('id');
let oldResult = cell.data('result');
let newResult = '?';
setResult(gameId, newResult, oldResult);
});
$('#results-filter').on('click', e => { $('#results-filter').on('click', e => {
let filter = $('#results-filter input')[0]; let filter = $('#results-filter input')[0];
filter.checked = !filter.checked; filter.checked = !filter.checked;

View File

@@ -24,7 +24,18 @@ 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]);
$('.criterium').on('click', e => { $('.criterium').on('click', e => {
let alreadyOpen = e.target.closest('select'); let alreadyOpen = e.target.closest('select');
if (alreadyOpen) return; if (alreadyOpen) return;
@@ -85,4 +96,9 @@ onLoad(() => {
$('.publish-html').on('click', e => { $('.publish-html').on('click', e => {
publishHtml(); publishHtml();
}); });
$('#freeze').on('click', e => {
if (confirm("Once frozen, names, levels and even pairings can be changed, but the scores and the standings will stay the same. Freeze the standings?")) {
freeze()
}
});
}); });

View File

@@ -28,12 +28,18 @@
#end #end
#set($games = $utils.removeBye($roundPairing.games)) #set($games = $utils.removeBye($roundPairing.games))
#set($pages = ($games.size() + 3) / 4) #set($pages = ($games.size() + 3) / 4)
#foreach($i in [1..$games.size()]) #set($items = $pages * 4)
#foreach($i in [1..$items])
#set($j = ($i - 1) / 4 + (($i - 1) % 4) * $pages) #set($j = ($i - 1) / 4 + (($i - 1) % 4) * $pages)
#if($j < $games.size()) #if($j < $games.size())
#set($game = $games[$j]) #set($game = $games[$j])
#set($white = $pmap[$game.w]) #set($white = $pmap[$game.w])
#set($black = $pmap[$game.b]) #set($black = $pmap[$game.b])
#else
#set($game = { 't': 'xxx', 'h': 'xxx' })
#set($white = { 'name': 'xxx', 'firstname': 'xxx', 'rank': -99, 'country': 'XX', 'club': 'xxx' })
#set($black = { 'name': 'xxx', 'firstname': 'xxx', 'rank': -99, 'country': 'XX', 'club': 'xxx' })
#end
#if($foreach.index % 4 == 0) #if($foreach.index % 4 == 0)
<div class="page"> <div class="page">
#end #end
@@ -75,7 +81,6 @@
</div> </div>
#end #end
#end #end
#end
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
onLoad(() => { onLoad(() => {

View File

@@ -22,6 +22,14 @@
<button class="ui floating choose-round next-round button">&raquo;</button> <button class="ui floating choose-round next-round button">&raquo;</button>
</div> </div>
<div class="pairing-stats nobreak">( $pairables.size() pairable, $games.size() games )</div> <div class="pairing-stats nobreak">( $pairables.size() pairable, $games.size() games )</div>
<div class="tables-exclusion">
#if($tour.tablesExclusion && $round <= $tour.tablesExclusion.size())
#set($tablesExclusion = $!tour.tablesExclusion[$round - 1])
#else
#set($tablesExclusion = '')
#end
Exclude table numbers: <input type="text" id="exclude-tables" name="exclude-tables" placeholder="ex: 1-34, 38, 45-77" data-orig="$tablesExclusion" value="$tablesExclusion"/>
</div>
<div id="pairing-lists"> <div id="pairing-lists">
<div id="pairing-left"> <div id="pairing-left">
<div id="pairables" class="multi-select" title="pairable players"> <div id="pairables" class="multi-select" title="pairable players">
@@ -36,6 +44,9 @@
<div data-id="$part.id" class="listitem unpairable"><span class="name">$part.name#if($part.firstname) $part.firstname#end</span><span>#rank($part.rank)#if($part.country) $part.country#end</span></div> <div data-id="$part.id" class="listitem unpairable"><span class="name">$part.name#if($part.firstname) $part.firstname#end</span><span>#rank($part.rank)#if($part.country) $part.country#end</span></div>
#end #end
</div> </div>
<div id="previous_games" class="hidden multi-select">
</div>
</div> </div>
<div id="pairing-right"> <div id="pairing-right">
<div class="pairing-buttons"> <div class="pairing-buttons">
@@ -87,7 +98,7 @@
#set($white = $pmap[$game.w]) #set($white = $pmap[$game.w])
#set($black = $pmap[$game.b]) #set($black = $pmap[$game.b])
<tr> <tr>
<td>${game.t}</td> <td class="t" data-table="${game.t}">${game.t}</td>
<td class="left">#if($white)${white.name} ${white.firstname} (#rank($white.rank), $white.country $white.club)#{else}BIP#end</td> <td class="left">#if($white)${white.name} ${white.firstname} (#rank($white.rank), $white.country $white.club)#{else}BIP#end</td>
<td class="left">#if($black)${black.name} ${black.firstname} (#rank($black.rank), $black.country $black.club)#{else}BIP#end</td> <td class="left">#if($black)${black.name} ${black.firstname} (#rank($black.rank), $black.country $black.club)#{else}BIP#end</td>
<td>${game.h}</td> <td>${game.h}</td>
@@ -103,6 +114,7 @@
<div class="popup-body"> <div class="popup-body">
<form id="pairing-form" class="ui form edit"> <form id="pairing-form" class="ui form edit">
<input type="hidden" name="id"/> <input type="hidden" name="id"/>
<input type="hidden" name="prev-table"/>
<div class="popup-content"> <div class="popup-content">
<div class="inline fields"> <div class="inline fields">
<div class="field"> <div class="field">
@@ -176,3 +188,8 @@
</form> </form>
</div> </div>
</div> </div>
## For dynamic texts to be translated, they must be somewhere in the html source.
## TODO - gather all "text only" nodes like this somewhere
<div id="previous_games_prefix" class="hidden">Games of </div>
<div id="previous_games_postfix" class="hidden"></div>

View File

@@ -3,7 +3,14 @@
<div class="title"><i class="dropdown icon"></i>Base parameters</div> <div class="title"><i class="dropdown icon"></i>Base parameters</div>
<div class="content"> <div class="content">
<div class="field"> <div class="field">
<label><input type="checkbox" name="deterministic" value="true" #if($tour.pairing.base.deterministic) checked #end>&nbsp;deterministic randomness</label> <label>
Randomness:
<select name="randomness">
<option value="none" #if($tour.pairing.base.random == 0.0)selected#end>none</option>
<option value="deterministic" #if($tour.pairing.base.random != 0.0 && $tour.pairing.base.deterministic)selected#end>deterministic</option>
<option value="non-deterministic" #if($tour.pairing.base.random != 0.0 && !$tour.pairing.base.deterministic)selected#end>non-deterministic</option>
</select>
</label>
</div> </div>
<div class="field"> <div class="field">
<label><input type="checkbox" name="colorBalance" value="true" #if($tour.pairing.base.colorBalanceWeight) checked #end>&nbsp;balance white and black</label> <label><input type="checkbox" name="colorBalance" value="true" #if($tour.pairing.base.colorBalanceWeight) checked #end>&nbsp;balance white and black</label>
@@ -14,14 +21,7 @@
#if($tour.pairing.type == 'MAC_MAHON') #if($tour.pairing.type == 'MAC_MAHON')
<div class="inline fields"> <div class="inline fields">
<div class="field"> <div class="field">
<label> <label><input type="checkbox" name="roundDownScore" value="true" #if($tour.pairing.main.roundDownScore) checked #end>&nbsp;round down NBW/MMS score</label>
Round
<select name="roundDownScore">
<option value="true" #if($tour.pairing.main.roundDownScore) selected #end>down</option>
<option value="false" #if(!$tour.pairing.main.roundDownScore) selected #end>up</option>
</select>
NBW/MMS score
</label>
</div> </div>
</div> </div>
#end #end

View File

@@ -6,6 +6,26 @@
#set($pmap = $utils.toMap($teams)) #set($pmap = $utils.toMap($teams))
#end #end
## Team players do not have an individual MMS
#if($tour.type == 'INDIVIDUAL' && $tour.pairing.type == 'MAC_MAHON')
#set($mmbase = $api.get("tour/${params.id}/standings/0?include_preliminary=true"))
#if($mmbase.isObject() && ($mmbase.error || $mmbase.message))
#if($mmbase.error)
#set($error = $mmbase.error)
#else
#set($error = $mmbase.message)
#end
<script type="text/javascript">
onLoad(() => {
showError("$error")
});
</script>
#set($mmbase = [])
#end
#set($mmsMap = $utils.getMmsMap($mmbase))
#set($mmsPlayersMap = $utils.getMmsPlayersMap($mmbase))
#end
<div class="tab-content" id="registration-tab"> <div class="tab-content" id="registration-tab">
<div id="reg-view"> <div id="reg-view">
<div id="list-header"> <div id="list-header">
@@ -33,14 +53,18 @@
<th>First name</th> <th>First name</th>
<th>Country</th> <th>Country</th>
<th>Club</th> <th>Club</th>
##if($tour.country == 'FR') #if($utils.showRatings('egf', $tour.country.toLowerCase()))
## <th>FFG</th>
##else
<th>PIN</th> <th>PIN</th>
##end #end
#if($utils.showRatings('ffg', $tour.country.toLowerCase()))
<th>FFG</th>
#end
<th>Rank</th> <th>Rank</th>
## TableSort bug which inverts specified sort... ## TableSort bug which inverts specified sort...
<th data-sort-default="1" aria-sort="ascending">Rating</th> <th data-sort-default="1" aria-sort="ascending">Rating</th>
#if($tour.type == 'INDIVIDUAL' && $tour.pairing.type == 'MAC_MAHON')
<th>MMS</th>
#end
<th>Participation</th> <th>Participation</th>
</thead> </thead>
<tbody> <tbody>
@@ -54,13 +78,18 @@
<td>$part.firstname</td> <td>$part.firstname</td>
<td>$part.country.toUpperCase()</td> <td>$part.country.toUpperCase()</td>
<td>$part.club</td> <td>$part.club</td>
#if($tour.country == 'FR') #if($utils.showRatings('egf', $tour.country.toLowerCase()))
<td>$!part.ffg</td> <td>$!part.egf </td>
#else
<td>$!part.egf</td>
#end #end
<td data-sort="$part.rank">#rank($part.rank)#if($part.mmsCorrection) (#if($part.mmsCorrection > 0)+#end$part.mmsCorrection)#end</td> #if($utils.showRatings('ffg', $tour.country.toLowerCase()))
<td>$!part.ffg</td>
#end
## display MMS correction on the screen, but not when printed
<td data-sort="$part.rank">#rank($part.rank)#if($part.mmsCorrection)<span class="noprint"> (#if($part.mmsCorrection > 0)+#end$part.mmsCorrection)</span>#end</td>
<td>$part.rating</td> <td>$part.rating</td>
#if($tour.type == 'INDIVIDUAL' && $tour.pairing.type == 'MAC_MAHON')
<td>$!mmsPlayersMap[$part.id]</td>
#end
<td class="participating" data-sort="#if($part.skip)$part.skip.size()/part.skip#{else}0#end"> <td class="participating" data-sort="#if($part.skip)$part.skip.size()/part.skip#{else}0#end">
<div class="participation"> <div class="participation">
#foreach($round in [1..$tour.rounds]) #foreach($round in [1..$tour.rounds])
@@ -108,7 +137,15 @@
</div> </div>
</div> </div>
#end #end
<div class="needle eight wide field"> #set($needleWidth = 12)
#if($utils.displayRatings('egf', $tour.country.toLowerCase()))
#set($needleWidth = $needleWidth - 2)
#end
#if($utils.displayRatings('ffg', $tour.country.toLowerCase()))
#set($needleWidth = $needleWidth - 2)
#end
#set($cssWidth = { 8: 'eight', 10: 'ten', 12: 'twelve' })
<div class="needle $cssWidth[$needleWidth] wide field">
<div class="ui icon input"> <div class="ui icon input">
<input id="needle" name="needle" type="text" placeholder="Search..." spellcheck="false"> <input id="needle" name="needle" type="text" placeholder="Search..." spellcheck="false">
<i id="clear-search" class="clickable close icon"></i> <i id="clear-search" class="clickable close icon"></i>
@@ -125,6 +162,7 @@
</div> </div>
</div> </div>
*# *#
#if($utils.displayRatings('egf', $tour.country.toLowerCase()))
<div class="two wide centered field"> <div class="two wide centered field">
<div class="toggle" title="${utils.ratingsDates.egf|'no egf ratings'}"> <div class="toggle" title="${utils.ratingsDates.egf|'no egf ratings'}">
<input id="egf" name="egf" type="checkbox" checked value="true"/> <input id="egf" name="egf" type="checkbox" checked value="true"/>
@@ -134,6 +172,8 @@
<label>EGF</label> <label>EGF</label>
</div> </div>
</div> </div>
#end
#if($utils.displayRatings('ffg', $tour.country.toLowerCase()))
<div class="two wide centered field"> <div class="two wide centered field">
<div class="toggle" title="${utils.ratingsDates.ffg|'no ffg ratings'}"> <div class="toggle" title="${utils.ratingsDates.ffg|'no ffg ratings'}">
<input id="ffg" name="ffg" type="checkbox" checked value="true"/> <input id="ffg" name="ffg" type="checkbox" checked value="true"/>
@@ -143,8 +183,9 @@
<label>FFG</label> <label>FFG</label>
</div> </div>
</div> </div>
#end
<div class="two wide centered field"> <div class="two wide centered field">
<div class="toggle" title="${utils.ratingsDates.ffg|'no ffg ratings'}"> <div class="toggle" title="browse">
<input id="browse" name="browse" type="checkbox" value="true"/> <input id="browse" name="browse" type="checkbox" value="true"/>
<div class="search-param checkbox"> <div class="search-param checkbox">
<div class="circle"></div> <div class="circle"></div>
@@ -249,21 +290,6 @@
</div> </div>
</div> </div>
#if($tour.type == 'INDIVIDUAL' && $tour.pairing.type == 'MAC_MAHON') #if($tour.type == 'INDIVIDUAL' && $tour.pairing.type == 'MAC_MAHON')
#set($mmbase = $api.get("tour/${params.id}/standings/0"))
#if($mmbase.isObject() && ($mmbase.error || $mmbase.message))
#if($mmbase.error)
#set($error = $mmbase.error)
#else
#set($error = $mmbase.message)
#end
<script type="text/javascript">
onLoad(() => {
showError("$error")
});
</script>
#set($mmbase = [])
#end
#set($mmsMap = $utils.getMmsMap($mmbase))
<div id="macmahon-groups" class="wide popup"> <div id="macmahon-groups" class="wide popup">
<div class="popup-body"> <div class="popup-body">
<div class="popup-content"> <div class="popup-content">

View File

@@ -20,6 +20,7 @@
<th data-sort-method="number">table</th> <th data-sort-method="number">table</th>
<th>white</th> <th>white</th>
<th>black</th> <th>black</th>
<th>hd</th>
<th>result</th> <th>result</th>
</thead> </thead>
<tbody> <tbody>
@@ -32,6 +33,7 @@
<td data-sort="$game.t">${game.t}.</td> <td data-sort="$game.t">${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#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #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#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #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#if($black.firstname) $black.firstname#end"><span>#if($black)$black.name#if($black.firstname) $black.firstname#end #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#if($black.firstname) $black.firstname#end"><span>#if($black)$black.name#if($black.firstname) $black.firstname#end #rank($black.rank)#{else}BIP#end</span></td>
<td class="handicap centered">$!game.h</td>
<td class="result centered" data-sort="$game.r" 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

View File

@@ -44,6 +44,10 @@
}); });
</script> </script>
#set($standings = []) #set($standings = [])
#end
#set($smap = {})
#foreach($part in $standings)
#set($smap[$part.num] = $part)
#end #end
<table id="standings-table" class="ui striped table"> <table id="standings-table" class="ui striped table">
<thead> <thead>
@@ -56,31 +60,45 @@
#foreach($r in [1..$round]) #foreach($r in [1..$round])
<th>R$r</th> <th>R$r</th>
#end #end
#set($criteres = [])
#foreach($crit in $tour.pairing.placement) #foreach($crit in $tour.pairing.placement)
#set($junk = $criteres.add($crit))
#end
#if($criteres[0] == 'SCOREX')
#set($junk = $criteres.add(1, 'MMS'))
#end
#foreach($crit in $criteres)
<th>$crit</th> <th>$crit</th>
#end #end
</thead> </thead>
<tbody> <tbody>
#foreach($part in $standings) #foreach($part in $standings)
<tr> <tr data-id="$part.id">
<td>$part.num</td> <td>$part.num</td>
<td>$part.place</td> <td>$part.place</td>
<td>$part.name#if($part.firstname) $part.firstname#end</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 data-sort="$part.rank">#rank($part.rank)</td>
<td>#if($part.country)$part.country#end</td> <td>#if($part.country)$part.country#end</td>
<td>$number.format('0.#', $part.NBW)</td> <td>$number.format('0.#', $part.NBW)</td>
#set($mx = $round - 1) #set($mx = $round - 1)
#foreach($r in [0..$mx]) #foreach($r in [0..$mx])
#set($rst = $part.results[$r]) #set($rst = $part.results[$r])
#set($opp_num = $math.toLong($rst))
#if($opp_num)
#set($opponent = $!smap[$opp_num])
#else
#set($opponent = false)
#end
#if($rst.contains('+')) #if($rst.contains('+'))
#set($rst = "<b>$rst</b>") #set($rst = "<b>$rst</b>")
#elseif($rst.contains('-')) #elseif($rst.contains('-'))
#set($rst = "<i>$rst</i>") #set($rst = "<i>$rst</i>")
#end #end
<td class="nobreak">$rst</td> <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 #end
#foreach($crit in $tour.pairing.placement) #foreach($crit in $criteres)
<td>$number.format('0.#', $part[$crit])</td> #set($value = "$number.format('0.#', $part[$crit])")
<td data-sort="$value">$value.replace('.5', '½')</td>
#end #end
</tr> </tr>
#end #end
@@ -88,6 +106,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

View File

@@ -26,7 +26,20 @@
#if($tour) #if($tour)
#set($round = $math.toInteger($!params.round)) #set($round = $math.toInteger($!params.round))
#if(!$round) #if(!$round)
#set($lastCompleteRound = 0)
#foreach($r in [1..$tour.rounds])
#set($stats = $tour.stats[$r - 1])
#if($stats.ready == $stats.games)
#set($lastCompleteRound = $r)
#else
#break
#end
#end
#if($lastCompleteRound)
#set($round = $math.min($lastCompleteRound + 1, $tour.rounds))
#else
#set($round = 1) #set($round = 1)
#end
#else #else
#set($round = $math.min($math.max($round, 1), $tour.rounds)) #set($round = $math.min($math.max($round, 1), $tour.rounds))
#end #end

View File

@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.jeudego.pairgoth</groupId> <groupId>org.jeudego.pairgoth</groupId>
<artifactId>engine-parent</artifactId> <artifactId>engine-parent</artifactId>
<version>0.14</version> <version>0.15</version>
</parent> </parent>
<artifactId>webserver</artifactId> <artifactId>webserver</artifactId>
<packaging>jar</packaging> <packaging>jar</packaging>