Team handling debugging; basic MacMahon implementation
This commit is contained in:
@@ -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))
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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())
|
||||
|
@@ -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="${
|
||||
|
@@ -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}"
|
||||
)
|
||||
|
@@ -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 ...)
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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()
|
||||
|
||||
}
|
@@ -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
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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":"?"]"""
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user