Merge branch 'master' into 'translations'

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

View File

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

View File

@@ -25,7 +25,7 @@ interface ApiHandler {
notImplemented()
}
fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
notImplemented()
}

View File

@@ -2,10 +2,9 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json
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.Pairable
import org.jeudego.pairgoth.model.Pairable.Companion.MIN_RANK
import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID
@@ -16,11 +15,11 @@ import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.round
// 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 {
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
// Note: this works for now because we only have .0 and .5 fractional parts
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)) {
if (pairing.type == PairingType.SWISS) wins.mapValues { Pair(0.0, it.value) }
else pairables.mapValues {
@@ -42,7 +46,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
val score = roundScore(mmBase +
(nbW(pairable) ?: 0.0) +
(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)
Pair(
if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase
@@ -55,6 +59,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
val neededCriteria = ArrayList(pairing.placementParams.criteria)
if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW)
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 ->
crit.name to when (crit) {
Criterion.NONE -> StandingsHandler.nullMap
@@ -63,6 +68,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
Criterion.RATING -> pairables.mapValues { it.value.rating }
Criterion.NBW -> historyHelper.wins
Criterion.MMS -> historyHelper.mms
Criterion.SCOREX -> historyHelper.scoresX
Criterion.STS -> StandingsHandler.nullMap
Criterion.CPS -> StandingsHandler.nullMap
@@ -71,7 +77,7 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
Criterion.SOSWM2 -> historyHelper.sosm2
Criterion.SODOSW -> historyHelper.sodos
Criterion.SOSOSW -> historyHelper.sosos
Criterion.CUSSW -> historyHelper.cumScore
Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else historyHelper.cumScore
Criterion.SOSM -> historyHelper.sos
Criterion.SOSMM1 -> historyHelper.sosm1
Criterion.SOSMM2 -> historyHelper.sosm2
@@ -88,10 +94,10 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
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 ->
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=" })
}
@@ -113,5 +119,61 @@ fun Tournament<*>.getSortedPairables(round: Int): List<Json.Object> {
it.value.forEach { p -> p["place"] = place }
place += it.value.size
}
return sortedPairables
}
fun Tournament<*>.populateFrozenStandings(sortedPairables: List<Json.Object>, round: Int = rounds) {
val sortedMap = sortedPairables.associateBy {
it.getID()!!
}
// refresh name, firstname, club and level
sortedMap.forEach { (id, player) ->
val mutable = player as Json.MutableObject
val live = players[id]!!
mutable["name"] = live.name
mutable["firstname"] = live.firstname
mutable["club"] = live.club
mutable["rating"] = live.rating
mutable["rank"] = live.rank
}
// fill result
for (r in 1..round) {
games(r).values.forEach { game ->
val white = if (game.white != 0) sortedMap[game.white] else null
val black = if (game.black != 0) sortedMap[game.black] else null
val whiteNum = white?.getInt("num") ?: 0
val blackNum = black?.getInt("num") ?: 0
val whiteColor = if (black == null) "" else "w"
val blackColor = if (white == null) "" else "b"
val handicap = if (game.handicap == 0) "" else "${game.handicap}"
assert(white != null || black != null)
if (white != null) {
val mark = when (game.result) {
Game.Result.UNKNOWN -> "?"
Game.Result.BLACK, Game.Result.BOTHLOOSE -> "-"
Game.Result.WHITE, Game.Result.BOTHWIN -> "+"
Game.Result.JIGO, Game.Result.CANCELLED -> "="
}
val results = white.getArray("results") as Json.MutableArray
results[r - 1] =
if (blackNum == 0) "0$mark"
else "$blackNum$mark/$whiteColor$handicap"
}
if (black != null) {
val mark = when (game.result) {
Game.Result.UNKNOWN -> "?"
Game.Result.BLACK, Game.Result.BOTHWIN -> "+"
Game.Result.WHITE, Game.Result.BOTHLOOSE -> "-"
Game.Result.JIGO, Game.Result.CANCELLED -> "="
}
val results = black.getArray("results") as Json.MutableArray
results[r - 1] =
if (whiteNum == 0) "0$mark"
else "$whiteNum$mark/$blackColor$handicap"
}
}
}
}

View File

@@ -4,6 +4,7 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID
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")
val payload = getArrayPayload(request)
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"
//if (!allPlayers && tournament.pairing.type == PairingType.SWISS) badRequest("Swiss pairing requires all pairable players")
val playing = (tournament.games(round).values).flatMap {
@@ -57,16 +59,18 @@ object PairingHandler: PairgothApiHandler {
} ?: badRequest("invalid pairable id: #$id")
}
val games = tournament.pair(round, pairables)
val ret = games.map { it.toJson() }.toJsonArray()
tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret))
return ret
}
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
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...)
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)
if (payload.containsKey("id")) {
val gameId = payload.getInt("id") ?: badRequest("invalid game id")
@@ -97,38 +101,41 @@ object PairingHandler: PairgothApiHandler {
if (payload.containsKey("h")) game.handicap = payload.getString("h")?.toIntOrNull() ?: badRequest("invalid handicap")
if (payload.containsKey("t")) {
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()))
if (game.table != previousTable) {
val sortedPairables = tournament.getSortedPairables(round)
val sortedMap = sortedPairables.associateBy {
it.getID()!!
}
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()))
)
val tableWasOccupied = ( tournament.games(round).values.find { g -> g != game && g.table == game.table } != null )
if (tableWasOccupied) {
// some renumbering is necessary
renumberTables(request, tournament, round, game)
}
}
return Json.Object("success" to true)
} else {
// without id, it's a table renumbering
if (payload.containsKey("excludeTables")) {
val tablesExclusion = payload.getString("excludeTables") ?: badRequest("missing 'excludeTables'")
TournamentHandler.validateTablesExclusion(tablesExclusion)
while (tournament.tablesExclusion.size < round) tournament.tablesExclusion.add("")
tournament.tablesExclusion[round - 1] = tablesExclusion
tournament.dispatchEvent(TournamentUpdated, request, tournament.toJson())
}
renumberTables(request, tournament, round)
return Json.Object("success" to true)
}
}
private fun renumberTables(request: HttpServletRequest, tournament: Tournament<*>, round: Int, pivot: Game? = null) {
val sortedPairables = tournament.getSortedPairables(round)
val sortedMap = sortedPairables.associateBy {
it.getID()!!
}
val changed = tournament.renumberTables(round, null) { game ->
val whitePosition = sortedMap[game.white]?.getInt("num") ?: Int.MIN_VALUE
val blackPosition = sortedMap[game.black]?.getInt("num") ?: Int.MIN_VALUE
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) {
@@ -137,11 +144,13 @@ object PairingHandler: PairgothApiHandler {
}
tournament.dispatchEvent(
TablesRenumbered, request,
Json.Object("round" to round, "games" to games.map { it.toJson() }.toCollection(Json.MutableArray()))
Json.Object(
"round" to round,
"games" to games.map { it.toJson() }.toCollection(Json.MutableArray())
)
)
}
return Json.Object("success" to true)
}
}
override fun delete(request: HttpServletRequest, response: HttpServletResponse): Json {

View File

@@ -31,7 +31,7 @@ object PlayerHandler: PairgothApiHandler {
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 id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
val player = tournament.players[id] ?: badRequest("invalid player id")
@@ -46,7 +46,7 @@ object PlayerHandler: PairgothApiHandler {
if (round <= tournament.lastRound()) {
val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) }
if (playing.contains(id)) {
throw badRequest("player is playing in round #$round")
badRequest("player is playing in round #$round")
}
}
}

View File

@@ -18,7 +18,7 @@ object ResultsHandler: PairgothApiHandler {
return games.map { it.toJson() }.toJsonArray()
}
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
val payload = getObjectPayload(request)

View File

@@ -4,76 +4,35 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray
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.MacMahon
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.adjustedTime
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.time.format.DateTimeFormatter
import javax.servlet.http.HttpServletRequest
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.toJson
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.WebappManager
import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat
import java.text.Normalizer
import java.util.*
import kotlin.collections.ArrayList
object StandingsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
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 sortedMap = sortedPairables.associateBy {
it.getID()!!
}
val sortedPairables = tournament.getSortedPairables(round, includePreliminary)
tournament.populateFrozenStandings(sortedPairables, round)
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 accept = acceptHeader?.substringBefore(";")
val acceptEncoding = acceptHeader?.substringAfter(";charset=", "utf-8") ?: "utf-8"
@@ -91,6 +50,9 @@ object StandingsHandler: PairgothApiHandler {
response.contentType = "text/plain;charset=${encoding}"
val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria)
if (!neededCriteria.contains(NBW)) neededCriteria.add(NBW)
if (neededCriteria.first() == SCOREX) {
neededCriteria.add(1, MMS)
}
exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer)
writer.flush()
return null
@@ -161,13 +123,18 @@ ${
"${
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, ' ')
} ${
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, ' ') }
} ${
@@ -181,6 +148,24 @@ ${
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) {
val version = WebappManager.properties.getProperty("version")!!
val ret =
@@ -209,14 +194,14 @@ ${
"${
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, ' ')
} ${
player.getString("ffg") ?: " "
} ${
if (player.getString("country") == "FR")
(player.getString("club") ?: "").padEnd(4).take(4)
(player.getString("club") ?: "").toSnake().padEnd(4).take(4)
else
(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 frDate: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
}

View File

@@ -29,7 +29,7 @@ object TeamHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to team.id)
}
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json {
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
if (tournament !is TeamTournament) badRequest("tournament is not a team tournament")
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")

View File

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

View File

@@ -2,6 +2,7 @@ package org.jeudego.pairgoth.ext
import jakarta.xml.bind.JAXBContext
import jakarta.xml.bind.JAXBElement
import org.apache.commons.text.StringEscapeUtils
import java.time.LocalDate
import org.jeudego.pairgoth.model.*
import org.jeudego.pairgoth.opengotha.TournamentType
@@ -35,7 +36,8 @@ object OpenGotha {
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("_", "")
@@ -225,7 +227,7 @@ object OpenGotha {
player as Player
}.joinToString("\n") { player ->
"""<Player agaExpirationDate="" agaId="" club="${
player.club
player.club.escapeXML()
}" country="${
player.country
}" egfPin="${
@@ -233,11 +235,11 @@ object OpenGotha {
}" ffgLicence="${
player.externalIds[DatabaseId.FFG] ?: ""
}" ffgLicenceStatus="" firstName="${
player.firstname
player.firstname.escapeXML()
}" grade="${
player.displayRank()
}" name="${
player.name
player.name.escapeXML()
}" participating="${
(1..20).map {
if (player.skip.contains(it)) 0 else 1
@@ -255,7 +257,6 @@ object OpenGotha {
}
</Players>
<Games>
// TODO - table number is not any more kinda random like this
${(1..tournament.lastRound()).map { tournament.games(it) }.flatMapIndexed { index, games ->
games.values.mapNotNull { game ->
if (game.black == 0 || game.white == 0) null
@@ -264,9 +265,11 @@ object OpenGotha {
}.joinToString("\n") { (round, game) ->
"""<Game blackPlayer="${
(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) {
Game.Result.UNKNOWN, Game.Result.CANCELLED -> "RESULT_UNKNOWN"
Game.Result.BLACK -> "RESULT_BLACKWINS"
@@ -281,7 +284,7 @@ object OpenGotha {
game.table
}" whitePlayer="${
(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>
<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.JAPANESE -> "STDBYOYOMI"
TimeSystem.TimeSystemType.CANADIAN -> "CANBYOYOMI"
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(
if (tournament.pairing is MacMahon) tournament.pairing.mmBar else 8
).uppercase(Locale.ROOT)
@@ -321,18 +339,94 @@ object OpenGotha {
(tournament.pairing.pairingParams.main.mmsValueAbsent * 2).roundToInt()
}" genMMS2ValueBye="2" genMMZero="30K" genNBW2ValueAbsent="0" genNBW2ValueBye="2" genRoundDownNBWMMS="${
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}"/>
<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)}"/>
}" komi="${
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>
<PlacementCriteria>
${
(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>
</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"/>
<PublishParameterSet exportToLocalFile="true" htmlAutoScroll="false" print="false"/>
</TournamentParameterSet>

View File

@@ -11,8 +11,10 @@ data class Game(
var black: ID,
var handicap: Int = 0,
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 {}
enum class Result(val symbol: Char) {
UNKNOWN('?'),
@@ -36,15 +38,17 @@ data class Game(
// serialization
fun Game.toJson() = Json.Object(
fun Game.toJson() = Json.MutableObject(
"id" to id,
"t" to table,
"w" to white,
"b" to black,
"h" to handicap,
"r" to "${result.symbol}",
"dd" to drawnUpDown
)
"r" to "${result.symbol}"
).also { game ->
if (drawnUpDown != 0) game["dd"] = drawnUpDown
if (forcedTable) game["ft"] = true
}
fun Game.Companion.fromJson(json: Json.Object) = Game(
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"),
handicap = json.getInt("h") ?: 0,
result = json.getChar("r")?.let { Game.Result.fromSymbol(it) } ?: UNKNOWN,
drawnUpDown = json.getInt("dd") ?: 0
drawnUpDown = json.getInt("dd") ?: 0,
forcedTable = json.getBoolean("ft") ?: false
)

View File

@@ -12,6 +12,7 @@ enum class Criterion {
MMS, // Macmahon score
STS, // Strasbourg 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
SOSWM1, //-1

View File

@@ -6,12 +6,12 @@ import com.republicate.kson.toJsonArray
//import kotlinx.datetime.LocalDate
import java.time.LocalDate
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
import org.jeudego.pairgoth.pairing.solver.SwissSolver
import org.jeudego.pairgoth.api.ApiHandler.Companion.logger
import org.jeudego.pairgoth.store.nextPlayerId
import org.jeudego.pairgoth.store.nextTournamentId
import kotlin.math.max
import java.util.*
import java.util.regex.Pattern
import kotlin.math.roundToInt
sealed class Tournament <P: Pairable>(
@@ -30,7 +30,8 @@ sealed class Tournament <P: Pairable>(
val pairing: Pairing,
val rules: Rules = Rules.FRENCH,
val gobanSize: Int = 19,
val komi: Double = 7.5
val komi: Double = 7.5,
val tablesExclusion: MutableList<String> = mutableListOf()
) {
companion object {}
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>()
val pairables: Map<ID, Pairable> get() = _pairables
// frozen standings
var frozen: Json.Array? = null
// pairing
fun pair(round: Int, pairables: List<Pairable>): List<Game> {
// Minimal check on round number.
@@ -113,11 +117,17 @@ sealed class Tournament <P: Pairable>(
}
}
fun usedTables(round: Int): BitSet =
games(round).values.map { it.table }.fold(BitSet()) { acc, table ->
fun usedTables(round: Int): BitSet {
val assigned = games(round).values.map { it.table }.fold(BitSet()) { acc, table ->
acc.set(table)
acc
}
val excluded = excludedTables(round)
for (table in excluded) {
assigned.set(table)
}
return assigned
}
private fun defaultGameOrderBy(game: Game): Int {
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 {
var changed = false
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) {
++nextTable
while (excludedAndForced.contains(nextTable)) ++nextTable
}
if (game.table != 0) {
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)
)
}.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
@@ -170,8 +206,9 @@ class StandardTournament(
pairing: Pairing,
rules: Rules = Rules.FRENCH,
gobanSize: Int = 19,
komi: Double = 7.5
): Tournament<Player>(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) {
komi: Double = 7.5,
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
}
@@ -192,8 +229,9 @@ class TeamTournament(
pairing: Pairing,
rules: Rules = Rules.FRENCH,
gobanSize: Int = 19,
komi: Double = 7.5
): Tournament<TeamTournament.Team>(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) {
komi: Double = 7.5,
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 {
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,
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
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
TeamTournament(
@@ -286,7 +325,8 @@ fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament<*>? = n
gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19,
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
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 ->
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)
}
}
(json["games"] as Json.Array?)?.forEachIndexed { i, arr ->
json.getArray("games")?.forEachIndexed { i, arr ->
val round = i + 1
val tournamentGames = tournament.games(round)
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)
}
}
json.getArray("frozen")?.also {
tournament.frozen = it
}
return tournament
}
@@ -327,7 +370,14 @@ fun Tournament<*>.toJson() = Json.MutableObject(
"timeSystem" to timeSystem.toJson(),
"rounds" to rounds,
"pairing" to pairing.toJson()
)
).also { tour ->
if (tablesExclusion.isNotEmpty()) {
tour["tablesExclusion"] = tablesExclusion.toJsonArray()
}
if (frozen != null) {
tour["frozen"] = frozen
}
}
fun Tournament<*>.toFullJson(): Json.Object {
val json = toJson()
@@ -336,5 +386,11 @@ fun Tournament<*>.toFullJson(): Json.Object {
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() } });
if (tablesExclusion.isNotEmpty()) {
json["tablesExclusion"] = tablesExclusion.toJsonArray()
}
if (frozen != null) {
json["frozen"] = frozen
}
return json
}

View File

@@ -13,6 +13,7 @@ abstract class BasePairingHelper(
) {
abstract val scores: Map<ID, Pair<Double, Double>>
abstract val scoresX: Map<ID, Double>
val historyHelper =
if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(
history
@@ -47,7 +48,7 @@ abstract class BasePairingHelper(
// Decide each pairable group based on the main criterion
protected val groupsCount get() = 1 + (mainLimits.second - mainLimits.first).toInt()
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)

View File

@@ -3,7 +3,10 @@ package org.jeudego.pairgoth.pairing
import org.jeudego.pairgoth.model.*
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
val allPairables = history.flatten()
@@ -24,6 +27,12 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
scoresGetter()
}
val scoresX by lazy {
scoresGetter().mapValues { entry ->
entry.value.first + (wins[entry.key] ?: 0.0)
}
}
// Generic helper functions
open fun playedTogether(p1: Pairable, p2: Pairable) = paired.contains(Pair(p1.id, p2.id))
open fun colorBalance(p: Pairable) = colorBalance[p.id]

View File

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

View File

@@ -33,6 +33,15 @@ class MacMahonSolver(round: Int,
}
}
override val scoresX: Map<ID, Double> by lazy {
require (mmBar > mmFloor) { "MMFloor is higher than MMBar" }
pairablesMap.mapValues {
it.value.let { pairable ->
roundScore(pairable.mmBase + pairable.nbW)
}
}
}
override fun computeWeightForBye(p: Pairable): Double{
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
// mms: current Mac-Mahon score of the pairable
val Pairable.mms: Double get() = scores[id]?.second ?: 0.0
val Pairable.scoreX: Double get() = scoresX[id] ?: 0.0
// CB TODO - configurable criteria
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 fun evalCriterion(pairable: Pairable, criterion: Criterion) = when (criterion) {
Criterion.MMS -> pairable.mms
Criterion.SCOREX -> pairable.scoreX
Criterion.SOSM -> pairable.sos
Criterion.SOSOSM -> pairable.sosos
Criterion.SOSMM1 -> pairable.sosm1

View File

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

View File

@@ -235,7 +235,7 @@ class ApiServlet: HttpServlet() {
// 2) there will be other content types: .tou, .h9, .html
if (!isJson(accept) &&
(!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(
HttpServletResponse.SC_BAD_REQUEST,

View File

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

View File

@@ -162,7 +162,7 @@ class BasicTests: TestBase() {
assertEquals(aTournamentID, resp.getInt("id"), "First tournament should have id #$aTournamentID")
// 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
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 ->
map["stats"] = Json.Array(
Json.Object("participants" to 0, "paired" to 0, "games" to 0, "ready" to 0),
@@ -170,7 +170,7 @@ class BasicTests: TestBase() {
)
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
@@ -203,8 +203,8 @@ class BasicTests: TestBase() {
var games = TestAPI.post("/api/tour/$aTournamentID/pair/1", Json.Array("all")).asArray()
aTournamentGameID = (games[0] as Json.Object).getInt("id")
val possibleResults = setOf(
"""[{"id":$aTournamentGameID,"t":1,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"?","dd":0}]""",
"""[{"id":$aTournamentGameID,"t":1,"w":$anotherPlayerID,"b":$aPlayerID,"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":"?"}]"""
)
assertTrue(possibleResults.contains(games.toString()), "pairing differs")
games = TestAPI.get("/api/tour/$aTournamentID/res/1").asArray()!!
@@ -219,8 +219,8 @@ class BasicTests: TestBase() {
assertTrue(resp.getBoolean("success") == true, "expecting success")
val games = TestAPI.get("/api/tour/$aTournamentID/res/1")
val possibleResults = setOf(
"""[{"id":$aTournamentGameID,"t":1,"w":$aPlayerID,"b":$anotherPlayerID,"h":0,"r":"b","dd":0}]""",
"""[{"id":$aTournamentGameID,"t":1,"w":$anotherPlayerID,"b":$aPlayerID,"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"}]"""
)
assertTrue(possibleResults.contains(games.toString()), "results differ")
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -5,7 +5,7 @@
<groupId>org.jeudego.pairgoth</groupId>
<artifactId>engine-parent</artifactId>
<version>0.14</version>
<version>0.15</version>
<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 -->

View File

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

View File

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

View File

@@ -281,8 +281,13 @@
vertical-align: baseline;
}
.ui.striped.table>tbody>tr:nth-child(2n),.ui.striped.table>tr:nth-child(2n) {
background-color: rgba(0,0,50,.1)
.ui.striped.table > tbody > tr:nth-child(2n), .ui.striped.table > tr:nth-child(2n) {
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 {
@@ -303,7 +308,7 @@
}
.hidden {
display: none;
display: none !important;
}
.roundbox {
@@ -518,6 +523,21 @@
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 {
body {
@@ -545,7 +565,8 @@
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;
}
@@ -675,9 +696,10 @@
display: none;
}
#players-list tr > :first-child {
display: none;
}
/* should final/preliminary column be printed? */
/* #players-list tr > :first-child { */
/* display: none; */
/* } */
#players-list #players .participation .ui.label {
background: none;
@@ -726,6 +748,9 @@
#standings-table {
font-size: 0.70rem;
}
.title-popup {
display: none;
}
}
}

View File

@@ -87,6 +87,8 @@
flex-flow: row wrap;
justify-content: space-between;
margin: 0 1em;
align-items: baseline;
gap: 0.5em;
}
#players-list {
@@ -377,7 +379,7 @@
gap: 1em;
max-width: max(10em, 20vw);
}
#unpairables {
#unpairables, #previous_games {
display: flex;
flex-flow: column nowrap;
min-height: 10vh;
@@ -405,6 +407,18 @@
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-filter {
@@ -472,6 +486,23 @@
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 {
margin-top: 0.2em;
.step {

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,31 @@
let focused = undefined;
function pair(parts) {
let doWork = () => {
api.postJson(`tour/${tour_id}/pair/${activeRound}`, parts)
.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) {
@@ -19,7 +38,14 @@ function unpair(games) {
}
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 => {
if (rst !== 'error') {
document.location.reload();
@@ -28,6 +54,7 @@ function renumberTables() {
}
function editGame(game) {
// CB TODO - those should be data attributes of the parent game tag
let t = game.find('.table');
let w = game.find('.white');
let b = game.find('.black');
@@ -35,6 +62,7 @@ function editGame(game) {
let form = $('#pairing-form')[0];
form.val('id', game.data('id'));
form.val('prev-table', t.data('value'));
form.val('t', t.data('value'));
form.val('w', w.data('id'));
$('#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(()=>{
// note - this handler is also in use for lists on Mac Mahon super groups and teams pages
$('.listitem').on('click', e => {
let listitem = e.target.closest('.listitem');
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 to = listitem.index('.listitem');
if (from > to) {
@@ -102,10 +156,12 @@ onLoad(()=>{
if (e.detail === 1) {
// single click
focused = listitem.toggleClass('selected').attr('draggable', listitem.hasClass('selected'));
if (box.getAttribute('id') === 'pairables') showOpponents(focused)
} else if (listitem.closest('#pairing-lists')) {
// on pairing page
if (listitem.closest('#paired')) {
// double click
hideOpponents()
focused = listitem.attr('draggable', listitem.hasClass('selected'));
editGame(focused);
} else if (listitem.closest('#pairables')) {
@@ -162,6 +218,12 @@ onLoad(()=>{
b: form.val('b'),
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)
.then(game => {
if (game !== 'error') {
@@ -169,10 +231,11 @@ onLoad(()=>{
}
});
});
$('.multi-select').on('dblclick', e => {
let box = e.target.closest('.multi-select');
document.on('dblclick', e => {
if (!e.target.closest('.listitem')) {
box.find('.listitem').removeClass('selected');
$('.listitem').removeClass('selected');
focused = undefined;
hideOpponents()
}
});
$('#update-pairable').on('click', e => {

View File

@@ -70,6 +70,13 @@ onLoad(()=>{
let newResult = results[(index + 1)%results.length];
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 => {
let filter = $('#results-filter input')[0];
filter.checked = !filter.checked;

View File

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

View File

@@ -28,12 +28,18 @@
#end
#set($games = $utils.removeBye($roundPairing.games))
#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)
#if($j < $games.size())
#set($game = $games[$j])
#set($white = $pmap[$game.w])
#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)
<div class="page">
#end
@@ -75,7 +81,6 @@
</div>
#end
#end
#end
</div>
<script type="text/javascript">
onLoad(() => {

View File

@@ -22,6 +22,14 @@
<button class="ui floating choose-round next-round button">&raquo;</button>
</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-left">
<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>
#end
</div>
<div id="previous_games" class="hidden multi-select">
</div>
</div>
<div id="pairing-right">
<div class="pairing-buttons">
@@ -87,7 +98,7 @@
#set($white = $pmap[$game.w])
#set($black = $pmap[$game.b])
<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($black)${black.name} ${black.firstname} (#rank($black.rank), $black.country $black.club)#{else}BIP#end</td>
<td>${game.h}</td>
@@ -103,6 +114,7 @@
<div class="popup-body">
<form id="pairing-form" class="ui form edit">
<input type="hidden" name="id"/>
<input type="hidden" name="prev-table"/>
<div class="popup-content">
<div class="inline fields">
<div class="field">
@@ -176,3 +188,8 @@
</form>
</div>
</div>
## For dynamic texts to be translated, they must be somewhere in the html source.
## TODO - gather all "text only" nodes like this somewhere
<div id="previous_games_prefix" class="hidden">Games of </div>
<div id="previous_games_postfix" class="hidden"></div>

View File

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

View File

@@ -6,6 +6,26 @@
#set($pmap = $utils.toMap($teams))
#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 id="reg-view">
<div id="list-header">
@@ -33,14 +53,18 @@
<th>First name</th>
<th>Country</th>
<th>Club</th>
##if($tour.country == 'FR')
## <th>FFG</th>
##else
#if($utils.showRatings('egf', $tour.country.toLowerCase()))
<th>PIN</th>
##end
#end
#if($utils.showRatings('ffg', $tour.country.toLowerCase()))
<th>FFG</th>
#end
<th>Rank</th>
## TableSort bug which inverts specified sort...
<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>
</thead>
<tbody>
@@ -54,13 +78,18 @@
<td>$part.firstname</td>
<td>$part.country.toUpperCase()</td>
<td>$part.club</td>
#if($tour.country == 'FR')
<td>$!part.ffg</td>
#else
<td>$!part.egf</td>
#if($utils.showRatings('egf', $tour.country.toLowerCase()))
<td>$!part.egf </td>
#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>
#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">
<div class="participation">
#foreach($round in [1..$tour.rounds])
@@ -108,7 +137,15 @@
</div>
</div>
#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">
<input id="needle" name="needle" type="text" placeholder="Search..." spellcheck="false">
<i id="clear-search" class="clickable close icon"></i>
@@ -125,6 +162,7 @@
</div>
</div>
*#
#if($utils.displayRatings('egf', $tour.country.toLowerCase()))
<div class="two wide centered field">
<div class="toggle" title="${utils.ratingsDates.egf|'no egf ratings'}">
<input id="egf" name="egf" type="checkbox" checked value="true"/>
@@ -134,6 +172,8 @@
<label>EGF</label>
</div>
</div>
#end
#if($utils.displayRatings('ffg', $tour.country.toLowerCase()))
<div class="two wide centered field">
<div class="toggle" title="${utils.ratingsDates.ffg|'no ffg ratings'}">
<input id="ffg" name="ffg" type="checkbox" checked value="true"/>
@@ -143,8 +183,9 @@
<label>FFG</label>
</div>
</div>
#end
<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"/>
<div class="search-param checkbox">
<div class="circle"></div>
@@ -249,21 +290,6 @@
</div>
</div>
#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 class="popup-body">
<div class="popup-content">

View File

@@ -20,6 +20,7 @@
<th data-sort-method="number">table</th>
<th>white</th>
<th>black</th>
<th>hd</th>
<th>result</th>
</thead>
<tbody>
@@ -32,6 +33,7 @@
<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="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>
</tr>
#end

View File

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

View File

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

View File

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