Merge branch 'master' into 'translations'

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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