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? {
|
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||||
val tournament = getTournament(request)
|
val tournament = getTournament(request)
|
||||||
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
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)
|
listOf(it.black, it.white)
|
||||||
}.toSet()
|
}.toSet()
|
||||||
return tournament.pairables.values.filter { !it.skip.contains(round) && !playing.contains(it.id) }.map { it.id }.toJsonArray()
|
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 payload = getArrayPayload(request)
|
||||||
val allPlayers = payload.size == 1 && payload[0] == "all"
|
val allPlayers = payload.size == 1 && payload[0] == "all"
|
||||||
if (!allPlayers && tournament.pairing.type == Pairing.PairingType.SWISS) badRequest("Swiss pairing requires all pairable players")
|
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)
|
listOf(it.black, it.white)
|
||||||
}.toSet()
|
}.toSet()
|
||||||
val pairables =
|
val pairables =
|
||||||
@@ -52,15 +52,15 @@ object PairingHandler: PairgothApiHandler {
|
|||||||
val tournament = getTournament(request)
|
val tournament = getTournament(request)
|
||||||
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
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...)
|
// 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 payload = getArrayPayload(request)
|
||||||
val allPlayers = payload.size == 1 && payload[0] == "all"
|
val allPlayers = payload.size == 1 && payload[0] == "all"
|
||||||
if (allPlayers) {
|
if (allPlayers) {
|
||||||
tournament.games.removeLast()
|
tournament.games(round).clear()
|
||||||
} else {
|
} else {
|
||||||
payload.forEach {
|
payload.forEach {
|
||||||
val id = (it as Number).toInt()
|
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))
|
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? {
|
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||||
val tournament = getTournament(request)
|
val tournament = getTournament(request)
|
||||||
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
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()
|
return games.map { it.toJson() }.toJsonArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@ object ResultsHandler: PairgothApiHandler {
|
|||||||
val tournament = getTournament(request)
|
val tournament = getTournament(request)
|
||||||
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
||||||
val payload = getObjectPayload(request)
|
val payload = getObjectPayload(request)
|
||||||
val game = tournament.games[round][payload.getInt("id")] ?: badRequest("invalid game id")
|
val game = tournament.games(round)[payload.getInt("id")] ?: badRequest("invalid game id")
|
||||||
game.result = Game.Result.valueOf(payload.getString("result") ?: badRequest("missing result"))
|
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))
|
Event.dispatch(Event.resultUpdated, Json.Object("tournament" to tournament.id, "round" to round, "data" to game))
|
||||||
return Json.Object("success" to true)
|
return Json.Object("success" to true)
|
||||||
}
|
}
|
||||||
|
@@ -54,11 +54,15 @@ object TournamentHandler: PairgothApiHandler {
|
|||||||
// disallow changing type
|
// disallow changing type
|
||||||
if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed")
|
if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed")
|
||||||
val updated = Tournament.fromJson(payload, tournament)
|
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)
|
updated.players.putAll(tournament.players)
|
||||||
if (tournament is TeamTournament && updated is TeamTournament) {
|
if (tournament is TeamTournament && updated is TeamTournament) {
|
||||||
updated.teams.putAll(tournament.teams)
|
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)
|
updated.criteria.addAll(tournament.criteria)
|
||||||
Store.replaceTournament(updated)
|
Store.replaceTournament(updated)
|
||||||
Event.dispatch(tournamentUpdated, tournament.toJson())
|
Event.dispatch(tournamentUpdated, tournament.toJson())
|
||||||
|
@@ -173,7 +173,9 @@ object OpenGotha {
|
|||||||
)
|
)
|
||||||
}.associateBy { it.id }.toMutableMap()
|
}.associateBy { it.id }.toMutableMap()
|
||||||
}
|
}
|
||||||
tournament.games.addAll(gamesPerRound)
|
gamesPerRound.forEachIndexed { index, games ->
|
||||||
|
tournament.games(index).putAll(games)
|
||||||
|
}
|
||||||
return tournament
|
return tournament
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,9 +211,9 @@ object OpenGotha {
|
|||||||
}
|
}
|
||||||
</Players>
|
</Players>
|
||||||
<Games>
|
<Games>
|
||||||
${tournament.games.flatMapIndexed { round, games ->
|
${(1..tournament.lastRound()).map { tournament.games(it) }.flatMapIndexed { index, games ->
|
||||||
games.values.mapIndexed { table, game ->
|
games.values.mapIndexed { table, game ->
|
||||||
Triple(round, table , game)
|
Triple(index + 1, table , game)
|
||||||
}
|
}
|
||||||
}.joinToString("\n") { (round, table, game) ->
|
}.joinToString("\n") { (round, table, game) ->
|
||||||
"""<Game blackPlayer="${
|
"""<Game blackPlayer="${
|
||||||
@@ -228,7 +230,7 @@ object OpenGotha {
|
|||||||
Game.Result.BOTHLOOSE -> "RESULT_BOTHLOOSE"
|
Game.Result.BOTHLOOSE -> "RESULT_BOTHLOOSE"
|
||||||
}
|
}
|
||||||
}" roundNumber="${
|
}" roundNumber="${
|
||||||
round + 1
|
round
|
||||||
}" tableNumber="${
|
}" tableNumber="${
|
||||||
table + 1
|
table + 1
|
||||||
}" whitePlayer="${
|
}" whitePlayer="${
|
||||||
|
@@ -2,6 +2,7 @@ package org.jeudego.pairgoth.model
|
|||||||
|
|
||||||
import com.republicate.kson.Json
|
import com.republicate.kson.Json
|
||||||
import org.jeudego.pairgoth.model.Game.Result.*
|
import org.jeudego.pairgoth.model.Game.Result.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
data class Game(
|
data class Game(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
@@ -10,7 +11,20 @@ data class Game(
|
|||||||
val handicap: Int = 0,
|
val handicap: Int = 0,
|
||||||
var result: Result = UNKNOWN
|
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
|
// serialization
|
||||||
@@ -19,5 +33,6 @@ fun Game.toJson() = Json.Object(
|
|||||||
"id" to id,
|
"id" to id,
|
||||||
"w" to white,
|
"w" to white,
|
||||||
"b" to black,
|
"b" to black,
|
||||||
|
"h" to handicap,
|
||||||
"r" to "${result.symbol}"
|
"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) {
|
sealed class Pairable(val id: Int, val name: String, open val rating: Int, open val rank: Int) {
|
||||||
companion object {}
|
companion object {}
|
||||||
abstract fun toJson(): Json.Object
|
abstract fun toJson(): Json.Object
|
||||||
|
abstract val club: String?
|
||||||
|
abstract val country: String?
|
||||||
val skip = mutableSetOf<Int>() // skipped rounds
|
val skip = mutableSetOf<Int>() // skipped rounds
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,13 +21,15 @@ object ByePlayer: Pairable(0, "bye", 0, Int.MIN_VALUE) {
|
|||||||
override fun toJson(): Json.Object {
|
override fun toJson(): Json.Object {
|
||||||
throw Error("bye player should never be serialized")
|
throw Error("bye player should never be serialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val club = "none"
|
||||||
|
override val country = "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Pairable.displayRank(): String = when {
|
fun Pairable.displayRank(): String = when {
|
||||||
rank < 0 -> "${-rank}k"
|
rank < 0 -> "${-rank}k"
|
||||||
rank >= 0 && rank < 10 -> "${rank + 1}d"
|
rank < 10 -> "${rank + 1}d"
|
||||||
rank >= 10 -> "${rank - 9}p"
|
else -> "${rank - 9}p"
|
||||||
else -> throw Error("impossible")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val rankRegex = Regex("(\\d+)([kdp])", RegexOption.IGNORE_CASE)
|
private val rankRegex = Regex("(\\d+)([kdp])", RegexOption.IGNORE_CASE)
|
||||||
@@ -50,8 +54,8 @@ class Player(
|
|||||||
var firstname: String,
|
var firstname: String,
|
||||||
rating: Int,
|
rating: Int,
|
||||||
rank: Int,
|
rank: Int,
|
||||||
var country: String,
|
override var country: String,
|
||||||
var club: String
|
override var club: String
|
||||||
): Pairable(id, name, rating, rank) {
|
): Pairable(id, name, rating, rank) {
|
||||||
companion object
|
companion object
|
||||||
// used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...)
|
// 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.MacMahon
|
||||||
import org.jeudego.pairgoth.model.RoundRobin
|
import org.jeudego.pairgoth.model.RoundRobin
|
||||||
import org.jeudego.pairgoth.model.Swiss
|
import org.jeudego.pairgoth.model.Swiss
|
||||||
|
import org.jeudego.pairgoth.pairing.MacMahonSolver
|
||||||
import org.jeudego.pairgoth.pairing.SwissSolver
|
import org.jeudego.pairgoth.pairing.SwissSolver
|
||||||
import java.util.Random
|
import java.util.Random
|
||||||
|
|
||||||
// TODO - this is only an early draft
|
|
||||||
|
|
||||||
sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) {
|
sealed class Pairing(val type: PairingType, val weights: Weights = Weights()) {
|
||||||
companion object {}
|
companion object {}
|
||||||
enum class PairingType { SWISS, MACMAHON, ROUNDROBIN }
|
enum class PairingType { SWISS, MACMAHON, ROUNDROBIN }
|
||||||
data class Weights(
|
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 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 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>
|
abstract fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game>
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Tournament<*>.historyBefore(round: Int) =
|
fun Tournament<*>.historyBefore(round: Int) =
|
||||||
if (games.isEmpty()) emptyList()
|
if (lastRound() == 0) emptyList()
|
||||||
else games.slice(0 until round).flatMap { it.values }
|
else (0 until round).flatMap { games(round).values }
|
||||||
|
|
||||||
class Swiss(
|
class Swiss(
|
||||||
var method: Method,
|
var method: Method,
|
||||||
var firstRoundMethod: Method = method
|
var firstRoundMethod: Method = method,
|
||||||
): Pairing(SWISS) {
|
): 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 }
|
enum class Method { SPLIT_AND_FOLD, SPLIT_AND_RANDOM, SPLIT_AND_SLIP }
|
||||||
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||||
val actualMethod = if (round == 1) firstRoundMethod else method
|
val actualMethod = if (round == 1) firstRoundMethod else method
|
||||||
@@ -41,12 +48,13 @@ class Swiss(
|
|||||||
|
|
||||||
class MacMahon(
|
class MacMahon(
|
||||||
var bar: Int = 0,
|
var bar: Int = 0,
|
||||||
var minLevel: Int = -30
|
var minLevel: Int = -30,
|
||||||
|
var reducer: Int = 1
|
||||||
): Pairing(MACMAHON) {
|
): Pairing(MACMAHON) {
|
||||||
val groups = mutableListOf<Int>()
|
val groups = mutableListOf<Int>()
|
||||||
|
|
||||||
override fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
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(
|
MACMAHON -> MacMahon(
|
||||||
bar = json.getInt("bar") ?: 0,
|
bar = json.getInt("bar") ?: 0,
|
||||||
minLevel = json.getInt("minLevel") ?: -30
|
minLevel = json.getInt("minLevel") ?: -30,
|
||||||
|
reducer = json.getInt("reducer") ?: 1
|
||||||
)
|
)
|
||||||
ROUNDROBIN -> RoundRobin()
|
ROUNDROBIN -> RoundRobin()
|
||||||
}
|
}
|
||||||
@@ -74,7 +83,7 @@ fun Pairing.toJson() = when (this) {
|
|||||||
is Swiss ->
|
is Swiss ->
|
||||||
if (method == firstRoundMethod) Json.Object("type" to type.name, "method" to method.name)
|
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)
|
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)
|
is RoundRobin -> Json.Object("type" to type.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -56,11 +56,17 @@ sealed class Tournament <P: Pairable>(
|
|||||||
val evenPairables =
|
val evenPairables =
|
||||||
if (pairables.size % 2 == 0) pairables
|
if (pairables.size % 2 == 0) pairables
|
||||||
else pairables.toMutableList().also { it.add(ByePlayer) }
|
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
|
// 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
|
// standings criteria
|
||||||
val criteria = mutableListOf<Criterion>(
|
val criteria = mutableListOf<Criterion>(
|
||||||
@@ -116,10 +122,10 @@ class TeamTournament(
|
|||||||
inner class Team(id: Int, name: String): Pairable(id, name, 0, 0) {
|
inner class Team(id: Int, name: String): Pairable(id, name, 0, 0) {
|
||||||
val playerIds = mutableSetOf<Int>()
|
val playerIds = mutableSetOf<Int>()
|
||||||
val teamPlayers: Set<Player> get() = playerIds.mapNotNull { players[id] }.toSet()
|
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 rating: Int get() = if (teamPlayers.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()
|
override val rank: Int get() = if (teamPlayers.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 }
|
override val club: String? get() = teamPlayers.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 country: String? get() = teamPlayers.map { country }.distinct().let { if (it.size == 1) it[0] else null }
|
||||||
override fun toJson() = Json.Object(
|
override fun toJson() = Json.Object(
|
||||||
"id" to id,
|
"id" to id,
|
||||||
"name" to name,
|
"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
|
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> {
|
fun pair(): List<Game> {
|
||||||
// check that at this stage, we have an even number of pairables
|
// 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) {
|
for (j in i + 1 until n) {
|
||||||
val p = pairables[i]
|
val p = pairables[i]
|
||||||
val q = pairables[j]
|
val q = pairables[j]
|
||||||
builder.addEdge(p, q, weight(p, q))
|
weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it) }
|
||||||
builder.addEdge(q, p, weight(q, p))
|
weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val graph = builder.build()
|
val graph = builder.build()
|
||||||
val matching = KolmogorovWeightedPerfectMatching(graph, ObjectiveSense.MINIMIZE)
|
val matching = KolmogorovWeightedPerfectMatching(graph, ObjectiveSense.MINIMIZE)
|
||||||
val solution = matching.matching
|
val solution = matching.matching
|
||||||
|
|
||||||
val result = solution.map {
|
val result = solution.flatMap {
|
||||||
Game(Store.nextGameId, graph.getEdgeSource(it).id , graph.getEdgeTarget(it).id)
|
games(black = graph.getEdgeSource(it) , white = graph.getEdgeTarget(it))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -94,49 +99,55 @@ sealed class Solver(val history: List<Game>, val pairables: List<Pairable>, val
|
|||||||
}
|
}
|
||||||
|
|
||||||
// score (number of wins)
|
// score (number of wins)
|
||||||
val Pairable.score: Int get() = _score[id] ?: 0
|
val Pairable.score: Double get() = _score[id] ?: 0.0
|
||||||
private val _score: Map<Int, Int> by lazy {
|
private val _score: Map<Int, Double> by lazy {
|
||||||
history.mapNotNull { game ->
|
mutableMapOf<Int, Double>().apply {
|
||||||
|
history.forEach { game ->
|
||||||
when (game.result) {
|
when (game.result) {
|
||||||
BLACK -> game.black
|
BLACK -> put(game.black, getOrDefault(game.black, 0.0) + 1.0)
|
||||||
WHITE -> game.white
|
WHITE -> put(game.white, getOrDefault(game.white, 0.0) + 1.0)
|
||||||
else -> null
|
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
|
// sos
|
||||||
val Pairable.sos: Int get() = _sos[id] ?: 0
|
val Pairable.sos: Double get() = _sos[id] ?: 0.0
|
||||||
private val _sos by lazy {
|
private val _sos by lazy {
|
||||||
(history.map { game ->
|
(history.map { game ->
|
||||||
Pair(game.black, _score[game.white] ?: 0)
|
Pair(game.black, _score[game.white] ?: 0.0)
|
||||||
} + history.map { game ->
|
} + history.map { game ->
|
||||||
Pair(game.white, _score[game.black] ?: 0)
|
Pair(game.white, _score[game.black] ?: 0.0)
|
||||||
}).groupingBy { it.first }.fold(0) { acc, next ->
|
}).groupingBy { it.first }.fold(0.0) { acc, next ->
|
||||||
acc + next.second
|
acc + next.second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sosos
|
// sosos
|
||||||
val Pairable.sosos: Int get() = _sosos[id] ?: 0
|
val Pairable.sosos: Double get() = _sosos[id] ?: 0.0
|
||||||
private val _sosos by lazy {
|
private val _sosos by lazy {
|
||||||
(history.map { game ->
|
(history.map { game ->
|
||||||
Pair(game.black, _sos[game.white] ?: 0)
|
Pair(game.black, _sos[game.white] ?: 0.0)
|
||||||
} + history.map { game ->
|
} + history.map { game ->
|
||||||
Pair(game.white, _sos[game.black] ?: 0)
|
Pair(game.white, _sos[game.black] ?: 0.0)
|
||||||
}).groupingBy { it.first }.fold(0) { acc, next ->
|
}).groupingBy { it.first }.fold(0.0) { acc, next ->
|
||||||
acc + next.second
|
acc + next.second
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sodos
|
// sodos
|
||||||
val Pairable.sodos: Int get() = _sodos[id] ?: 0
|
val Pairable.sodos: Double get() = _sodos[id] ?: 0.0
|
||||||
private val _sodos by lazy {
|
private val _sodos by lazy {
|
||||||
(history.map { game ->
|
(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 ->
|
} + history.map { game ->
|
||||||
Pair(game.white, if (game.result == WHITE) _score[game.black] ?: 0 else 0)
|
Pair(game.white, if (game.result == WHITE) _score[game.black] ?: 0.0 else 0.0)
|
||||||
}).groupingBy { it.first }.fold(0) { acc, next ->
|
}).groupingBy { it.first }.fold(0.0) { acc, next ->
|
||||||
acc + next.second
|
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 =
|
override fun sort(p: Pairable, q: Pairable): Int =
|
||||||
when (p.score) {
|
when (p.score) {
|
||||||
q.score -> p.rating - q.rating
|
q.score -> q.rating - p.rating
|
||||||
else -> p.score - q.score
|
else -> ((q.score - p.score) * 1000).toInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun weight(p: Pairable, q: Pairable) = when {
|
override fun weight(black: Pairable, white: Pairable): Double {
|
||||||
p.played(q) -> weights.played
|
var weight = 0.0
|
||||||
p.score != q.score -> {
|
if (black.played(white)) weight += weights.played
|
||||||
|
if (black.score != white.score) {
|
||||||
val placeWeight =
|
val placeWeight =
|
||||||
if (p.score > q.score) (p.placeInGroup.second + q.placeInGroup.first) * weights.place
|
if (black.score > white.score) (black.placeInGroup.second + white.placeInGroup.first) * weights.place
|
||||||
else (q.placeInGroup.second + p.placeInGroup.first) * weights.place
|
else (white.placeInGroup.second + black.placeInGroup.first) * weights.place
|
||||||
abs(p.score - q.score) * weights.score + placeWeight
|
weight += abs(black.score - white.score) * weights.score + placeWeight
|
||||||
}
|
} else {
|
||||||
else -> when (method) {
|
weight += when (method) {
|
||||||
SPLIT_AND_FOLD ->
|
SPLIT_AND_FOLD ->
|
||||||
if (p.placeInGroup.first > q.placeInGroup.first) abs(p.placeInGroup.first - (q.placeInGroup.second - q.placeInGroup.first)) * weights.place
|
if (black.placeInGroup.first > white.placeInGroup.first) abs(black.placeInGroup.first - (white.placeInGroup.second - white.placeInGroup.first)) * weights.place
|
||||||
else abs(q.placeInGroup.first - (p.placeInGroup.second - p.placeInGroup.first)) * weights.place
|
else abs(white.placeInGroup.first - (black.placeInGroup.second - black.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
|
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
|
package org.jeudego.pairgoth.test
|
||||||
|
|
||||||
import com.republicate.kson.Json
|
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.Test
|
||||||
import org.junit.jupiter.api.TestInstance
|
import org.junit.jupiter.api.TestInstance
|
||||||
import org.junit.jupiter.api.TestMethodOrder
|
import org.junit.jupiter.api.TestMethodOrder
|
||||||
import java.util.Objects
|
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
|
||||||
@TestMethodOrder(Alphanumeric::class)
|
@TestMethodOrder(MethodName::class)
|
||||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
class BasicTests: TestBase() {
|
class BasicTests: TestBase() {
|
||||||
|
|
||||||
@@ -51,8 +50,7 @@ class BasicTests: TestBase() {
|
|||||||
),
|
),
|
||||||
"rounds" to 2,
|
"rounds" to 2,
|
||||||
"pairing" to Json.Object(
|
"pairing" to Json.Object(
|
||||||
"type" to "SWISS",
|
"type" to "MACMAHON"
|
||||||
"method" to "SPLIT_AND_RANDOM"
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +58,7 @@ class BasicTests: TestBase() {
|
|||||||
"name" to "Burma",
|
"name" to "Burma",
|
||||||
"firstname" to "Nestor",
|
"firstname" to "Nestor",
|
||||||
"rating" to 1600,
|
"rating" to 1600,
|
||||||
"rank" to -2,
|
"rank" to -5,
|
||||||
"country" to "FR",
|
"country" to "FR",
|
||||||
"club" to "13Ma"
|
"club" to "13Ma"
|
||||||
)
|
)
|
||||||
@@ -114,33 +112,59 @@ class BasicTests: TestBase() {
|
|||||||
fun `005 pair`() {
|
fun `005 pair`() {
|
||||||
val resp = TestAPI.post("/api/tour/1/part", anotherPlayer) as Json.Object
|
val resp = TestAPI.post("/api/tour/1/part", anotherPlayer) as Json.Object
|
||||||
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
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(
|
val possibleResults = setOf(
|
||||||
"[{\"id\":1,\"w\":1,\"b\":2,\"r\":\"?\"}]",
|
"""[{"id":1,"w":1,"b":2,"h":0,"r":"?"}]""",
|
||||||
"[{\"id\":1,\"w\":2,\"b\":1,\"r\":\"?\"}]")
|
"""[{"id":1,"w":2,"b":1,"h":0,"r":"?"}]"""
|
||||||
|
)
|
||||||
assertTrue(possibleResults.contains(games.toString()), "pairing differs")
|
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
|
@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
|
var resp = TestAPI.post("/api/tour", aTeamTournament) as Json.Object
|
||||||
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
||||||
assertEquals(2, resp.getInt("id"), "expecting id #2 for new tournament")
|
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")
|
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
||||||
assertEquals(3, resp.getInt("id"), "expecting id #3 for new player")
|
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")
|
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
||||||
assertEquals(4, resp.getInt("id"), "expecting id #{ for new player")
|
assertEquals(4, resp.getInt("id"), "expecting id #{ for new player")
|
||||||
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
||||||
var arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array
|
var arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array
|
||||||
assertEquals("[]", arr.toString(), "expecting an empty 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")
|
assertTrue(resp.getBoolean("success") == true, "expecting success")
|
||||||
assertEquals(5, resp.getInt("id"), "expecting team id #5")
|
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")
|
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
|
arr = TestAPI.get("/api/tour/2/pair/1") as Json.Array
|
||||||
assertEquals("[5]", arr.toString(), "expecting a singleton 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