Merge branch 'master' into 'translations'
# Conflicts: # view-webapp/src/main/webapp/WEB-INF/translations/kr
This commit is contained in:
@@ -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>
|
||||||
|
@@ -25,7 +25,7 @@ interface ApiHandler {
|
|||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
|
fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||||
notImplemented()
|
notImplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -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,53 +101,58 @@ 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
|
||||||
val sortedPairables = tournament.getSortedPairables(round)
|
if (payload.containsKey("excludeTables")) {
|
||||||
val sortedMap = sortedPairables.associateBy {
|
val tablesExclusion = payload.getString("excludeTables") ?: badRequest("missing 'excludeTables'")
|
||||||
it.getID()!!
|
TournamentHandler.validateTablesExclusion(tablesExclusion)
|
||||||
}
|
while (tournament.tablesExclusion.size < round) tournament.tablesExclusion.add("")
|
||||||
val changed = tournament.renumberTables(round, null) { game ->
|
tournament.tablesExclusion[round - 1] = tablesExclusion
|
||||||
val whitePosition = sortedMap[game.white]?.getInt("num") ?: Int.MIN_VALUE
|
tournament.dispatchEvent(TournamentUpdated, request, tournament.toJson())
|
||||||
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()))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renumberTables(request, tournament, round)
|
||||||
|
|
||||||
return Json.Object("success" to true)
|
return Json.Object("success" to true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun renumberTables(request: HttpServletRequest, tournament: Tournament<*>, round: Int, pivot: Game? = null) {
|
||||||
|
val sortedPairables = tournament.getSortedPairables(round)
|
||||||
|
val sortedMap = sortedPairables.associateBy {
|
||||||
|
it.getID()!!
|
||||||
|
}
|
||||||
|
val changed = tournament.renumberTables(round, pivot) { gm ->
|
||||||
|
val whitePosition = sortedMap[gm.white]?.getInt("num") ?: Int.MIN_VALUE
|
||||||
|
val blackPosition = sortedMap[gm.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())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {
|
override fun delete(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")
|
||||||
|
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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")
|
||||||
}
|
}
|
||||||
|
@@ -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")
|
||||||
|
@@ -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,30 +64,86 @@ 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")
|
||||||
val updated = Tournament.fromJson(payload, tournament)
|
// specific handling for 'excludeTables'
|
||||||
// copy players, games, criteria (this copy should be provided by the Tournament class - CB TODO)
|
if (payload.containsKey("excludeTables")) {
|
||||||
updated.players.putAll(tournament.players)
|
val tablesExclusion = payload.getString("excludeTables") ?: badRequest("missing 'excludeTables'")
|
||||||
if (tournament is TeamTournament && updated is TeamTournament) {
|
validateTablesExclusion(tablesExclusion)
|
||||||
updated.teams.putAll(tournament.teams)
|
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)
|
||||||
|
// copy players, games, criteria (this copy should be provided by the Tournament class - CB TODO)
|
||||||
|
updated.players.putAll(tournament.players)
|
||||||
|
if (tournament is TeamTournament && updated is TeamTournament) {
|
||||||
|
updated.teams.putAll(tournament.teams)
|
||||||
|
}
|
||||||
|
for (round in 1..tournament.lastRound()) updated.games(round).apply {
|
||||||
|
clear()
|
||||||
|
putAll(tournament.games(round))
|
||||||
|
}
|
||||||
|
updated.dispatchEvent(TournamentUpdated, request, updated.toJson())
|
||||||
}
|
}
|
||||||
for (round in 1..tournament.lastRound()) updated.games(round).apply {
|
|
||||||
clear()
|
|
||||||
putAll(tournament.games(round))
|
|
||||||
}
|
|
||||||
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+)?)*$")
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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
|
||||||
)
|
)
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
}
|
}
|
||||||
|
@@ -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)
|
||||||
|
@@ -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]
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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,
|
||||||
|
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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
1400
api-webapp/src/test/resources/opengotha/pairings/malavasi.geobug.xml
Normal file
1400
api-webapp/src/test/resources/opengotha/pairings/malavasi.geobug.xml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
@@ -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.
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
2
pom.xml
2
pom.xml
@@ -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 -->
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
}
|
}
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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"/>
|
||||||
|
@@ -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 올림
|
||||||
|
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -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');
|
||||||
|
@@ -1,12 +1,31 @@
|
|||||||
let focused = undefined;
|
let focused = undefined;
|
||||||
|
|
||||||
function pair(parts) {
|
function pair(parts) {
|
||||||
api.postJson(`tour/${tour_id}/pair/${activeRound}`, parts)
|
|
||||||
.then(rst => {
|
let doWork = () => {
|
||||||
if (rst !== 'error') {
|
api.postJson(`tour/${tour_id}/pair/${activeRound}`, parts)
|
||||||
document.location.reload();
|
.then(rst => {
|
||||||
}
|
if (rst !== 'error') {
|
||||||
});
|
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 => {
|
||||||
|
@@ -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;
|
||||||
|
@@ -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()
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -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(() => {
|
||||||
|
@@ -22,6 +22,14 @@
|
|||||||
<button class="ui floating choose-round next-round button">»</button>
|
<button class="ui floating choose-round next-round button">»</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>
|
@@ -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> 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> balance white and black</label>
|
<label><input type="checkbox" name="colorBalance" value="true" #if($tour.pairing.base.colorBalanceWeight) checked #end> 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> 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
|
||||||
|
@@ -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">
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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($round = 1)
|
#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)
|
||||||
|
#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
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user