From d983574d4a30e92b3b9e47689fba3e215314c100 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Mon, 22 Jul 2024 12:58:00 +0200 Subject: [PATCH 01/50] Increment version --- api-webapp/pom.xml | 2 +- application/pom.xml | 2 +- pairgoth-common/pom.xml | 2 +- pom.xml | 2 +- view-webapp/pom.xml | 2 +- webserver/pom.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api-webapp/pom.xml b/api-webapp/pom.xml index 2d85781..f84ed7f 100644 --- a/api-webapp/pom.xml +++ b/api-webapp/pom.xml @@ -7,7 +7,7 @@ org.jeudego.pairgoth engine-parent - 0.14 + 0.15 api-webapp diff --git a/application/pom.xml b/application/pom.xml index a400187..82300d5 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -4,7 +4,7 @@ org.jeudego.pairgoth engine-parent - 0.14 + 0.15 application pom diff --git a/pairgoth-common/pom.xml b/pairgoth-common/pom.xml index fac5401..a801812 100644 --- a/pairgoth-common/pom.xml +++ b/pairgoth-common/pom.xml @@ -7,7 +7,7 @@ org.jeudego.pairgoth engine-parent - 0.14 + 0.15 pairgoth-common diff --git a/pom.xml b/pom.xml index 06b75fc..9f27535 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.jeudego.pairgoth engine-parent - 0.14 + 0.15 pom diff --git a/view-webapp/pom.xml b/view-webapp/pom.xml index 15cbf3b..38b3fac 100644 --- a/view-webapp/pom.xml +++ b/view-webapp/pom.xml @@ -7,7 +7,7 @@ org.jeudego.pairgoth engine-parent - 0.14 + 0.15 view-webapp diff --git a/webserver/pom.xml b/webserver/pom.xml index 2f1bbe7..167af8b 100644 --- a/webserver/pom.xml +++ b/webserver/pom.xml @@ -4,7 +4,7 @@ org.jeudego.pairgoth engine-parent - 0.14 + 0.15 webserver jar From 7ab113eedf5c669096b8f4c2c45a7e39a207c508 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Mon, 22 Jul 2024 13:29:26 +0200 Subject: [PATCH 02/50] Store backups in an 'history' subdirectory --- .../kotlin/org/jeudego/pairgoth/store/FileStore.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt index 6fefa10..90a8a7f 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt @@ -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()) } } From 67b1e1ed2e1a29498d6847595a85f438b416d036 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sun, 11 Aug 2024 11:39:14 +0200 Subject: [PATCH 03/50] Fix configuration doc --- doc/configuration.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/configuration.md b/doc/configuration.md index 333509a..3e88826 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -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 From 74af08a59285b92490e4142c71b781aa5868dd1b Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Tue, 23 Jul 2024 21:04:50 +0200 Subject: [PATCH 04/50] Fix controls display bug in tournament creation on mm/swiss changes --- view-webapp/src/main/webapp/js/tour-information.inc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view-webapp/src/main/webapp/js/tour-information.inc.js b/view-webapp/src/main/webapp/js/tour-information.inc.js index a009703..4bdcf82 100644 --- a/view-webapp/src/main/webapp/js/tour-information.inc.js +++ b/view-webapp/src/main/webapp/js/tour-information.inc.js @@ -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'); From 02e1fb8319ee4725523b66c715773f9bc4cb2351 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Wed, 24 Jul 2024 09:11:52 +0200 Subject: [PATCH 05/50] Bugfix tournament deletion --- .../src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt index 90a8a7f..c92b4a2 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/store/FileStore.kt @@ -162,7 +162,7 @@ class FileStore(pathStr: String): Store { val file = path.resolve(filename).toFile() if (!file.exists()) throw Error("File $filename does not exist") val history = path.resolve("history").toFile() - if (!history.exists() || !history.mkdir()) { + if (!history.exists() && !history.mkdir()) { throw Error("cannot create 'history' sub-directory") } file.renameTo(path.resolve("history/${filename}-${timestamp}").toFile()) From 6e97c91b2fcd7c153be7dcc13316afbc4447697d Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 27 Jul 2024 15:53:41 +0200 Subject: [PATCH 06/50] Table numbers exclusion: handle saving --- .../jeudego/pairgoth/api/TournamentHandler.kt | 36 +++++++++++++------ .../org/jeudego/pairgoth/model/Tournament.kt | 24 +++++++------ view-webapp/src/main/sass/tour.scss | 4 +++ .../src/main/webapp/js/tour-pairing.inc.js | 31 ++++++++++++---- .../src/main/webapp/tour-pairing.inc.html | 8 +++++ 5 files changed, 77 insertions(+), 26 deletions(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt index c11065f..d276ddc 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -67,24 +67,40 @@ object TournamentHandler: PairgothApiHandler { val payload = getObjectPayload(request) // 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 { + 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) } + private 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+)?)*$") } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index f1b0c5e..d3304fa 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -6,8 +6,6 @@ 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.store.nextPlayerId import org.jeudego.pairgoth.store.nextTournamentId import kotlin.math.max @@ -30,7 +28,8 @@ sealed class Tournament ( 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 = mutableListOf() ) { companion object {} enum class Type(val playersNumber: Int, val individual: Boolean = true) { @@ -170,8 +169,9 @@ class StandardTournament( pairing: Pairing, rules: Rules = Rules.FRENCH, gobanSize: Int = 19, - komi: Double = 7.5 -): Tournament(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { + komi: Double = 7.5, + tablesExclusion: MutableList = mutableListOf() +): Tournament(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi, tablesExclusion) { override val players get() = _pairables } @@ -192,8 +192,9 @@ class TeamTournament( pairing: Pairing, rules: Rules = Rules.FRENCH, gobanSize: Int = 19, - komi: Double = 7.5 -): Tournament(id, type, name, shortName, startDate, endDate, director, country, location, online, timeSystem, rounds, pairing, rules, gobanSize, komi) { + komi: Double = 7.5, + tablesExclusion: MutableList = mutableListOf() +): Tournament(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 +268,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 +288,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 @@ -326,7 +329,8 @@ fun Tournament<*>.toJson() = Json.MutableObject( "gobanSize" to gobanSize, "timeSystem" to timeSystem.toJson(), "rounds" to rounds, - "pairing" to pairing.toJson() + "pairing" to pairing.toJson(), + "tablesExclusion" to tablesExclusion.toJsonArray() ) fun Tournament<*>.toFullJson(): Json.Object { diff --git a/view-webapp/src/main/sass/tour.scss b/view-webapp/src/main/sass/tour.scss index d182047..deaf4cf 100644 --- a/view-webapp/src/main/sass/tour.scss +++ b/view-webapp/src/main/sass/tour.scss @@ -405,6 +405,10 @@ margin-top: 0.2em; } + .tables-exclusion { + margin-top: 0.2em; + } + /* results section */ #results-filter { diff --git a/view-webapp/src/main/webapp/js/tour-pairing.inc.js b/view-webapp/src/main/webapp/js/tour-pairing.inc.js index 381234d..4df7bcf 100644 --- a/view-webapp/src/main/webapp/js/tour-pairing.inc.js +++ b/view-webapp/src/main/webapp/js/tour-pairing.inc.js @@ -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) { diff --git a/view-webapp/src/main/webapp/tour-pairing.inc.html b/view-webapp/src/main/webapp/tour-pairing.inc.html index 685ecc4..14c317e 100644 --- a/view-webapp/src/main/webapp/tour-pairing.inc.html +++ b/view-webapp/src/main/webapp/tour-pairing.inc.html @@ -22,6 +22,14 @@
( $pairables.size() pairable, $games.size() games )
+
+#if($tour.tablesExclusion && $round <= $tour.tablesExclusion.size()) + #set($tablesExclusion = $!tour.tablesExclusion[$round - 1]) +#else + #set($tablesExclusion = '') +#end + Exclude table numbers: +
From 471b316dce49050131ad82d0e086baa76726261c Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 27 Jul 2024 16:34:35 +0200 Subject: [PATCH 07/50] Table numbers exclusion is functional --- .../jeudego/pairgoth/api/PairingHandler.kt | 14 +++++++++++++ .../jeudego/pairgoth/api/TournamentHandler.kt | 2 +- .../org/jeudego/pairgoth/model/Tournament.kt | 21 +++++++++++++++++++ .../src/main/webapp/js/tour-pairing.inc.js | 9 +++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index ee534a9..1e9eef1 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -3,6 +3,7 @@ package org.jeudego.pairgoth.api import com.republicate.kson.Json import com.republicate.kson.toJsonArray import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest +import org.jeudego.pairgoth.api.TournamentHandler.dispatchEvent import org.jeudego.pairgoth.model.Game import org.jeudego.pairgoth.model.getID import org.jeudego.pairgoth.model.toID @@ -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,6 +59,10 @@ object PairingHandler: PairgothApiHandler { } ?: badRequest("invalid pairable id: #$id") } val games = tournament.pair(round, pairables) + + // always renumber table to take table exclusion into account + tournament.renumberTables(round) + val ret = games.map { it.toJson() }.toJsonArray() tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret)) return ret @@ -122,6 +128,14 @@ object PairingHandler: PairgothApiHandler { return Json.Object("success" to true) } else { // without id, it's a table renumbering + if (payload.containsKey("excludeTables")) { + val tablesExclusion = payload.getString("excludeTables") ?: badRequest("missing 'excludeTables'") + TournamentHandler.validateTablesExclusion(tablesExclusion) + while (tournament.tablesExclusion.size < round) tournament.tablesExclusion.add("") + tournament.tablesExclusion[round - 1] = tablesExclusion + tournament.dispatchEvent(TournamentUpdated, request, tournament.toJson()) + } + val sortedPairables = tournament.getSortedPairables(round) val sortedMap = sortedPairables.associateBy { it.getID()!! diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt index d276ddc..25b0e55 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -91,7 +91,7 @@ object TournamentHandler: PairgothApiHandler { return Json.Object("success" to true) } - private fun validateTablesExclusion(exclusion: String) { + internal fun validateTablesExclusion(exclusion: String) { if (!tablesExclusionValidator.matches(exclusion)) badRequest("invalid tables exclusion pattern") } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index d3304fa..a95dc15 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -6,10 +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.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 ( @@ -127,9 +129,12 @@ sealed class Tournament ( fun renumberTables(round: Int, pivot: Game? = null, orderBY: (Game) -> Int = ::defaultGameOrderBy): Boolean { var changed = false var nextTable = 1 + val excluded = excludedTables(round) games(round).values.filter{ game -> pivot?.let { pivot.id != game.id } ?: true }.sortedBy(orderBY).forEach { game -> + while (excluded.contains(nextTable)) ++nextTable if (pivot != null && nextTable == pivot.table) { ++nextTable + while (excluded.contains(nextTable)) ++nextTable } if (game.table != 0) { changed = changed || game.table != nextTable @@ -150,6 +155,22 @@ sealed class Tournament ( "ready" to (games.getOrNull(index)?.values?.count { it.result != Game.Result.UNKNOWN } ?: 0) ) }.toJsonArray() + + fun excludedTables(round: Int): Set { + if (round > tablesExclusion.size) return emptySet() + val excluded = mutableSetOf() + 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 diff --git a/view-webapp/src/main/webapp/js/tour-pairing.inc.js b/view-webapp/src/main/webapp/js/tour-pairing.inc.js index 4df7bcf..9ca05ed 100644 --- a/view-webapp/src/main/webapp/js/tour-pairing.inc.js +++ b/view-webapp/src/main/webapp/js/tour-pairing.inc.js @@ -38,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(); From 160a15603e3ab349921c4209d240df71eaafaa10 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 27 Jul 2024 20:24:40 +0200 Subject: [PATCH 08/50] Tables exclusion control is not to be printed --- view-webapp/src/main/sass/main.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 7b8a9e9..92061a0 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -545,7 +545,7 @@ 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 { + #header, #logo, #lang, .steps, #filter-box, #reglist-mode, #footer, #unpairables, #pairing-buttons, button, #standings-params, #logout, .pairing-stats, .result-sheets, .toggle, #overview, .tables-exclusion { display: none !important; } From 7643d88e34350900f0bfa7c8c6d2f1acfbb52b59 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sat, 27 Jul 2024 22:48:27 +0200 Subject: [PATCH 09/50] Display final/prelim column --- view-webapp/src/main/sass/main.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 92061a0..8c25b7d 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -675,9 +675,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; From ecd973dd227bd1be5df47848ff942cdf4ed8d556 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sun, 28 Jul 2024 00:07:01 +0200 Subject: [PATCH 10/50] Manual tables handling --- .../org/jeudego/pairgoth/api/PairingHandler.kt | 1 + .../main/kotlin/org/jeudego/pairgoth/model/Game.kt | 10 +++++++--- .../kotlin/org/jeudego/pairgoth/model/Tournament.kt | 13 ++++++++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index 1e9eef1..6e926c3 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -103,6 +103,7 @@ 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) { diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt index 252f7cb..7d6f618 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Game.kt @@ -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('?'), @@ -43,7 +45,8 @@ fun Game.toJson() = Json.Object( "b" to black, "h" to handicap, "r" to "${result.symbol}", - "dd" to drawnUpDown + "dd" to drawnUpDown, + "ft" to forcedTable ) fun Game.Companion.fromJson(json: Json.Object) = Game( @@ -53,5 +56,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 ) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index a95dc15..5ea4daf 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -130,11 +130,18 @@ sealed class Tournament ( var changed = false var nextTable = 1 val excluded = excludedTables(round) - games(round).values.filter{ game -> pivot?.let { pivot.id != game.id } ?: true }.sortedBy(orderBY).forEach { game -> - while (excluded.contains(nextTable)) ++nextTable + 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 (excluded.contains(nextTable)) ++nextTable + while (excludedAndForced.contains(nextTable)) ++nextTable } if (game.table != 0) { changed = changed || game.table != nextTable From 9cee68a4893d11789a71fbd443e8c86b8ee79651 Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Sun, 28 Jul 2024 00:29:11 +0200 Subject: [PATCH 11/50] Add MMS column --- .../org/jeudego/pairgoth/view/PairgothTool.kt | 5 +++ view-webapp/src/main/sass/main.scss | 2 +- .../main/webapp/tour-registration.inc.html | 41 ++++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt index 60e9bdd..d73e5de 100644 --- a/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt +++ b/view-webapp/src/main/kotlin/org/jeudego/pairgoth/view/PairgothTool.kt @@ -74,6 +74,11 @@ class PairgothTool { } } + fun getMmsPlayersMap(pairables: Collection) = + pairables.associate { part -> + Pair(part.getLong("id"), part.getDouble("MMS")?.toLong()) + } + fun removeBye(games: Collection) = games.filter { it.getInt("b")!! != 0 && it.getInt("w")!! != 0 diff --git a/view-webapp/src/main/sass/main.scss b/view-webapp/src/main/sass/main.scss index 8c25b7d..8062ff0 100644 --- a/view-webapp/src/main/sass/main.scss +++ b/view-webapp/src/main/sass/main.scss @@ -545,7 +545,7 @@ 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, .tables-exclusion { + #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 { display: none !important; } diff --git a/view-webapp/src/main/webapp/tour-registration.inc.html b/view-webapp/src/main/webapp/tour-registration.inc.html index 348009e..d784246 100644 --- a/view-webapp/src/main/webapp/tour-registration.inc.html +++ b/view-webapp/src/main/webapp/tour-registration.inc.html @@ -6,6 +6,26 @@ #set($pmap = $utils.toMap($teams)) #end +## CB TODO - why limit to INDIVIDUAL here? +#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 + + #set($mmbase = []) + #end + #set($mmsMap = $utils.getMmsMap($mmbase)) + #set($mmsPlayersMap = $utils.getMmsPlayersMap($mmbase)) +#end +
@@ -41,6 +61,9 @@ Rank ## TableSort bug which inverts specified sort... Rating +#if($tour.pairing.type == 'MAC_MAHON') + MMS +#end Participation @@ -61,6 +84,9 @@ #end #rank($part.rank)#if($part.mmsCorrection) (#if($part.mmsCorrection > 0)+#end$part.mmsCorrection)#end $part.rating +#if($tour.pairing.type == 'MAC_MAHON') + $mmsPlayersMap[$part.id] +#end
#foreach($round in [1..$tour.rounds]) @@ -249,21 +275,6 @@
#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 - - #set($mmbase = []) - #end - #set($mmsMap = $utils.getMmsMap($mmbase)) #end -
+#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' }) +
@@ -152,6 +162,7 @@
*# +#if($utils.displayRatings('egf', $tour.country.toLowerCase()))
@@ -161,6 +172,8 @@
+#end +#if($utils.displayRatings('ffg', $tour.country.toLowerCase()))
@@ -170,8 +183,9 @@
+#end
-
+
From fe48bfb4b6796d8da0d8375854ee2d705822ed2a Mon Sep 17 00:00:00 2001 From: Claude Brisson Date: Thu, 29 Aug 2024 18:37:42 +0200 Subject: [PATCH 48/50] Fix rounding option: correct choice is 'round down' or 'no rounding' --- .../src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt | 7 ++++--- .../org/jeudego/pairgoth/pairing/BasePairingHelper.kt | 2 +- .../org/jeudego/pairgoth/pairing/solver/BaseSolver.kt | 2 +- view-webapp/src/main/webapp/WEB-INF/tools.xml | 1 + view-webapp/src/main/webapp/WEB-INF/translations/fr | 1 + view-webapp/src/main/webapp/WEB-INF/translations/kr | 1 + view-webapp/src/main/webapp/tour-parameters.inc.html | 9 +-------- view-webapp/src/main/webapp/tour-standings.inc.html | 3 ++- 8 files changed, 12 insertions(+), 14 deletions(-) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt index 5f5b7fb..ac741a3 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/api/ApiTools.kt @@ -15,6 +15,7 @@ import kotlin.math.ceil import kotlin.math.floor import kotlin.math.max import kotlin.math.min +import kotlin.math.round // TODO CB avoid code redundancy with solvers @@ -29,7 +30,7 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f 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) { @@ -45,7 +46,7 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f 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 @@ -96,7 +97,7 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f 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=" }) } diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt index 05b47fd..39b2a09 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/BasePairingHelper.kt @@ -48,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) diff --git a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt index 699e388..694972f 100644 --- a/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt +++ b/api-webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/solver/BaseSolver.kt @@ -480,7 +480,7 @@ 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 { diff --git a/view-webapp/src/main/webapp/WEB-INF/tools.xml b/view-webapp/src/main/webapp/WEB-INF/tools.xml index 6d68015..85aa1d0 100644 --- a/view-webapp/src/main/webapp/WEB-INF/tools.xml +++ b/view-webapp/src/main/webapp/WEB-INF/tools.xml @@ -5,6 +5,7 @@ +