Merge branch 'master' into 'translations'

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

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.Game
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.toID import org.jeudego.pairgoth.model.toID
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
@@ -38,6 +39,7 @@ object PairingHandler: PairgothApiHandler {
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played") if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
val payload = getArrayPayload(request) val payload = getArrayPayload(request)
if (payload.isEmpty()) badRequest("nobody to pair") if (payload.isEmpty()) badRequest("nobody to pair")
// CB TODO - change convention to empty array for all players
val allPlayers = payload.size == 1 && payload[0] == "all" val allPlayers = payload.size == 1 && payload[0] == "all"
//if (!allPlayers && tournament.pairing.type == PairingType.SWISS) badRequest("Swiss pairing requires all pairable players") //if (!allPlayers && tournament.pairing.type == PairingType.SWISS) badRequest("Swiss pairing requires all pairable players")
val playing = (tournament.games(round).values).flatMap { val playing = (tournament.games(round).values).flatMap {
@@ -57,16 +59,18 @@ object PairingHandler: PairgothApiHandler {
} ?: badRequest("invalid pairable id: #$id") } ?: badRequest("invalid pairable id: #$id")
} }
val games = tournament.pair(round, pairables) val games = tournament.pair(round, pairables)
val ret = games.map { it.toJson() }.toJsonArray() val ret = games.map { it.toJson() }.toJsonArray()
tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret)) tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret))
return ret return ret
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json { override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
// only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...) // only allow last round (if players have not been paired in the last round, it *may* be possible to be more laxist...)
if (round != tournament.lastRound()) badRequest("cannot edit pairings in other rounds but the last") // TODO - check in next line commented out: following founds can exist, but be empty...
// if (round != tournament.lastRound()) badRequest("cannot edit pairings in other rounds but the last")
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
if (payload.containsKey("id")) { if (payload.containsKey("id")) {
val gameId = payload.getInt("id") ?: badRequest("invalid game id") val gameId = payload.getInt("id") ?: badRequest("invalid game id")
@@ -97,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")

View File

@@ -31,7 +31,7 @@ object PlayerHandler: PairgothApiHandler {
return Json.Object("success" to true, "id" to player.id) return Json.Object("success" to true, "id" to player.id)
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json { override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector") val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
val player = tournament.players[id] ?: badRequest("invalid player id") val player = tournament.players[id] ?: badRequest("invalid player id")
@@ -46,7 +46,7 @@ object PlayerHandler: PairgothApiHandler {
if (round <= tournament.lastRound()) { if (round <= tournament.lastRound()) {
val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) } val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) }
if (playing.contains(id)) { if (playing.contains(id)) {
throw badRequest("player is playing in round #$round") badRequest("player is playing in round #$round")
} }
} }
} }

View File

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

View File

@@ -4,76 +4,35 @@ import com.republicate.kson.Json
import com.republicate.kson.toJsonArray import com.republicate.kson.toJsonArray
import org.jeudego.pairgoth.model.Criterion import org.jeudego.pairgoth.model.Criterion
import org.jeudego.pairgoth.model.Criterion.* import org.jeudego.pairgoth.model.Criterion.*
import org.jeudego.pairgoth.model.Game.Result.*
import org.jeudego.pairgoth.model.ID import org.jeudego.pairgoth.model.ID
import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Pairable
import org.jeudego.pairgoth.model.PairingType import org.jeudego.pairgoth.model.PairingType
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.adjustedTime import org.jeudego.pairgoth.model.adjustedTime
import org.jeudego.pairgoth.model.displayRank import org.jeudego.pairgoth.model.displayRank
import org.jeudego.pairgoth.model.getID
import org.jeudego.pairgoth.model.historyBefore
import org.jeudego.pairgoth.pairing.HistoryHelper
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
import java.io.PrintWriter import java.io.PrintWriter
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
import kotlin.math.max
import kotlin.math.min
import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.* import org.jeudego.pairgoth.model.TimeSystem.TimeSystemType.*
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.server.Event
import org.jeudego.pairgoth.server.WebappManager import org.jeudego.pairgoth.server.WebappManager
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.text.DecimalFormat import java.text.DecimalFormat
import java.text.Normalizer
import java.util.*
import kotlin.collections.ArrayList
object StandingsHandler: PairgothApiHandler { object StandingsHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request) val tournament = getTournament(request)
val round = getSubSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("invalid round number") val round = getSubSelector(request)?.toIntOrNull() ?: tournament.rounds
val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false
val sortedPairables = tournament.getSortedPairables(round) val sortedPairables = tournament.getSortedPairables(round, includePreliminary)
val sortedMap = sortedPairables.associateBy { tournament.populateFrozenStandings(sortedPairables, round)
it.getID()!!
}
for (r in 1..round) {
tournament.games(r).values.forEach { game ->
val white = if (game.white != 0) sortedMap[game.white] else null
val black = if (game.black != 0) sortedMap[game.black] else null
val whiteNum = white?.getInt("num") ?: 0
val blackNum = black?.getInt("num") ?: 0
val whiteColor = if (black == null) "" else "w"
val blackColor = if (white == null) "" else "b"
val handicap = if (game.handicap == 0) "" else "${game.handicap}"
assert(white != null || black != null)
if (white != null) {
val mark = when (game.result) {
UNKNOWN -> "?"
BLACK, BOTHLOOSE -> "-"
WHITE, BOTHWIN -> "+"
JIGO, CANCELLED -> "="
}
val results = white.getArray("results") as Json.MutableArray
results[r - 1] =
if (blackNum == 0) "0$mark"
else "$blackNum$mark/$whiteColor$handicap"
}
if (black != null) {
val mark = when (game.result) {
UNKNOWN -> "?"
BLACK, BOTHWIN -> "+"
WHITE, BOTHLOOSE -> "-"
JIGO, CANCELLED -> "="
}
val results = black.getArray("results") as Json.MutableArray
results[r - 1] =
if (whiteNum == 0) "0$mark"
else "$whiteNum$mark/$blackColor$handicap"
}
}
}
val acceptHeader = request.getHeader("Accept") as String? val acceptHeader = request.getHeader("Accept") as String?
val accept = acceptHeader?.substringBefore(";") val accept = acceptHeader?.substringBefore(";")
val acceptEncoding = acceptHeader?.substringAfter(";charset=", "utf-8") ?: "utf-8" val acceptEncoding = acceptHeader?.substringAfter(";charset=", "utf-8") ?: "utf-8"
@@ -91,6 +50,9 @@ object StandingsHandler: PairgothApiHandler {
response.contentType = "text/plain;charset=${encoding}" response.contentType = "text/plain;charset=${encoding}"
val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria) val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria)
if (!neededCriteria.contains(NBW)) neededCriteria.add(NBW) if (!neededCriteria.contains(NBW)) neededCriteria.add(NBW)
if (neededCriteria.first() == SCOREX) {
neededCriteria.add(1, MMS)
}
exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer) exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer)
writer.flush() writer.flush()
return null return null
@@ -161,13 +123,18 @@ ${
"${ "${
player.getString("num")!!.padStart(4, ' ') player.getString("num")!!.padStart(4, ' ')
} ${ } ${
"${player.getString("name")} ${player.getString("firstname") ?: ""}".padEnd(30, ' ').take(30) "${
player.getString("name")?.toSnake(true)
} ${
player.getString("firstname")?.toSnake() ?: ""
}".padEnd(30, ' ').take(30)
} ${ } ${
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ') displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
} ${ } ${
player.getString("country")?.uppercase() ?: "" player.getString("country")?.uppercase() ?: ""
} ${ } ${
(player.getString("club") ?: "").padStart(4).take(4) (player.getString("club") ?: "").toSnake().padStart(4).take(4)
} ${ } ${
criteria.joinToString(" ") { numFormat.format(player.getDouble(it.name)!!).let { if (it.contains('.')) it else "$it " }.padStart(7, ' ') } criteria.joinToString(" ") { numFormat.format(player.getDouble(it.name)!!).let { if (it.contains('.')) it else "$it " }.padStart(7, ' ') }
} ${ } ${
@@ -181,6 +148,24 @@ ${
writer.println(ret) writer.println(ret)
} }
private fun String.toSnake(upper: Boolean = false): String {
val sanitized = sanitizeISO()
val parts = sanitized.trim().split(Regex("(?:\\s|\\xA0)+"))
val snake = parts.joinToString("_") { part ->
if (upper) part.uppercase(Locale.ROOT)
else part.capitalize()
}
return snake
}
private fun String.sanitizeISO(): String {
val ret = Normalizer.normalize(this, Normalizer.Form.NFD)
return ret.replace(Regex("\\p{M}"), "")
// some non accented letters give problems in ISO, there may be other
.replace('Ð', 'D')
.replace('ø', 'o')
}
private fun exportToFFGFormat(tournament: Tournament<*>, lines: List<Json.Object>, writer: PrintWriter) { private fun exportToFFGFormat(tournament: Tournament<*>, lines: List<Json.Object>, writer: PrintWriter) {
val version = WebappManager.properties.getProperty("version")!! val version = WebappManager.properties.getProperty("version")!!
val ret = val ret =
@@ -209,14 +194,14 @@ ${
"${ "${
player.getString("num")!!.padStart(4, ' ') player.getString("num")!!.padStart(4, ' ')
} ${ } ${
"${player.getString("name")} ${player.getString("firstname") ?: ""}".padEnd(24, ' ').take(24) "${player.getString("name")?.toSnake(true)} ${player.getString("firstname")?.toSnake() ?: ""}".padEnd(24, ' ').take(24)
} ${ } ${
displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ') displayRank(player.getInt("rank")!!).uppercase().padStart(3, ' ')
} ${ } ${
player.getString("ffg") ?: " " player.getString("ffg") ?: " "
} ${ } ${
if (player.getString("country") == "FR") if (player.getString("country") == "FR")
(player.getString("club") ?: "").padEnd(4).take(4) (player.getString("club") ?: "").toSnake().padEnd(4).take(4)
else else
(player.getString("country") ?: "").padEnd(4).take(4) (player.getString("country") ?: "").padEnd(4).take(4)
} ${ } ${
@@ -264,6 +249,14 @@ ${
} }
} }
override fun put(request: HttpServletRequest, response: HttpServletResponse): Json? {
val tournament = getTournament(request)
val sortedPairables = tournament.getSortedPairables(tournament.rounds)
tournament.frozen = sortedPairables.toJsonArray()
tournament.dispatchEvent(Event.TournamentUpdated, request, tournament.toJson())
return Json.Object("status" to "ok")
}
private val numFormat = DecimalFormat("###0.#") private val numFormat = DecimalFormat("###0.#")
private val frDate: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy") private val frDate: DateTimeFormatter = DateTimeFormatter.ofPattern("dd/MM/yyyy")
} }

View File

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

View File

@@ -2,9 +2,11 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import com.republicate.kson.toJsonObject import com.republicate.kson.toJsonObject
import com.republicate.kson.toMutableJsonObject
import org.jeudego.pairgoth.api.ApiHandler.Companion.PAYLOAD_KEY import org.jeudego.pairgoth.api.ApiHandler.Companion.PAYLOAD_KEY
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.ext.OpenGotha import org.jeudego.pairgoth.ext.OpenGotha
import org.jeudego.pairgoth.model.BaseCritParams
import org.jeudego.pairgoth.model.TeamTournament import org.jeudego.pairgoth.model.TeamTournament
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
@@ -34,6 +36,7 @@ object TournamentHandler: PairgothApiHandler {
// additional attributes for the webapp // additional attributes for the webapp
json["stats"] = tour.stats() json["stats"] = tour.stats()
json["teamSize"] = tour.type.playersNumber json["teamSize"] = tour.type.playersNumber
json["frozen"] = tour.frozen != null
} }
} }
} ?: badRequest("no tournament with id #${id}") } ?: badRequest("no tournament with id #${id}")
@@ -61,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+)?)*$")
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,6 +87,8 @@
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
margin: 0 1em; margin: 0 1em;
align-items: baseline;
gap: 0.5em;
} }
#players-list { #players-list {
@@ -377,7 +379,7 @@
gap: 1em; gap: 1em;
max-width: max(10em, 20vw); max-width: max(10em, 20vw);
} }
#unpairables { #unpairables, #previous_games {
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
min-height: 10vh; min-height: 10vh;
@@ -405,6 +407,18 @@
margin-top: 0.2em; margin-top: 0.2em;
} }
.bottom-pairing-actions {
margin-top: 0.2em;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
gap: 0.2em;
}
.tables-exclusion {
margin-top: 0.2em;
}
/* results section */ /* results section */
#results-filter { #results-filter {
@@ -471,6 +485,23 @@
#standings-container { #standings-container {
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;

View File

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

View File

@@ -1,270 +1,278 @@
(docker required) (Docker 필요) (docker required) (Docker 필요)
(java required) (Java 필요) (java required) (Java 필요)
, allowing you to tweak it in any possible way. Be sure to contribute back your enhancements! 하에 배포되어 자유롭게 수정할 수 있습니다. 개선 및 건의 사항을 전달해 주시면 감사하겠습니다! , allowing you to tweak it in any possible way. Be sure to contribute back your enhancements! 하에 배포되어 자유롭게 수정할 수 있습니다. 개선 및 건의 사항을 전달해 주시면 감사하겠습니다!
, the well known pairing system software developed by 의 후속작으로, , the well known pairing system software developed by 의 후속작으로,
, your Go Pairing Engine! 에 오신 것을 환영합니다! , your Go Pairing Engine! 에 오신 것을 환영합니다!
, last modified , 마지막 수정 , last modified , 마지막 수정
1st round seeding 1라운드 시드 배정 1st round seeding 1라운드 시드 배정
: If you prefer convenience, you can simply use the : 프랑스 바둑 연맹이 제공하는 : If you prefer convenience, you can simply use the : 프랑스 바둑 연맹이 제공하는
: This mode allows you to run : 이 모드는 로컬 컴퓨터에서 : This mode allows you to run : 이 모드는 로컬 컴퓨터에서
: This mode is the best suited for big Go events like congresses, it allows to register players, enter results and manage pairing from several workstations at once. : 이 모드는 큰 바둑 대회인 콩그레스와 같은 행사에 가장 적합합니다. 여러 컴퓨터에서 선수 등록, 결과 입력 및 매칭 관리를 할 수 있습니다. : This mode is the best suited for big Go events like congresses, it allows to register players, enter results and manage pairing from several workstations at once. : 이 모드는 큰 바둑 대회인 콩그레스와 같은 행사에 가장 적합합니다. 여러 컴퓨터에서 선수 등록, 결과 입력 및 매칭 관리를 할 수 있습니다.
: the : : the :
Add player 선수 추가 Add player 선수 추가
Advanced parameters 고급 설정 Advanced parameters 고급 설정
At its core, 핵심적으로, At its core, 핵심적으로,
Browse 파일 선택 Browse 파일 선택
Byo-yomi periods 초읽기 횟수 Byo-yomi periods 초읽기 횟수
Byo-yomi stones 착수 횟수 Byo-yomi stones 착수 횟수
Byo-yomi time 초읽기 시간 Byo-yomi time 초읽기 시간
Canadian byo-yomi 캐나다식 초읽기 Canadian byo-yomi 캐나다식 초읽기
Cancel 취소 Cancel 취소
Change 변경 Change 변경
Chinese rules 중국 규칙 Chinese rules 중국 규칙
Choose format 형식 선택 Choose format 형식 선택
Clone 복제 Clone 복제
Clone example tournament 예제 대회 복제 Clone example tournament 예제 대회 복제
Close 닫기 Close 닫기
Club 소속 Club 소속
Compile from the sources 소스에서 컴파일하기 Compile from the sources 소스에서 컴파일하기
Country 국적 Country 국적
Create 생성 Create 생성
Crit 기준 Crit 기준
Ctr 국적 Ctr 국적
Dates 날짜 Dates 날짜
Delete 삭제 Delete 삭제
Director 진행자 Director 진행자
Download Download
Download _BLANK_WINDOWS Java 포함 Windows용 Download _BLANK_WINDOWS Java 포함 Windows용
Download the standalone web interface module which suits your need, then follow 독립 실행형 웹 인터페이스 모듈을 다운로드하여, Download the standalone web interface module which suits your need, then follow 독립 실행형 웹 인터페이스 모듈을 다운로드하여,
Drop changes? 변경 사항을 폐기하시겠습니까? Drop changes? 변경 사항을 폐기하시겠습니까?
Edit 편집 Edit 편집
Encoding 인코딩 Encoding 인코딩
Enter the magic word 마법의 단어 입력 Enter the magic word 마법의 단어 입력
Example tournament 예제 대회 Example tournament 예제 대회
Exclude table numbers: 테이블 번호 제외: Exclude table numbers: 테이블 번호 제외:
Export 내보내기 Export 내보내기
Export tournament 대회 내보내기 Export tournament 대회 내보내기
Family name 성 Family name 성
Filter 필터 Filter 필터
Filter... 필터링... Filter... 필터링...
Final only 최종 등록 Final only 최종 등록
First name 이름 First name 이름
Fischer timing 피셔 방식 Fischer timing 피셔 방식
French rules 프랑스식 French rules 프랑스식
Given name 이름 Given name 이름
Goban 바둑판 Goban 바둑판
Handicap 핸디캡 Handicap 핸디캡
Hd correction 핸디캡 보정 Hd correction 핸디캡 보정
No hd threshold 핸디캡 임계값 없음 No hd threshold 핸디캡 임계값 없음
How to use How to use
? _BLANK_HOWTOUSE 사용 방법은 ? ? _BLANK_HOWTOUSE 사용 방법은 ?
Import 가져오기 Import 가져오기
Import tournament 대회 가져오기 Import tournament 대회 가져오기
Increment 시간 증가분 Increment 시간 증가분
Individual players 개인 선수 Individual players 개인 선수
Information 정보 Information 정보
Invalid tournament id 유효하지 않은 대회 ID입니다 Invalid tournament id 유효하지 않은 대회 ID입니다
Japanese rules 일본식 Japanese rules 일본식
Komi 덤 Komi 덤
Launch Launch
_BLANK_LAUNCH 실행 _BLANK_LAUNCH 실행
Launch a pairing server 매칭 서버 실행하기 Launch a pairing server 매칭 서버 실행하기
Launch a standalone instance 독립 실행형 인스턴스 실행하기 Launch a standalone instance 독립 실행형 인스턴스 실행하기
Location 장소 Location 장소
Log in 로그인 Log in 로그인
Luc Vannier Luc Vannier
MM bar MM 바 MM bar MM 바
Mac Mahon 맥마흔 Mac Mahon 맥마흔
Mac Mahon groups 맥마흔 그룹 Mac Mahon groups 맥마흔 그룹
MacMahon 맥마흔 MacMahon 맥마흔
Main time 제한 시간 Main time 제한 시간
Max time 최대 시간 Max time 최대 시간
MM floor MM 바닥 MM floor MM 바닥
Name 이름 Name 이름
Nbw 승 Nbw 승
New Tournament 새 대회 New Tournament 새 대회
New tournament 새 대회 New tournament 새 대회
Next rounds seeding 다음 라운드 시드 배정 Next rounds seeding 다음 라운드 시드 배정
OpenGotha / Pairgoth file OpenGotha / Pairgoth 파일 OpenGotha / Pairgoth file OpenGotha / Pairgoth 파일
Pair 매칭하기 Pair 매칭하기
Pair-go tournament 패어바둑 대회 Pair-go tournament 패어바둑 대회
Pairing 매칭 시스템 Pairing 매칭 시스템
Pairings for round 라운드 매칭 Pairings for round 라운드 매칭
Participation 참가 Participation 참가
Preliminary and final 모든 등록 Preliminary and final 모든 등록
Preliminary only 예비 등록 Preliminary only 예비 등록
Publish 게시 Publish 게시
Publish standings 순위 게시 Publish standings 순위 게시
Rank 기력 Rank 기력
Rating 레이팅 Rating 레이팅
Reg 등록 Reg 등록
Register 등록 Register 등록
Registration 등록 Registration 등록
Rengo with 2 players teams 2인 팀 Rengo with 2 players teams 2인 팀
Rengo with 3 players team 3인 팀 Rengo with 3 players team 3인 팀
Renumber 번호 다시 매기기 Renumber 번호 다시 매기기
Required field 필수 항목 Required field 필수 항목
Reset 재설정 Reset 재설정
Results 결과 Results 결과
Results for round 라운드 결과 Results for round 라운드 결과
Round-robin 라운드 로빈 Round-robin 라운드 로빈
Rounds 라운드 Rounds 라운드
Rules 규칙 Rules 규칙
Search... 검색… Search... 검색…
Short name 단축명 Short name 단축명
Since the project is still in beta, the sources are only available to FFG actors. If that's your case, you can access the sources here: Since the project is still in beta, the sources are only available to FFG actors. If that's your case, you can access the sources here:
Split and fold 분할 및 접기 Split and fold 분할 및 접기
Split and random 분할 및 무작위 Split and random 분할 및 무작위
Split and slip 스플릿 앤 슬립 Split and slip 스플릿 앤 슬립
Standard byo-yomi 초읽기 Standard byo-yomi 초읽기
Standings 순위 Standings 순위
Standings after round 라운드 후 순위 Standings after round 라운드 후 순위
Stay in the browser 브라우저에서 사용하기 Stay in the browser 브라우저에서 사용하기
Sudden death 서든 데스 Sudden death 서든 데스
Surround winner's name or ½-½ 승자의 이름 또는 0.5 - 0.5을 둘러싸기 Surround winner's name or ½-½ 승자의 이름 또는 0.5 - 0.5을 둘러싸기
Signature: 서명: Signature: 서명:
Swiss 스위스 Swiss 스위스
Team of 2 individual players 2인 팀 Team of 2 individual players 2인 팀
Team of 3 individual players 3인 팀 Team of 3 individual players 3인 팀
Team of 4 individual players 4인 팀 Team of 4 individual players 4인 팀
Team of 5 individual players 5인 팀 Team of 5 individual players 5인 팀
That's the best option if you feel more comfortable when running locally or whenever you want to be able to do the pairing without internet. Pairgoth will launch a local web server on port 8080 to which you can connect using a browser. 로컬에서 실행하는 것이 더 편하거나 인터넷 없이 매칭을 하고 싶을 때 가장 좋은 옵션입니다. Pairgoth는 포트 8080에서 로컬 웹 서버를 실행하며, 이를 브라우저를 통해 연결할 수 있습니다. That's the best option if you feel more comfortable when running locally or whenever you want to be able to do the pairing without internet. Pairgoth will launch a local web server on port 8080 to which you can connect using a browser. 로컬에서 실행하는 것이 더 편하거나 인터넷 없이 매칭을 하고 싶을 때 가장 좋은 옵션입니다. Pairgoth는 포트 8080에서 로컬 웹 서버를 실행하며, 이를 브라우저를 통해 연결할 수 있습니다.
Time system 제한 시간 방식 Time system 제한 시간 방식
Tournament director 대회 담당자 Tournament director 대회 담당자
Tournament name 대회 이름 Tournament name 대회 이름
Tournament type 대회 유형 Tournament type 대회 유형
Unpair 매칭 취소 Unpair 매칭 취소
Unregister 등록 해제 Unregister 등록 해제
Unregister this player? 이 선수를 등록 해제하시겠습니까? Unregister this player? 이 선수를 등록 해제하시겠습니까?
Update 업데이트 Update 업데이트
We offer you the flexibility to use 저희는 여러분의 필요에 맞게 We offer you the flexibility to use 저희는 여러분의 필요에 맞게
Welcome to 여러분의 바둑 매칭 엔진 Welcome to 여러분의 바둑 매칭 엔진
What is What is
? _BLANK_WHATIS 란 무엇인가? ? _BLANK_WHATIS 란 무엇인가?
Your feedback is most welcome! 여러분의 피드백은 언제나 환영입니다! Your feedback is most welcome! 여러분의 피드백은 언제나 환영입니다!
and uses the same algorithm and parameters internally, as well as import and export features towards its format. 내부적으로 동일한 알고리즘과 매개변수를 사용하며, 해당 형식으로의 가져오기 및 내보내기 기능을 지원합니다. and uses the same algorithm and parameters internally, as well as import and export features towards its format. 내부적으로 동일한 알고리즘과 매개변수를 사용하며, 해당 형식으로의 가져오기 및 내보내기 기능을 지원합니다.
apache licence 아파치 라이선스 apache licence 아파치 라이선스
black 흑 black 흑
Black 흑 Black 흑
club 소속 club 소속
country 국적 country 국적
d 단 d 단
end date 종료일 end date 종료일
first name 이름 first name 이름
from 부터 from 부터
games ) 판 ) games ) 판 )
h 시간 h 시간
in a way that best suits your needs. Here are your options: 사용할 수 있게 하고자 합니다. 다음은 선택할 수 있는 옵션들입니다: in a way that best suits your needs. Here are your options: 사용할 수 있게 하고자 합니다. 다음은 선택할 수 있는 옵션들입니다:
instance graciously hosted by the French Go Federation. 인스턴스를 편하게 사용하실 수 있습니다. instance graciously hosted by the French Go Federation. 인스턴스를 편하게 사용하실 수 있습니다.
is a Go tournament pairing engine designed to make your tournament experience effortless. 는 여러분의 바둑 대회 경험을 손쉽게 만들어 주는 바둑 대회 매칭 엔진입니다. is a Go tournament pairing engine designed to make your tournament experience effortless. 는 여러분의 바둑 대회 경험을 손쉽게 만들어 주는 바둑 대회 매칭 엔진입니다.
is the successor of 는<a href="http://vannier.info/jeux/accueil.htm">Luc Vannier</a>가 개발한 잘 알려진 매칭 시스템 소프트웨어인 is the successor of 는<a href="http://vannier.info/jeux/accueil.htm">Luc Vannier</a>가 개발한 잘 알려진 매칭 시스템 소프트웨어인
k 급 k 급
last name 성 last name 성
on your local computer. 를 실행할 수 있게 해줍니다. on your local computer. 를 실행할 수 있게 해줍니다.
online tournament 온라인 대회 online tournament 온라인 대회
opengotha OpenGotha opengotha OpenGotha
or 또는 or 또는
pairable players 매칭 가능한 선수 pairable players 매칭 가능한 선수
pairable, 매칭 가능, pairable, 매칭 가능,
pairgoth pairgoth pairgoth pairgoth
pairing system, ideal for championships with no handicap games, as well as the 매칭 시스템과, 일반 대회 및 컵에 더 적합한 pairing system, ideal for championships with no handicap games, as well as the 매칭 시스템과, 일반 대회 및 컵에 더 적합한
pairing system, more suited for classical tournaments and cups. Future versions will support more pairing systems and more features. 매칭 시스템을 지원합니다. 향후 버전에서는 더 많은 매칭 시스템과 기능을 지원할 예정입니다. pairing system, more suited for classical tournaments and cups. Future versions will support more pairing systems and more features. 매칭 시스템을 지원합니다. 향후 버전에서는 더 많은 매칭 시스템과 기능을 지원할 예정입니다.
project is fully open source, and under the very permissive 프로젝트는 완전히 오픈 소스이며, project is fully open source, and under the very permissive 프로젝트는 완전히 오픈 소스이며,
result 결과 result 결과
result sheets 결과 시트 result sheets 결과 시트
sources 소스 sources 소스
sources on github 소스를 확인해 보세요 sources on github 소스를 확인해 보세요
_BLANK_GITHUB Github에서 _BLANK_GITHUB Github에서
standalone, web interface 독립 실행형, 웹 인터페이스 다운로드 standalone, web interface 독립 실행형, 웹 인터페이스 다운로드
standalone, web interface, via docker 독립 실행형, 웹 인터페이스 다운로드, Docker를 통한 실행 standalone, web interface, via docker 독립 실행형, 웹 인터페이스 다운로드, Docker를 통한 실행
start date 시작 날짜 start date 시작 날짜
table 테이블 table 테이블
Table 테이블 Table 테이블
the configuration guide 구성 가이드를 따라하세요 the configuration guide 구성 가이드를 따라하세요
to 까지 to 까지
tournament location 대회 위치 tournament location 대회 위치
unpairable players 매칭 불가능한 선수 unpairable players 매칭 불가능한 선수
unpairable, 매칭 불가능, unpairable, 매칭 불가능,
supports the 는 접바둑이 없는 챔피언십일 경우에 잘 맞는 supports the 는 접바둑이 없는 챔피언십일 경우에 잘 맞는
white 백 white 백
White 백 White 백
white vs. black 백 vs 흑 white vs. black 백 vs 흑
confirmed. 확인됨 confirmed. 확인됨
Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us 이 인스턴스에 로그인하는 것은 프랑스 연맹 관계자와 당사의 재량에 따라 선택된 외부인에게만 허용됩니다. 접근을 요청하려면 Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us 이 인스턴스에 로그인하는 것은 프랑스 연맹 관계자와 당사의 재량에 따라 선택된 외부인에게만 허용됩니다. 접근을 요청하려면
yyyymmdd-city yyyymmdd-도시 yyyymmdd-city yyyymmdd-도시
an email 이메일 an email 이메일
to request an access. 을 보내주세요. to request an access. 을 보내주세요.
(not yet available) (현재 제공되지 않음). (not yet available) (현재 제공되지 않음).
Log in using 사용하여 로그인 Log in using 사용하여 로그인
(reserved to FFG actors) (FFG 관계자에게만 허용) (reserved to FFG actors) (FFG 관계자에게만 허용)
Log in using an email 이메일로 로그인 Log in using an email 이메일로 로그인
password 비밀번호 password 비밀번호
Warning: publishing partial results at round 경고: 라운드에서 부분 결과를 게시합니다 Warning: publishing partial results at round 경고: 라운드에서 부분 결과를 게시합니다
out of 중에서 out of 중에서
For any further help or question, contact us 추가적인 도움이 필요하시거나 질문이 있으시면 For any further help or question, contact us 추가적인 도움이 필요하시거나 질문이 있으시면
by email 이메일 by email 이메일
or 이나 or 이나
on our discord channel 디스코드 on our discord channel 디스코드
. _BLANK_CONTACT 채널을 통해 문의해 주시기 바랍니다. . _BLANK_CONTACT 채널을 통해 문의해 주시기 바랍니다.
standalone installer for Windows with Java included 독립 실행형 설치 프로그램 다운로드 standalone installer for Windows with Java included 독립 실행형 설치 프로그램 다운로드
(please ensure that your Windows user has administrative rights). (Windows 사용자가 관리자 권한을 가지고 있는지 확인하세요). (please ensure that your Windows user has administrative rights). (Windows 사용자가 관리자 권한을 가지고 있는지 확인하세요).
AGA rules 미국바둑협회 규칙 AGA rules 미국바둑협회 규칙
initialized from rating 레이팅 기준으로 초기화 initialized from rating 레이팅 기준으로 초기화
You can join the You can join the
Pairgoth mailing list Pairgoth 메일링 리스트 Pairgoth mailing list Pairgoth 메일링 리스트
to be notified about updates and to discuss the software. 에 등록하여 업데이트 알림을 받고, 소프트웨어에 대해 논의하세요. to be notified about updates and to discuss the software. 에 등록하여 업데이트 알림을 받고, 소프트웨어에 대해 논의하세요.
(give us a star if you have a github account!) (Github 계정이 있으시면 별을 눌러 주세요!). (give us a star if you have a github account!) (Github 계정이 있으시면 별을 눌러 주세요!).
Clear results 결과 지우기 Clear results 결과 지우기
choisir un fichier 파일 선택 choisir un fichier 파일 선택
aucun fichier choisi 선택된 파일 없음 aucun fichier choisi 선택된 파일 없음
Round 라운드 Round 라운드
Participants 참가자 Participants 참가자
participants, 참가자, participants, 참가자,
Paired 매칭됨 Paired 매칭됨
Base parameters 기본 설정 Base parameters 기본 설정
Main parameters 주요 설정 Main parameters 주요 설정
Secondary parameters 부가 설정 Secondary parameters 부가 설정
Geographical parameters 지리적 설정 Geographical parameters 지리적 설정
Handicap parameters 핸디캡 설정 Handicap parameters 핸디캡 설정
deterministic randomness 결정적 무작위성 deterministic randomness 결정적 무작위성
Randomness: 무작위성 <<<<<<< e8943b690eca2a284ab2fabd0d014fb77981af21
none 없음 Randomness: 무작위성
deterministic 결정적 none 없음
non-deterministic 결정 deterministic 결정적
balance white and black 점 차이를 선호 non-deterministic 비결정론적
Round balance white and black 점 차이를 선호
down 내림 =======
up 올림 Randomness 무작위성
NBW/MMS score 라운드 NBW/MMS none 없음
Special Mac Mahon handling for players absent from a round 라운드에 불참한 선수들에게 특별 맥마흔 처리 deterministic 없음
MMS score for non-played rounds: 불참한 라운드의 MMS: non-deterministic 비결정론적
SOS for non-played rounds: 불참한 라운드의 SOS: balance white and black
of player >>>>>>> 5528e07f8e0cdda908847577340c595e9d2df8aa
base MMS 기본 MMS Round
base MMS + rounds/2 기본 MMS + 라운드/2 down 내림
Seeding methods inside groups of same score 동일 점수 그룹 내 시드 배정 방법 up 올림
Apply first seeding method up to round NBW/MMS score 라운드 NBW/MMS
_BLANK_SEEDING 라운드까지 첫 번째 매칭 방법 적용 Special Mac Mahon handling for players absent from a round 라운드에 불참한 선수들에게 특별 맥마흔 처리
First seeding method 첫 번째 시드 배정 방법 MMS score for non-played rounds: 불참한 라운드의 MMS:
Second seeding method 두 번째 시드 배정 방법 SOS for non-played rounds: 불참한 라운드의 SOS:
add a sorting on rating 레이팅 정렬 추가 of player
Draw-up / draw-down between groups of same score 동일 점수 그룹 간의 상위/하위 매칭 base MMS 기본 MMS
try to compensate a previous draw-up/draw-down by a draw-down/draw-up, then 이전의 상위/하위 매칭을 하위/상위 매칭으로 보상 시도한 뒤, base MMS + rounds/2 기본 MMS + 라운드/2
pair a player in the 상위 그룹의 Seeding methods inside groups of same score 동일 점수 그룹 내 시드 배정 방법
top 상 Apply first seeding method up to round
middle 중 _BLANK_SEEDING 라운드까지 첫 번째 매칭 방법 적용
bottom 하 First seeding method 첫 번째 시드 배정 방법
of the upper group with a player in the 위 선수와 하위 그룹의 Second seeding method 두 번째 시드 배정 방법
of the lower group 위 선수를 매칭 add a sorting on rating 레이팅 정렬 추가
Do not apply secondary criteria for: 부가 기준을 적용하지 않음: Draw-up / draw-down between groups of same score 동일 점수 그룹 간의 상위/하위 매칭
players with a MMS equal to or stronger than MMS가 try to compensate a previous draw-up/draw-down by a draw-down/draw-up, then 이전의 상위/하위 매칭을 하위/상위 매칭으로 보상 시도한 뒤,
_BLANK_AFTER_RANK_THRESHOLD_ 이상인 선수 pair a player in the 상위 그룹의
players who won at least half of their games 절반 이상을 이긴 선수 top 상
players above the Mac Mahon bar 맥마흔 바 이상인 선수 middle 중
_BLANK_COUNTRY_PREFIX Prefer a score gap of 동일 국가 선수끼리 매칭하는 것보다 bottom 하
_BLANK_CLUB_PREFIX Prefer a score gap of 동일 클럽 선수끼리 매칭하는 것보다 of the upper group with a player in the 위 선수와 하위 그룹의
rather than pairing players of the same country. 점 차이를 선호 of the lower group 위 선수를 매칭
rather than pairing players of the same club. 점 차이를 선호 Do not apply secondary criteria for: 부가 기준을 적용하지 않음:
use MMS rather than rank for handicap 핸디캡에는 랭킹 대신 MMS 사용 players with a MMS equal to or stronger than MMS가
Handicap ceiling: 핸디캡 상한: _BLANK_AFTER_RANK_THRESHOLD_ 이상인 선수
round down NBW/MMS score 라운드 내림 라운드 NBW/MMS players who won at least half of their games 절반 이상을 이긴 선수
players above the Mac Mahon bar 맥마흔 바 이상인 선수
_BLANK_COUNTRY_PREFIX Prefer a score gap of 동일 국가 선수끼리 매칭하는 것보다
_BLANK_CLUB_PREFIX Prefer a score gap of 동일 클럽 선수끼리 매칭하는 것보다
rather than pairing players of the same country. 점 차이를 선호
rather than pairing players of the same club. 점 차이를 선호
use MMS rather than rank for handicap 핸디캡에는 랭킹 대신 MMS 사용
Handicap ceiling: 핸디캡 상한:
round down NBW/MMS score 라운드 내림 라운드 NBW/MMS

View File

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

View File

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

View File

@@ -1,12 +1,31 @@
let focused = undefined; let focused = undefined;
function pair(parts) { function pair(parts) {
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 => {

View File

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

View File

@@ -24,7 +24,18 @@ function publishHtml() {
close_modal(); close_modal();
} }
function freeze() {
api.put(`tour/${tour_id}/standings/${activeRound}`, {}
).then(resp => {
if (resp.ok) {
document.location.reload();
}
else throw "freeze error"
}).catch(err => showError(err));
}
onLoad(() => { onLoad(() => {
new Tablesort($('#standings-table')[0]);
$('.criterium').on('click', e => { $('.criterium').on('click', e => {
let alreadyOpen = e.target.closest('select'); let alreadyOpen = e.target.closest('select');
if (alreadyOpen) return; if (alreadyOpen) return;
@@ -85,4 +96,9 @@ onLoad(() => {
$('.publish-html').on('click', e => { $('.publish-html').on('click', e => {
publishHtml(); publishHtml();
}); });
$('#freeze').on('click', e => {
if (confirm("Once frozen, names, levels and even pairings can be changed, but the scores and the standings will stay the same. Freeze the standings?")) {
freeze()
}
});
}); });

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@
<th data-sort-method="number">table</th> <th data-sort-method="number">table</th>
<th>white</th> <th>white</th>
<th>black</th> <th>black</th>
<th>hd</th>
<th>result</th> <th>result</th>
</thead> </thead>
<tbody> <tbody>
@@ -32,6 +33,7 @@
<td data-sort="$game.t">${game.t}.</td> <td data-sort="$game.t">${game.t}.</td>
<td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #rank($white.rank)#{else}BIP#end</span></td> <td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #rank($white.rank)#{else}BIP#end</span></td>
<td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name#if($black.firstname) $black.firstname#end"><span>#if($black)$black.name#if($black.firstname) $black.firstname#end #rank($black.rank)#{else}BIP#end</span></td> <td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name#if($black.firstname) $black.firstname#end"><span>#if($black)$black.name#if($black.firstname) $black.firstname#end #rank($black.rank)#{else}BIP#end</span></td>
<td class="handicap centered">$!game.h</td>
<td class="result centered" data-sort="$game.r" data-result="$game.r">$dispRst[$game.r]</td> <td class="result centered" data-sort="$game.r" data-result="$game.r">$dispRst[$game.r]</td>
</tr> </tr>
#end #end

View File

@@ -44,6 +44,10 @@
}); });
</script> </script>
#set($standings = []) #set($standings = [])
#end
#set($smap = {})
#foreach($part in $standings)
#set($smap[$part.num] = $part)
#end #end
<table id="standings-table" class="ui striped table"> <table id="standings-table" class="ui striped table">
<thead> <thead>
@@ -56,31 +60,45 @@
#foreach($r in [1..$round]) #foreach($r in [1..$round])
<th>R$r</th> <th>R$r</th>
#end #end
#set($criteres = [])
#foreach($crit in $tour.pairing.placement) #foreach($crit in $tour.pairing.placement)
#set($junk = $criteres.add($crit))
#end
#if($criteres[0] == 'SCOREX')
#set($junk = $criteres.add(1, 'MMS'))
#end
#foreach($crit in $criteres)
<th>$crit</th> <th>$crit</th>
#end #end
</thead> </thead>
<tbody> <tbody>
#foreach($part in $standings) #foreach($part in $standings)
<tr> <tr data-id="$part.id">
<td>$part.num</td> <td>$part.num</td>
<td>$part.place</td> <td>$part.place</td>
<td>$part.name#if($part.firstname) $part.firstname#end</td> <td>$esc.html($part.name)#if($part.firstname) $esc.html($part.firstname)#end</td>
<td data-sort="$part.rank">#rank($part.rank)</td> <td data-sort="$part.rank">#rank($part.rank)</td>
<td>#if($part.country)$part.country#end</td> <td>#if($part.country)$part.country#end</td>
<td>$number.format('0.#', $part.NBW)</td> <td>$number.format('0.#', $part.NBW)</td>
#set($mx = $round - 1) #set($mx = $round - 1)
#foreach($r in [0..$mx]) #foreach($r in [0..$mx])
#set($rst = $part.results[$r]) #set($rst = $part.results[$r])
#set($opp_num = $math.toLong($rst))
#if($opp_num)
#set($opponent = $!smap[$opp_num])
#else
#set($opponent = false)
#end
#if($rst.contains('+')) #if($rst.contains('+'))
#set($rst = "<b>$rst</b>") #set($rst = "<b>$rst</b>")
#elseif($rst.contains('-')) #elseif($rst.contains('-'))
#set($rst = "<i>$rst</i>") #set($rst = "<i>$rst</i>")
#end #end
<td class="nobreak">$rst</td> <td class="nobreak game-result" #if($opponent)title="$esc.html($opponent.name)#if($opponent.firstname) $esc.html($opponent.firstname)#end #rank($opponent.rank)#if($opponent.country) $opponent.country#end"#end>$rst</td>
#end #end
#foreach($crit in $tour.pairing.placement) #foreach($crit in $criteres)
<td>$number.format('0.#', $part[$crit])</td> #set($value = "$number.format('0.#', $part[$crit])")
<td data-sort="$value">$value.replace('.5', '½')</td>
#end #end
</tr> </tr>
#end #end
@@ -88,6 +106,12 @@
</table> </table>
</div> </div>
<div class="right form-actions"> <div class="right form-actions">
#if(!$tour.frozen && $round == $tour.rounds)
<button id="freeze" class="ui orange floating right labeled icon button">
<i class="snowflake plane outline icon"></i>
Freeze
</button>
#end
<button id="publish" class="ui yellow floating right labeled icon button"> <button id="publish" class="ui yellow floating right labeled icon button">
<i class="paper plane outline icon"></i> <i class="paper plane outline icon"></i>
Publish Publish

View File

@@ -26,7 +26,20 @@
#if($tour) #if($tour)
#set($round = $math.toInteger($!params.round)) #set($round = $math.toInteger($!params.round))
#if(!$round) #if(!$round)
#set($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

View File

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