diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt index 445d55b..b07235b 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/PairingHandler.kt @@ -15,7 +15,7 @@ object PairingHandler: PairgothApiHandler { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { val tournament = getTournament(request) val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") - val playing = (tournament.games.getOrNull(round)?.values ?: emptyList()).flatMap { + val playing = tournament.games(round).values.flatMap { listOf(it.black, it.white) }.toSet() return tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.map { it.id }.toJsonArray() @@ -27,7 +27,7 @@ object PairingHandler: PairgothApiHandler { val payload = getArrayPayload(request) val allPlayers = payload.size == 1 && payload[0] == "all" if (!allPlayers && tournament.pairing.type == Pairing.PairingType.SWISS) badRequest("Swiss pairing requires all pairable players") - val playing = (tournament.games.getOrNull(round)?.values ?: emptyList()).flatMap { + val playing = (tournament.games(round).values).flatMap { listOf(it.black, it.white) }.toSet() val pairables = @@ -52,15 +52,15 @@ object PairingHandler: PairgothApiHandler { 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.games.size) badRequest("cannot delete games in other rounds but the last") + if (round != tournament.lastRound()) badRequest("cannot delete games in other rounds but the last") val payload = getArrayPayload(request) val allPlayers = payload.size == 1 && payload[0] == "all" if (allPlayers) { - tournament.games.removeLast() + tournament.games(round).clear() } else { payload.forEach { val id = (it as Number).toInt() - tournament.games[round].remove(id) + tournament.games(round).remove(id) } } Event.dispatch(gamesDeleted, Json.Object("tournament" to tournament.id, "round" to round, "data" to payload)) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt index bac6b0a..724b691 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/ResultsHandler.kt @@ -14,7 +14,7 @@ object ResultsHandler: PairgothApiHandler { override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? { val tournament = getTournament(request) val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") - val games = tournament.games.getOrNull(round)?.values ?: emptyList() + val games = tournament.games(round).values return games.map { it.toJson() }.toJsonArray() } @@ -22,8 +22,8 @@ object ResultsHandler: PairgothApiHandler { val tournament = getTournament(request) val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number") val payload = getObjectPayload(request) - val game = tournament.games[round][payload.getInt("id")] ?: badRequest("invalid game id") - game.result = Game.Result.valueOf(payload.getString("result") ?: badRequest("missing result")) + val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id") + game.result = Game.Result.fromSymbol(payload.getChar("result") ?: badRequest("missing result")) Event.dispatch(Event.resultUpdated, Json.Object("tournament" to tournament.id, "round" to round, "data" to game)) return Json.Object("success" to true) } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt index ed9c59f..3dc6a02 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/api/TournamentHandler.kt @@ -54,11 +54,15 @@ object TournamentHandler: PairgothApiHandler { // 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) } - updated.games.addAll(tournament.games) + for (round in 1..tournament.lastRound()) updated.games(round).apply { + clear() + putAll(tournament.games(round)) + } updated.criteria.addAll(tournament.criteria) Store.replaceTournament(updated) Event.dispatch(tournamentUpdated, tournament.toJson()) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt index b5f3171..df73957 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/ext/OpenGotha.kt @@ -173,7 +173,9 @@ object OpenGotha { ) }.associateBy { it.id }.toMutableMap() } - tournament.games.addAll(gamesPerRound) + gamesPerRound.forEachIndexed { index, games -> + tournament.games(index).putAll(games) + } return tournament } @@ -209,9 +211,9 @@ object OpenGotha { } - ${tournament.games.flatMapIndexed { round, games -> + ${(1..tournament.lastRound()).map { tournament.games(it) }.flatMapIndexed { index, games -> games.values.mapIndexed { table, game -> - Triple(round, table , game) + Triple(index + 1, table , game) } }.joinToString("\n") { (round, table, game) -> """() // skipped rounds } @@ -19,13 +21,15 @@ object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE) { override fun toJson(): Json.Object { throw Error("bye player should never be serialized") } + + override val club = "none" + override val country = "none" } fun Pairable.displayRank(): String = when { rank < 0 -> "${-rank}k" - rank >= 0 && rank < 10 -> "${rank + 1}d" - rank >= 10 -> "${rank - 9}p" - else -> throw Error("impossible") + rank < 10 -> "${rank + 1}d" + else -> "${rank - 9}p" } private val rankRegex = Regex("(\\d+)([kdp])", RegexOption.IGNORE_CASE) @@ -50,8 +54,8 @@ class Player( var firstname: String, rating: Int, rank: Int, - var country: String, - var club: String + override var country: String, + override var club: String ): Pairable(id, name, rating, rank) { companion object // used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...) diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt index 2abd8cb..19d422c 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Pairing.kt @@ -6,32 +6,39 @@ import org.jeudego.pairgoth.model.Pairing.PairingType.* import org.jeudego.pairgoth.model.MacMahon import org.jeudego.pairgoth.model.RoundRobin import org.jeudego.pairgoth.model.Swiss +import org.jeudego.pairgoth.pairing.MacMahonSolver import org.jeudego.pairgoth.pairing.SwissSolver import java.util.Random -// TODO - this is only an early draft - sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) { companion object {} enum class PairingType { SWISS, MACMAHON, ROUNDROBIN } data class Weights( - val played: Double = 1_000_000.0, // weight if players already met - val score: Double = 10_000.0, // per difference of score or MMS - val place: Double = 1_000.0, // per difference of expected position for Swiss - val color: Double = 100.0 // per color unbalancing + val played: Double = 1_000_000.0, // players already met + val group: Double = 100_000.0, // different group + val handicap: Double = 50_000.0, // for each handicap stone + val score: Double = 10_000.0, // per difference of score or MMS + val place: Double = 1_000.0, // per difference of expected position for Swiss + val color: Double = 500.0, // per color unbalancing + val club: Double = 100.0, // same club weight + val country: Double = 50.0 // same country ) abstract fun pair(tournament: Tournament<*>, round: Int, pairables: List): List } fun Tournament<*>.historyBefore(round: Int) = - if (games.isEmpty()) emptyList() - else games.slice(0 until round).flatMap { it.values } + if (lastRound() == 0) emptyList() + else (0 until round).flatMap { games(round).values } class Swiss( var method: Method, - var firstRoundMethod: Method = method -): Pairing(SWISS) { + var firstRoundMethod: Method = method, +): Pairing(SWISS, Weights( + handicap = 0.0, // no handicap games anyway + club = 0.0, + country = 0.0 +)) { enum class Method { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP } override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { val actualMethod = if (round == 1) firstRoundMethod else method @@ -41,12 +48,13 @@ class Swiss( class MacMahon( var bar: Int = 0, - var minLevel: Int = -30 + var minLevel: Int = -30, + var reducer: Int = 1 ): Pairing(MACMAHON) { val groups = mutableListOf() override fun pair(tournament: Tournament<*>, round: Int, pairables: List): List { - TODO() + return MacMahonSolver(tournament.historyBefore(round), pairables, weights, mmBase = minLevel, mmBar = bar, reducer = reducer).pair() } } @@ -65,7 +73,8 @@ fun Pairing.Companion.fromJson(json: Json.Object) = when (json.getString("type") ) MACMAHON -> MacMahon( bar = json.getInt("bar") ?: 0, - minLevel = json.getInt("minLevel") ?: -30 + minLevel = json.getInt("minLevel") ?: -30, + reducer = json.getInt("reducer") ?: 1 ) ROUNDROBIN -> RoundRobin() } @@ -74,7 +83,7 @@ fun Pairing.toJson() = when (this) { is Swiss -> if (method == firstRoundMethod) Json.Object("type" to type.name, "method" to method.name) else Json.Object("type" to type.name, "method" to method.name, "firstRoundMethod" to firstRoundMethod.name) - is MacMahon -> Json.Object("type" to type.name, "bar" to bar, "minLevel" to minLevel) + is MacMahon -> Json.Object("type" to type.name, "bar" to bar, "minLevel" to minLevel, "reducer" to reducer) is RoundRobin -> Json.Object("type" to type.name) } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt index 469f685..78bdeb2 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/model/Tournament.kt @@ -56,11 +56,17 @@ sealed class Tournament ( val evenPairables = if (pairables.size % 2 == 0) pairables else pairables.toMutableList().also { it.add(ByePlayer) } - return pairing.pair(this, round, evenPairables) + return pairing.pair(this, round, evenPairables).also { newGames -> + if (games.size < round) games.add(mutableMapOf()) + games[round - 1].putAll( newGames.associateBy { it.id } ) + } } // games per id for each round - val games = mutableListOf>() + private val games = mutableListOf>() + + fun games(round: Int) = games.getOrNull(round - 1) ?: mutableMapOf() + fun lastRound() = games.size // standings criteria val criteria = mutableListOf( @@ -116,10 +122,10 @@ class TeamTournament( inner class Team(id: Int, name: String): Pairable(id, name, 0, 0) { val playerIds = mutableSetOf() val teamPlayers: Set get() = playerIds.mapNotNull { players[id] }.toSet() - override val rating: Int get() = if (players.isEmpty()) super.rating else (teamPlayers.sumOf { player -> player.rating.toDouble() } / players.size).roundToInt() - override val rank: Int get() = if (players.isEmpty()) super.rank else (teamPlayers.sumOf { player -> player.rank.toDouble() } / players.size).roundToInt() - val club: String? get() = players.map { club }.distinct().let { if (it.size == 1) it[0] else null } - val country: String? get() = players.map { country }.distinct().let { if (it.size == 1) it[0] else null } + override val rating: Int get() = if (teamPlayers.isEmpty()) super.rating else (teamPlayers.sumOf { player -> player.rating.toDouble() } / players.size).roundToInt() + override val rank: Int get() = if (teamPlayers.isEmpty()) super.rank else (teamPlayers.sumOf { player -> player.rank.toDouble() } / players.size).roundToInt() + override val club: String? get() = teamPlayers.map { club }.distinct().let { if (it.size == 1) it[0] else null } + override val country: String? get() = teamPlayers.map { country }.distinct().let { if (it.size == 1) it[0] else null } override fun toJson() = Json.Object( "id" to id, "name" to name, diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt new file mode 100644 index 0000000..5f7b647 --- /dev/null +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/MacMahonSolver.kt @@ -0,0 +1,45 @@ +package org.jeudego.pairgoth.pairing + +import org.jeudego.pairgoth.model.Game +import org.jeudego.pairgoth.model.Pairable +import org.jeudego.pairgoth.model.Pairing +import org.jeudego.pairgoth.model.Swiss.Method.* +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt +import kotlin.math.sign + +class MacMahonSolver(history: List, pairables: List, weights: Pairing.Weights, val mmBase: Int, val mmBar: Int, val reducer: Int): Solver(history, pairables, weights) { + + val Pairable.mms get() = mmBase + score + + // CB TODO - configurable criteria + override fun sort(p: Pairable, q: Pairable): Int = + if (p.mms != q.mms) ((q.mms - p.mms) * 1000).toInt() + else if (p.sos != q.sos) ((q.sos - p.sos) * 1000).toInt() + else if (p.sosos != q.sosos) ((q.sosos - p.sosos) * 1000).toInt() + else 0 + + override fun weight(black: Pairable, white: Pairable): Double { + var weight = 0.0 + if (black.played(white)) weight += weights.played + if (black.club == white.club) weight += weights.club + if (black.country == white.country) weight += weights.country + weight += (abs(black.colorBalance + 1) + abs(white.colorBalance - 1)) * weights.color + + // MacMahon specific + weight += Math.abs(black.mms - white.mms) * weights.score + if (sign(mmBar - black.mms) != sign(mmBar - white.mms)) weight += weights.group + + if (black.mms < mmBar && white.mms < mmBar && abs(black.mms - white.mms) > reducer) { + if (black.mms > white.mms) weight = Double.NaN + else weight = handicap(black, white) * weights.handicap + } + return weight + } + + override fun handicap(black: Pairable, white: Pairable) = + if (black.mms > mmBar || white.mms > mmBar || abs(black.mms - white.mms) < reducer || black.mms > white.mms) 0 + else (white.mms - black.mms - reducer).roundToInt() + +} diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt index e18abf2..14c83c7 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/Solver.kt @@ -20,7 +20,12 @@ sealed class Solver(val history: List, val pairables: List, val } open fun sort(p: Pairable, q: Pairable): Int = 0 // no sort by default - abstract fun weight(p: Pairable, q: Pairable): Double + abstract fun weight(black: Pairable, white: Pairable): Double + open fun handicap(black: Pairable, white: Pairable) = 0 + open fun games(black: Pairable, white: Pairable): List { + // CB TODO team of individuals pairing + return listOf(Game(id = Store.nextGameId, black = black.id, white = white.id, handicap = handicap(black, white))) + } fun pair(): List { // check that at this stage, we have an even number of pairables @@ -30,16 +35,16 @@ sealed class Solver(val history: List, val pairables: List, val for (j in i + 1 until n) { val p = pairables[i] val q = pairables[j] - builder.addEdge(p, q, weight(p, q)) - builder.addEdge(q, p, weight(q, p)) + weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it) } + weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it) } } } val graph = builder.build() val matching = KolmogorovWeightedPerfectMatching(graph, ObjectiveSense.MINIMIZE) val solution = matching.matching - val result = solution.map { - Game(Store.nextGameId, graph.getEdgeSource(it).id , graph.getEdgeTarget(it).id) + val result = solution.flatMap { + games(black = graph.getEdgeSource(it) , white = graph.getEdgeTarget(it)) } return result } @@ -94,49 +99,55 @@ sealed class Solver(val history: List, val pairables: List, val } // score (number of wins) - val Pairable.score: Int get() = _score[id] ?: 0 - private val _score: Map by lazy { - history.mapNotNull { game -> - when (game.result) { - BLACK -> game.black - WHITE -> game.white - else -> null + val Pairable.score: Double get() = _score[id] ?: 0.0 + private val _score: Map by lazy { + mutableMapOf().apply { + history.forEach { game -> + when (game.result) { + BLACK -> put(game.black, getOrDefault(game.black, 0.0) + 1.0) + WHITE -> put(game.white, getOrDefault(game.white, 0.0) + 1.0) + BOTHWIN -> { + put(game.black, getOrDefault(game.black, 0.0) + 0.5) + put(game.white, getOrDefault(game.white, 0.0) + 0.5) + } + else -> {} + } } - }.groupingBy { it }.eachCount() + } } // sos - val Pairable.sos: Int get() = _sos[id] ?: 0 + val Pairable.sos: Double get() = _sos[id] ?: 0.0 private val _sos by lazy { (history.map { game -> - Pair(game.black, _score[game.white] ?: 0) + Pair(game.black, _score[game.white] ?: 0.0) } + history.map { game -> - Pair(game.white, _score[game.black] ?: 0) - }).groupingBy { it.first }.fold(0) { acc, next -> + Pair(game.white, _score[game.black] ?: 0.0) + }).groupingBy { it.first }.fold(0.0) { acc, next -> acc + next.second } } // sosos - val Pairable.sosos: Int get() = _sosos[id] ?: 0 + val Pairable.sosos: Double get() = _sosos[id] ?: 0.0 private val _sosos by lazy { (history.map { game -> - Pair(game.black, _sos[game.white] ?: 0) + Pair(game.black, _sos[game.white] ?: 0.0) } + history.map { game -> - Pair(game.white, _sos[game.black] ?: 0) - }).groupingBy { it.first }.fold(0) { acc, next -> + Pair(game.white, _sos[game.black] ?: 0.0) + }).groupingBy { it.first }.fold(0.0) { acc, next -> acc + next.second } } // sodos - val Pairable.sodos: Int get() = _sodos[id] ?: 0 + val Pairable.sodos: Double get() = _sodos[id] ?: 0.0 private val _sodos by lazy { (history.map { game -> - Pair(game.black, if (game.result == BLACK) _score[game.white] ?: 0 else 0) + Pair(game.black, if (game.result == BLACK) _score[game.white] ?: 0.0 else 0.0) } + history.map { game -> - Pair(game.white, if (game.result == WHITE) _score[game.black] ?: 0 else 0) - }).groupingBy { it.first }.fold(0) { acc, next -> + Pair(game.white, if (game.result == WHITE) _score[game.black] ?: 0.0 else 0.0) + }).groupingBy { it.first }.fold(0.0) { acc, next -> acc + next.second } } diff --git a/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt index 23e1fbf..368c614 100644 --- a/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt +++ b/webapp/src/main/kotlin/org/jeudego/pairgoth/pairing/SwissSolver.kt @@ -11,24 +11,29 @@ class SwissSolver(history: List, pairables: List, weights: Pairi override fun sort(p: Pairable, q: Pairable): Int = when (p.score) { - q.score -> p.rating - q.rating - else -> p.score - q.score + q.score -> q.rating - p.rating + else -> ((q.score - p.score) * 1000).toInt() } - override fun weight(p: Pairable, q: Pairable) = when { - p.played(q) -> weights.played - p.score != q.score -> { + override fun weight(black: Pairable, white: Pairable): Double { + var weight = 0.0 + if (black.played(white)) weight += weights.played + if (black.score != white.score) { val placeWeight = - if (p.score > q.score) (p.placeInGroup.second + q.placeInGroup.first) * weights.place - else (q.placeInGroup.second + p.placeInGroup.first) * weights.place - abs(p.score - q.score) * weights.score + placeWeight + if (black.score > white.score) (black.placeInGroup.second + white.placeInGroup.first) * weights.place + else (white.placeInGroup.second + black.placeInGroup.first) * weights.place + weight += abs(black.score - white.score) * weights.score + placeWeight + } else { + weight += when (method) { + SPLIT_AND_FOLD -> + if (black.placeInGroup.first > white.placeInGroup.first) abs(black.placeInGroup.first - (white.placeInGroup.second - white.placeInGroup.first)) * weights.place + else abs(white.placeInGroup.first - (black.placeInGroup.second - black.placeInGroup.first)) * weights.place + + SPLIT_AND_RANDOM -> rand.nextDouble() * black.placeInGroup.second * weights.place + SPLIT_AND_SLIP -> abs(abs(black.placeInGroup.first - white.placeInGroup.first) - black.placeInGroup.second) * weights.place + } } - else -> when (method) { - SPLIT_AND_FOLD -> - if (p.placeInGroup.first > q.placeInGroup.first) abs(p.placeInGroup.first - (q.placeInGroup.second - q.placeInGroup.first)) * weights.place - else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * weights.place - SPLIT_AND_RANDOM -> rand.nextDouble() * p.placeInGroup.second * weights.place - SPLIT_AND_SLIP -> abs(abs(p.placeInGroup.first - q.placeInGroup.first) - p.placeInGroup.second) * weights.place - } - } + (abs(p.colorBalance + 1) + abs(q.colorBalance - 1)) * weights.color + weight += (abs(black.colorBalance + 1) + abs(white.colorBalance - 1)) * weights.color + return weight + } } diff --git a/webapp/src/test/kotlin/BasicTests.kt b/webapp/src/test/kotlin/BasicTests.kt index c76de8d..e9c70cc 100644 --- a/webapp/src/test/kotlin/BasicTests.kt +++ b/webapp/src/test/kotlin/BasicTests.kt @@ -1,16 +1,15 @@ package org.jeudego.pairgoth.test import com.republicate.kson.Json -import org.junit.jupiter.api.MethodOrderer.Alphanumeric +import org.junit.jupiter.api.MethodOrderer.MethodName import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.api.TestMethodOrder -import java.util.Objects import kotlin.test.assertEquals import kotlin.test.assertTrue -@TestMethodOrder(Alphanumeric::class) +@TestMethodOrder(MethodName::class) @TestInstance(TestInstance.Lifecycle.PER_CLASS) class BasicTests: TestBase() { @@ -51,8 +50,7 @@ class BasicTests: TestBase() { ), "rounds" to 2, "pairing" to Json.Object( - "type" to "SWISS", - "method" to "SPLIT_AND_RANDOM" + "type" to "MACMAHON" ) ) @@ -60,7 +58,7 @@ class BasicTests: TestBase() { "name" to "Burma", "firstname" to "Nestor", "rating" to 1600, - "rank" to -2, + "rank" to -5, "country" to "FR", "club" to "13Ma" ) @@ -114,33 +112,59 @@ class BasicTests: TestBase() { fun `005 pair`() { val resp = TestAPI.post("/api/tour/1/part", anotherPlayer) as Json.Object assertTrue(resp.getBoolean("success") == true, "expecting success") - val games = TestAPI.post("/api/tour/1/pair/1", Json.Array("all")) + var games = TestAPI.post("/api/tour/1/pair/1", Json.Array("all")) val possibleResults = setOf( - "[{\"id\":1,\"w\":1,\"b\":2,\"r\":\"?\"}]", - "[{\"id\":1,\"w\":2,\"b\":1,\"r\":\"?\"}]") + """[{"id":1,"w":1,"b":2,"h":0,"r":"?"}]""", + """[{"id":1,"w":2,"b":1,"h":0,"r":"?"}]""" + ) assertTrue(possibleResults.contains(games.toString()), "pairing differs") + games = TestAPI.get("/api/tour/1/res/1") as Json.Array + assertTrue(possibleResults.contains(games.toString()), "results differs") + val empty = TestAPI.get("/api/tour/1/pair/1") as Json.Array + assertEquals("[]", empty.toString(), "no more pairables for round 1") } @Test - fun `006 team tournament`() { + fun `006 result`() { + val resp = TestAPI.put("/api/tour/1/res/1", Json.parse("""{"id":1,"result":"b"}""")) as Json.Object + assertTrue(resp.getBoolean("success") == true, "expecting success") + val games = TestAPI.get("/api/tour/1/res/1") + val possibleResults = setOf( + """[{"id":1,"w":1,"b":2,"h":0,"r":"b"}]""", + """[{"id":1,"w":2,"b":1,"h":0,"r":"b"}]""" + ) + assertTrue(possibleResults.contains(games.toString()), "results differ") + } + + @Test + fun `007 team tournament, MacMahon`() { var resp = TestAPI.post("/api/tour", aTeamTournament) as Json.Object assertTrue(resp.getBoolean("success") == true, "expecting success") assertEquals(2, resp.getInt("id"), "expecting id #2 for new tournament") - resp = TestAPI.post("api/tour/2/part", aPlayer) as Json.Object + resp = TestAPI.post("/api/tour/2/part", aPlayer) as Json.Object assertTrue(resp.getBoolean("success") == true, "expecting success") assertEquals(3, resp.getInt("id"), "expecting id #3 for new player") - resp = TestAPI.post("api/tour/2/part", anotherPlayer) as Json.Object + resp = TestAPI.post("/api/tour/2/part", anotherPlayer) as Json.Object assertTrue(resp.getBoolean("success") == true, "expecting success") assertEquals(4, resp.getInt("id"), "expecting id #{ for new player") assertTrue(resp.getBoolean("success") == true, "expecting success") var arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array assertEquals("[]", arr.toString(), "expecting an empty array") - resp = TestAPI.post("api/tour/2/team", Json.parse("""{ "name":"The Buffallos", "players":[3, 4] }""") as Json.Object) as Json.Object + resp = TestAPI.post("/api/tour/2/team", Json.parse("""{ "name":"The Buffallos", "players":[3, 4] }""") as Json.Object) as Json.Object assertTrue(resp.getBoolean("success") == true, "expecting success") assertEquals(5, resp.getInt("id"), "expecting team id #5") - resp = TestAPI.get("api/tour/2/team/5") as Json.Object + resp = TestAPI.get("/api/tour/2/team/5") as Json.Object assertEquals("""{"id":5,"name":"The Buffallos","players":[3,4]}""", resp.toString(), "expecting team description") arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array assertEquals("[5]", arr.toString(), "expecting a singleton array") + // nothing stops us in reusing players in different teams, at least for now... + resp = TestAPI.post("/api/tour/2/team", Json.parse("""{ "name":"The Billies", "players":[3, 4] }""") as Json.Object) as Json.Object + assertTrue(resp.getBoolean("success") == true, "expecting success") + assertEquals(6, resp.getInt("id"), "expecting team id #6") + arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array + assertEquals("[5,6]", arr.toString(), "expecting two pairables") + arr = TestAPI.post("/api/tour/2/pair/1", Json.parse("""["all"]""")) as Json.Array + assertTrue(resp.getBoolean("success") == true, "expecting success") + val expected = """"["id":1,"w":5,"b":6,"h":3,"r":"?"]""" } }