Team handling debugging; basic MacMahon implementation

This commit is contained in:
Claude Brisson
2023-05-23 16:19:56 +02:00
parent cca9752f20
commit 74dcb64899
12 changed files with 219 additions and 94 deletions

View File

@@ -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))

View File

@@ -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)
}

View File

@@ -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())

View File

@@ -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 {
}
</Players>
<Games>
${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) ->
"""<Game blackPlayer="${
@@ -228,7 +230,7 @@ object OpenGotha {
Game.Result.BOTHLOOSE -> "RESULT_BOTHLOOSE"
}
}" roundNumber="${
round + 1
round
}" tableNumber="${
table + 1
}" whitePlayer="${

View File

@@ -2,6 +2,7 @@ package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import org.jeudego.pairgoth.model.Game.Result.*
import java.util.*
data class Game(
val id: Int,
@@ -10,7 +11,20 @@ data class Game(
val handicap: Int = 0,
var result: Result = UNKNOWN
) {
enum class Result(val symbol: Char) { UNKNOWN('?'), BLACK('b'), WHITE('w'), JIGO('='), CANCELLED('x'), BOTHWIN('+'), BOTHLOOSE('-') }
enum class Result(val symbol: Char) {
UNKNOWN('?'),
BLACK('b'),
WHITE('w'),
JIGO('='),
CANCELLED('X'),
BOTHWIN('#'),
BOTHLOOSE('0');
companion object {
private val byChar = Result.values().associateBy { it.symbol }
fun fromSymbol(c: Char) = byChar[c] ?: throw Error("unknown result symbol: $c")
}
}
}
// serialization
@@ -19,5 +33,6 @@ fun Game.toJson() = Json.Object(
"id" to id,
"w" to white,
"b" to black,
"h" to handicap,
"r" to "${result.symbol}"
)

View File

@@ -12,6 +12,8 @@ import kotlin.math.roundToInt
sealed class Pairable(val id: Int, val name: String, open val rating: Int, open val rank: Int) {
companion object {}
abstract fun toJson(): Json.Object
abstract val club: String?
abstract val country: String?
val skip = mutableSetOf<Int>() // 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 ...)

View File

@@ -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 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 = 100.0 // per color unbalancing
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<Pairable>): List<Game>
}
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<Pairable>): List<Game> {
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<Int>()
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
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)
}

View File

@@ -56,11 +56,17 @@ sealed class Tournament <P: Pairable>(
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<MutableMap<Int, Game>>()
private val games = mutableListOf<MutableMap<Int, Game>>()
fun games(round: Int) = games.getOrNull(round - 1) ?: mutableMapOf()
fun lastRound() = games.size
// standings criteria
val criteria = mutableListOf<Criterion>(
@@ -116,10 +122,10 @@ class TeamTournament(
inner class Team(id: Int, name: String): Pairable(id, name, 0, 0) {
val playerIds = mutableSetOf<Int>()
val teamPlayers: Set<Player> 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,

View File

@@ -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<Game>, pairables: List<Pairable>, 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()
}

View File

@@ -20,7 +20,12 @@ sealed class Solver(val history: List<Game>, val pairables: List<Pairable>, 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<Game> {
// 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<Game> {
// check that at this stage, we have an even number of pairables
@@ -30,16 +35,16 @@ sealed class Solver(val history: List<Game>, val pairables: List<Pairable>, 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<Game>, val pairables: List<Pairable>, val
}
// score (number of wins)
val Pairable.score: Int get() = _score[id] ?: 0
private val _score: Map<Int, Int> by lazy {
history.mapNotNull { game ->
val Pairable.score: Double get() = _score[id] ?: 0.0
private val _score: Map<Int, Double> by lazy {
mutableMapOf<Int, Double>().apply {
history.forEach { game ->
when (game.result) {
BLACK -> game.black
WHITE -> game.white
else -> null
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
}
}

View File

@@ -11,24 +11,29 @@ class SwissSolver(history: List<Game>, pairables: List<Pairable>, 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
}
else -> when (method) {
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 (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
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
}
}
weight += (abs(black.colorBalance + 1) + abs(white.colorBalance - 1)) * weights.color
return weight
}
} + (abs(p.colorBalance + 1) + abs(q.colorBalance - 1)) * weights.color
}

View File

@@ -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":"?"]"""
}
}