Compare commits
10 Commits
be18f159be
...
b2f2e033dc
Author | SHA1 | Date | |
---|---|---|---|
b2f2e033dc | |||
e9f2bafcdc | |||
![]() |
0cc34a1f84 | ||
![]() |
c3cb5826a3 | ||
![]() |
84ab78c461 | ||
![]() |
d47d4fc8cc | ||
![]() |
3d06588889 | ||
![]() |
f704f3adb2 | ||
![]() |
ecec6556d1 | ||
![]() |
17bb013feb |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,3 +8,6 @@ target
|
||||
*.iml
|
||||
*~
|
||||
pairgoth.db
|
||||
ratings
|
||||
pairgoth
|
||||
pairgoth.tar.gz
|
@@ -1,3 +1,5 @@
|
||||
# This is largely a mirror of the original, adapted for the Slovenian Go Association.
|
||||
|
||||
# Pairgoth
|
||||
|
||||
Welcome to Pairgoth, your Go Pairing Engine!
|
||||
|
@@ -7,61 +7,23 @@ import org.jeudego.pairgoth.model.MacMahon
|
||||
import org.jeudego.pairgoth.model.Pairable
|
||||
import org.jeudego.pairgoth.model.PairingType
|
||||
import org.jeudego.pairgoth.model.Player
|
||||
import org.jeudego.pairgoth.model.TeamTournament
|
||||
import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.model.getID
|
||||
import org.jeudego.pairgoth.model.historyBefore
|
||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
|
||||
// TODO CB avoid code redundancy with solvers
|
||||
|
||||
fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = false): List<Json.Object> {
|
||||
|
||||
fun Pairable.mmBase(): Double {
|
||||
if (pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon")
|
||||
return min(max(rank, pairing.mmFloor), pairing.mmBar) + MacMahonSolver.mmsZero + mmsCorrection
|
||||
}
|
||||
|
||||
fun roundScore(score: Double): Double {
|
||||
val epsilon = 0.00001
|
||||
// Note: this works for now because we only have .0 and .5 fractional parts
|
||||
return if (pairing.pairingParams.main.roundDownScore) floor(score + epsilon)
|
||||
else round(2 * score) / 2
|
||||
}
|
||||
|
||||
if (frozen != null) {
|
||||
return ArrayList(frozen!!.map { it -> it as Json.Object })
|
||||
}
|
||||
|
||||
// CB TODO - factorize history helper creation between here and solver classes
|
||||
val historyHelper = HistoryHelper(historyBefore(round + 1)) {
|
||||
if (pairing.type == PairingType.SWISS) {
|
||||
pairables.mapValues {
|
||||
Pair(0.0, wins[it.key] ?: 0.0)
|
||||
}
|
||||
}
|
||||
else {
|
||||
pairables.mapValues {
|
||||
it.value.let { pairable ->
|
||||
val mmBase = pairable.mmBase()
|
||||
val score = roundScore(mmBase +
|
||||
(nbW(pairable) ?: 0.0) +
|
||||
(1..round).map { round ->
|
||||
if (playersPerRound.getOrNull(round - 1)?.contains(pairable.id) == true) 0.0 else 1.0
|
||||
}.sum() * pairing.pairingParams.main.mmsValueAbsent)
|
||||
Pair(
|
||||
if (pairing.pairingParams.main.sosValueAbsentUseBase) mmBase
|
||||
else roundScore(mmBase + round/2),
|
||||
score
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val history = historyHelper(round + 1)
|
||||
|
||||
val neededCriteria = ArrayList(pairing.placementParams.criteria)
|
||||
if (!neededCriteria.contains(Criterion.NBW)) neededCriteria.add(Criterion.NBW)
|
||||
if (!neededCriteria.contains(Criterion.RATING)) neededCriteria.add(Criterion.RATING)
|
||||
@@ -72,24 +34,24 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
|
||||
Criterion.CATEGORY -> StandingsHandler.nullMap
|
||||
Criterion.RANK -> pairables.mapValues { it.value.rank }
|
||||
Criterion.RATING -> pairables.mapValues { it.value.rating }
|
||||
Criterion.NBW -> historyHelper.wins
|
||||
Criterion.MMS -> historyHelper.mms
|
||||
Criterion.SCOREX -> historyHelper.scoresX
|
||||
Criterion.NBW -> history.wins
|
||||
Criterion.MMS -> history.mms
|
||||
Criterion.SCOREX -> history.scoresX
|
||||
Criterion.STS -> StandingsHandler.nullMap
|
||||
Criterion.CPS -> StandingsHandler.nullMap
|
||||
|
||||
Criterion.SOSW -> historyHelper.sos
|
||||
Criterion.SOSWM1 -> historyHelper.sosm1
|
||||
Criterion.SOSWM2 -> historyHelper.sosm2
|
||||
Criterion.SODOSW -> historyHelper.sodos
|
||||
Criterion.SOSOSW -> historyHelper.sosos
|
||||
Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else historyHelper.cumScore
|
||||
Criterion.SOSM -> historyHelper.sos
|
||||
Criterion.SOSMM1 -> historyHelper.sosm1
|
||||
Criterion.SOSMM2 -> historyHelper.sosm2
|
||||
Criterion.SODOSM -> historyHelper.sodos
|
||||
Criterion.SOSOSM -> historyHelper.sosos
|
||||
Criterion.CUSSM -> historyHelper.cumScore
|
||||
Criterion.SOSW -> history.sos
|
||||
Criterion.SOSWM1 -> history.sosm1
|
||||
Criterion.SOSWM2 -> history.sosm2
|
||||
Criterion.SODOSW -> history.sodos
|
||||
Criterion.SOSOSW -> history.sosos
|
||||
Criterion.CUSSW -> if (round == 0) StandingsHandler.nullMap else history.cumScore
|
||||
Criterion.SOSM -> history.sos
|
||||
Criterion.SOSMM1 -> history.sosm1
|
||||
Criterion.SOSMM2 -> history.sosm2
|
||||
Criterion.SODOSM -> history.sodos
|
||||
Criterion.SOSOSM -> history.sosos
|
||||
Criterion.CUSSM -> history.cumScore
|
||||
|
||||
Criterion.SOSTS -> StandingsHandler.nullMap
|
||||
|
||||
@@ -100,14 +62,14 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
|
||||
Criterion.DC -> StandingsHandler.nullMap
|
||||
}
|
||||
}
|
||||
val pairables = pairables.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
|
||||
pairables.forEach { player ->
|
||||
val jsonPairables = pairables.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
|
||||
jsonPairables.forEach { player ->
|
||||
for (crit in criteria) {
|
||||
player[crit.first] = crit.second[player.getID()] ?: 0.0
|
||||
}
|
||||
player["results"] = Json.MutableArray(List(round) { "0=" })
|
||||
}
|
||||
val sortedPairables = pairables.sortedWith { left, right ->
|
||||
val sortedPairables = jsonPairables.sortedWith { left, right ->
|
||||
for (crit in criteria) {
|
||||
val lval = left.getDouble(crit.first) ?: 0.0
|
||||
val rval = right.getDouble(crit.first) ?: 0.0
|
||||
@@ -129,15 +91,16 @@ fun Tournament<*>.getSortedPairables(round: Int, includePreliminary: Boolean = f
|
||||
return sortedPairables
|
||||
}
|
||||
|
||||
fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: Int = rounds) {
|
||||
val sortedMap = sortedPairables.associateBy {
|
||||
fun Tournament<*>.populateStandings(sortedEntries: List<Json.Object>, round: Int = rounds, individualStandings: Boolean) {
|
||||
val sortedMap = sortedEntries.associateBy {
|
||||
it.getID()!!
|
||||
}
|
||||
|
||||
// refresh name, firstname, club and level
|
||||
val refMap = if (individualStandings) players else pairables
|
||||
sortedMap.forEach { (id, pairable) ->
|
||||
val mutable = pairable as Json.MutableObject
|
||||
pairables[id]?.let {
|
||||
refMap[id]?.let {
|
||||
mutable["name"] = it.name
|
||||
if (it is Player) {
|
||||
mutable["firstname"] = it.firstname
|
||||
@@ -150,7 +113,8 @@ fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: I
|
||||
|
||||
// fill result
|
||||
for (r in 1..round) {
|
||||
games(r).values.forEach { game ->
|
||||
val roundGames = if (individualStandings) individualGames(r) else games(r)
|
||||
roundGames.values.forEach { game ->
|
||||
val white = if (game.white != 0) sortedMap[game.white] else null
|
||||
val black = if (game.black != 0) sortedMap[game.black] else null
|
||||
val whiteNum = white?.getInt("num") ?: 0
|
||||
@@ -186,3 +150,54 @@ fun Tournament<*>.populateStandings(sortedPairables: List<Json.Object>, round: I
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun TeamTournament.getSortedTeamMembers(round: Int, includePreliminary: Boolean = false): List<Json.Object> {
|
||||
|
||||
val teamGames = historyBefore(round + 1)
|
||||
val individualHistory = teamGames.map { roundTeamGames ->
|
||||
roundTeamGames.flatMap { game -> individualGames[game.id]?.toList() ?: listOf() }
|
||||
}
|
||||
val historyHelper = HistoryHelper(individualHistory).apply {
|
||||
scoresFactory = { wins }
|
||||
}
|
||||
val neededCriteria = mutableListOf(Criterion.NBW, Criterion.RATING)
|
||||
val criteria = neededCriteria.map { crit ->
|
||||
crit.name to when (crit) {
|
||||
Criterion.NBW -> historyHelper.wins
|
||||
Criterion.RANK -> pairables.mapValues { it.value.rank }
|
||||
Criterion.RATING -> pairables.mapValues { it.value.rating }
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
val jsonPlayers = players.values.filter { includePreliminary || it.final }.map { it.toDetailedJson() }
|
||||
jsonPlayers.forEach { player ->
|
||||
for (crit in criteria) {
|
||||
player[crit.first] = crit.second?.get(player.getID()) ?: 0.0
|
||||
}
|
||||
player["results"] = Json.MutableArray(List(round) { "0=" })
|
||||
}
|
||||
val sortedPlayers = jsonPlayers.sortedWith { left, right ->
|
||||
for (crit in criteria) {
|
||||
val lval = left.getDouble(crit.first) ?: 0.0
|
||||
val rval = right.getDouble(crit.first) ?: 0.0
|
||||
val cmp = lval.compareTo(rval)
|
||||
if (cmp != 0) return@sortedWith -cmp
|
||||
}
|
||||
return@sortedWith 0
|
||||
}.mapIndexed() { i, obj ->
|
||||
obj.set("num", i+1)
|
||||
}
|
||||
var place = 1
|
||||
sortedPlayers.groupBy { p ->
|
||||
Triple(
|
||||
criteria.getOrNull(0)?.first?.let { crit -> p.getDouble(crit) ?: 0.0 } ?: 0.0,
|
||||
criteria.getOrNull(1)?.first?.let { crit -> p.getDouble(crit) ?: 0.0 } ?: 0.0,
|
||||
criteria.getOrNull(2)?.first?.let { crit -> p.getDouble(crit) ?: 0.0 } ?: 0.0
|
||||
)
|
||||
}.forEach {
|
||||
it.value.forEach { p -> p["place"] = place }
|
||||
place += it.value.size
|
||||
}
|
||||
|
||||
return sortedPlayers
|
||||
}
|
||||
|
@@ -0,0 +1,68 @@
|
||||
package org.jeudego.pairgoth.api
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import com.republicate.kson.toJsonArray
|
||||
import com.republicate.kson.toJsonObject
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.model.toJson
|
||||
import org.jeudego.pairgoth.pairing.solver.CollectingListener
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
object ExplainHandler: PairgothApiHandler {
|
||||
|
||||
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||
val tournament = getTournament(request)
|
||||
val round = getSubSelector(request)?.toIntOrNull() ?: badRequest("invalid round number")
|
||||
if (round > tournament.lastRound() + 1) badRequest("invalid round: previous round has not been played")
|
||||
val paired = tournament.games(round).values.flatMap {
|
||||
listOf(it.black, it.white)
|
||||
}.filter {
|
||||
it != 0
|
||||
}.map {
|
||||
tournament.pairables[it] ?: throw Error("Unknown pairable ID: $it")
|
||||
}
|
||||
val games = tournament.games(round).map { it.value }.toList()
|
||||
// build the scores map by redoing the whole pairing
|
||||
tournament.unpair(round)
|
||||
val history = tournament.historyHelper(round)
|
||||
val weightsCollector = CollectingListener()
|
||||
tournament.pair(round, paired, false, weightsCollector)
|
||||
val weights = weightsCollector.out
|
||||
// Since weights are generally in two groups towards the min and the max,
|
||||
// compute the max of the low group ("low") and the min of the high group ("high")
|
||||
// to improve coloring.
|
||||
// Total weights axis:
|
||||
// ----[min]xxxx[low]----[middle]----[high]xxxx[max]---->
|
||||
val min = weights.values.minOfOrNull { it.values.sum() } ?: 0.0
|
||||
val max = weights.values.maxOfOrNull { it.values.sum() } ?: 0.0
|
||||
val middle = (max - min) / 2.0
|
||||
val low = weights.values.map { it.values.sum() }.filter { it < middle }.maxOrNull() ?: middle
|
||||
val high = weights.values.map { it.values.sum() }.filter { it > middle }.minOrNull() ?: middle
|
||||
val ret = Json.Object(
|
||||
"paired" to paired.sortedByDescending { 1000 * (history.scores[it.id] ?: 0.0) + (history.sos[it.id] ?: 0.0) }.map {
|
||||
it.toMutableJson().apply {
|
||||
put("score", history.scores[it.id])
|
||||
put("wins", history.wins[it.id])
|
||||
put("sos", history.sos[it.id])
|
||||
put("dudd", history.drawnUpDown[it.id])
|
||||
}
|
||||
}.toJsonArray(),
|
||||
// "games" to games.map { it.toJson() }.toJsonArray(),
|
||||
"games" to games.associateBy { "${it.white}-${it.black}" }.mapValues { it.value.toJson() }.toJsonObject(),
|
||||
"weights" to weights.entries.map { (key, value) ->
|
||||
Pair(
|
||||
"${key.first}-${key.second}",
|
||||
value.also {
|
||||
it.put("total", it.values.sum())
|
||||
}
|
||||
)
|
||||
}.toJsonObject(),
|
||||
"min" to min,
|
||||
"low" to low,
|
||||
"high" to high,
|
||||
"max" to max
|
||||
)
|
||||
return ret
|
||||
}
|
||||
}
|
@@ -13,7 +13,10 @@ import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.model.getID
|
||||
import org.jeudego.pairgoth.model.toID
|
||||
import org.jeudego.pairgoth.model.toJson
|
||||
import org.jeudego.pairgoth.pairing.solver.LoggingListener
|
||||
import org.jeudego.pairgoth.server.Event.*
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@@ -67,7 +70,15 @@ object PairingHandler: PairgothApiHandler {
|
||||
if (playing.contains(it.id)) badRequest("pairable #$id already plays round $round")
|
||||
} ?: badRequest("invalid pairable id: #$id")
|
||||
}
|
||||
val games = tournament.pair(round, pairables)
|
||||
|
||||
// POST pair/$round accepts a few parameters to help tests
|
||||
val legacy = request.getParameter("legacy")?.toBoolean() ?: false
|
||||
val weightsLogger = request.getParameter("weights_output")?.let {
|
||||
val append = request.getParameter("append")?.toBoolean() ?: false
|
||||
LoggingListener(PrintWriter(FileWriter(it, append)))
|
||||
}
|
||||
|
||||
val games = tournament.pair(round, pairables, legacy, weightsLogger)
|
||||
|
||||
val ret = games.map { it.toJson() }.toJsonArray()
|
||||
tournament.dispatchEvent(GamesAdded, request, Json.Object("round" to round, "games" to ret))
|
||||
|
@@ -6,6 +6,7 @@ import org.jeudego.pairgoth.model.Criterion
|
||||
import org.jeudego.pairgoth.model.Criterion.*
|
||||
import org.jeudego.pairgoth.model.ID
|
||||
import org.jeudego.pairgoth.model.PairingType
|
||||
import org.jeudego.pairgoth.model.TeamTournament
|
||||
import org.jeudego.pairgoth.model.Tournament
|
||||
import org.jeudego.pairgoth.model.adjustedTime
|
||||
import org.jeudego.pairgoth.model.displayRank
|
||||
@@ -27,10 +28,18 @@ object StandingsHandler: PairgothApiHandler {
|
||||
override fun get(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||
val tournament = getTournament(request)
|
||||
val round = getSubSelector(request)?.toIntOrNull() ?: tournament.rounds
|
||||
val includePreliminary = request.getParameter("include_preliminary")?.let { it.toBoolean() } ?: false
|
||||
val includePreliminary = request.getParameter("include_preliminary")?.toBoolean() ?: false
|
||||
|
||||
val sortedPairables = tournament.getSortedPairables(round, includePreliminary)
|
||||
tournament.populateStandings(sortedPairables, round)
|
||||
val individualStandings = tournament is TeamTournament &&
|
||||
tournament.type.individual &&
|
||||
request.getParameter("individual_standings")?.toBoolean() == true
|
||||
|
||||
val sortedEntries = if (individualStandings) {
|
||||
tournament.getSortedTeamMembers(round)
|
||||
} else {
|
||||
tournament.getSortedPairables(round, includePreliminary)
|
||||
}
|
||||
tournament.populateStandings(sortedEntries, round, individualStandings)
|
||||
|
||||
val acceptHeader = request.getHeader("Accept") as String?
|
||||
val accept = acceptHeader?.substringBefore(";")
|
||||
@@ -44,7 +53,7 @@ object StandingsHandler: PairgothApiHandler {
|
||||
PrintWriter(OutputStreamWriter(response.outputStream, encoding))
|
||||
}
|
||||
return when (accept) {
|
||||
"application/json" -> sortedPairables.toJsonArray()
|
||||
"application/json" -> sortedEntries.toJsonArray()
|
||||
"application/egf" -> {
|
||||
response.contentType = "text/plain;charset=${encoding}"
|
||||
val neededCriteria = ArrayList(tournament.pairing.placementParams.criteria)
|
||||
@@ -52,19 +61,19 @@ object StandingsHandler: PairgothApiHandler {
|
||||
if (neededCriteria.first() == SCOREX) {
|
||||
neededCriteria.add(1, MMS)
|
||||
}
|
||||
exportToEGFFormat(tournament, sortedPairables, neededCriteria, writer)
|
||||
exportToEGFFormat(tournament, sortedEntries, neededCriteria, writer)
|
||||
writer.flush()
|
||||
return null
|
||||
}
|
||||
"application/ffg" -> {
|
||||
response.contentType = "text/plain;charset=${encoding}"
|
||||
exportToFFGFormat(tournament, sortedPairables, writer)
|
||||
exportToFFGFormat(tournament, sortedEntries, writer)
|
||||
writer.flush()
|
||||
return null
|
||||
}
|
||||
"text/csv" -> {
|
||||
response.contentType = "text/csv;charset=${encoding}"
|
||||
exportToCSVFormat(tournament, sortedPairables, writer)
|
||||
exportToCSVFormat(tournament, sortedEntries, writer)
|
||||
writer.flush()
|
||||
return null
|
||||
}
|
||||
@@ -123,7 +132,7 @@ ${
|
||||
player.getString("num")!!.padStart(4, ' ')
|
||||
} ${
|
||||
"${
|
||||
player.getString("name")?.toSnake(true)
|
||||
player.getString("name")?.toSnake()
|
||||
|
||||
} ${
|
||||
player.getString("firstname")?.toSnake() ?: ""
|
||||
|
@@ -4,8 +4,10 @@ import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.SPLIT_AND_SLIP
|
||||
import org.jeudego.pairgoth.model.PairingType.*
|
||||
import org.jeudego.pairgoth.pairing.solver.BaseSolver
|
||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||
import org.jeudego.pairgoth.pairing.solver.Solver
|
||||
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
||||
import org.jeudego.pairgoth.pairing.solver.PairingListener
|
||||
import org.jeudego.pairgoth.pairing.solver.SwissSolver
|
||||
import kotlin.math.min
|
||||
|
||||
@@ -131,28 +133,22 @@ sealed class Pairing(
|
||||
val pairingParams: PairingParams,
|
||||
val placementParams: PlacementParams) {
|
||||
companion object {}
|
||||
abstract fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): BaseSolver
|
||||
fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): List<Game> {
|
||||
return solver(tournament, round, pairables).pair()
|
||||
internal abstract fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): Solver
|
||||
internal fun pair(tournament: Tournament<*>, round: Int, pairables: List<Pairable>, legacyMode: Boolean = false, listener: PairingListener? = null): List<Game> {
|
||||
return solver(tournament, round, pairables)
|
||||
.also { solver ->
|
||||
solver.legacyMode = legacyMode
|
||||
listener?.let {
|
||||
solver.pairingListener = listener
|
||||
}
|
||||
}
|
||||
.pair()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun Tournament<*>.historyBefore(round: Int) =
|
||||
(1 until min(round, lastRound() + 1)).map { games(it).values.toList() }
|
||||
|
||||
/*private fun Tournament<*>.historyBefore(round: Int) : List<List<Game>> {
|
||||
println("Welcome to tournament.historyBefore !")
|
||||
println("lastround and round = "+lastRound().toString()+" "+round.toString())
|
||||
println((1 until round).map { it })
|
||||
println((1 until round).map { games(it).values.toList() })
|
||||
if (lastRound() == 1){
|
||||
return emptyList()
|
||||
}
|
||||
else {
|
||||
return (1 until round).map { games(it).values.toList() }
|
||||
}
|
||||
}*/
|
||||
|
||||
class Swiss(
|
||||
pairingParams: PairingParams = PairingParams(
|
||||
base = BaseCritParams(),
|
||||
@@ -175,7 +171,7 @@ class Swiss(
|
||||
): Pairing(SWISS, pairingParams, placementParams) {
|
||||
companion object {}
|
||||
override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>) =
|
||||
SwissSolver(round, tournament.rounds, tournament.historyBefore(round), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round))
|
||||
SwissSolver(round, tournament.rounds, HistoryHelper(tournament.historyBefore(round)), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round))
|
||||
}
|
||||
|
||||
class MacMahon(
|
||||
@@ -203,14 +199,14 @@ class MacMahon(
|
||||
): Pairing(MAC_MAHON, pairingParams, placementParams) {
|
||||
companion object {}
|
||||
override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>) =
|
||||
MacMahonSolver(round, tournament.rounds, tournament.historyBefore(round), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round), mmFloor, mmBar)
|
||||
MacMahonSolver(round, tournament.rounds, HistoryHelper(tournament.historyBefore(round)), pairables, tournament.pairables, pairingParams, placementParams, tournament.usedTables(round), mmFloor, mmBar)
|
||||
}
|
||||
|
||||
class RoundRobin(
|
||||
pairingParams: PairingParams = PairingParams(),
|
||||
placementParams: PlacementParams = PlacementParams(Criterion.NBW, Criterion.RATING)
|
||||
): Pairing(ROUND_ROBIN, pairingParams, placementParams) {
|
||||
override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): BaseSolver {
|
||||
override fun solver(tournament: Tournament<*>, round: Int, pairables: List<Pairable>): Solver {
|
||||
TODO("not implemented")
|
||||
}
|
||||
}
|
||||
|
@@ -7,7 +7,9 @@ import com.republicate.kson.toJsonObject
|
||||
//import kotlinx.datetime.LocalDate
|
||||
import java.time.LocalDate
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.logger
|
||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||
import org.jeudego.pairgoth.pairing.solver.MacMahonSolver
|
||||
import org.jeudego.pairgoth.pairing.solver.PairingListener
|
||||
import org.jeudego.pairgoth.store.nextGameId
|
||||
import org.jeudego.pairgoth.store.nextPlayerId
|
||||
import org.jeudego.pairgoth.store.nextTournamentId
|
||||
@@ -15,8 +17,10 @@ import org.jeudego.pairgoth.util.MutableBiMultiMap
|
||||
import org.jeudego.pairgoth.util.mutableBiMultiMapOf
|
||||
import kotlin.math.max
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.collections.get
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
sealed class Tournament <P: Pairable>(
|
||||
@@ -61,7 +65,7 @@ sealed class Tournament <P: Pairable>(
|
||||
var frozen: Json.Array? = null
|
||||
|
||||
// pairing
|
||||
open fun pair(round: Int, pairables: List<Pairable>): List<Game> {
|
||||
open fun pair(round: Int, pairables: List<Pairable>, legacyMode: Boolean = false, listener: PairingListener? = null): List<Game> {
|
||||
// Minimal check on round number.
|
||||
// CB TODO - the complete check should verify, for each player, that he was either non pairable or implied in the previous round
|
||||
if (round > games.size + 1) badRequest("previous round not paired")
|
||||
@@ -69,7 +73,7 @@ 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).also { newGames ->
|
||||
return pairing.pair(this, round, evenPairables, legacyMode, listener).also { newGames ->
|
||||
if (games.size < round) games.add(mutableMapOf())
|
||||
games[round - 1].putAll( newGames.associateBy { it.id } )
|
||||
}
|
||||
@@ -96,14 +100,17 @@ sealed class Tournament <P: Pairable>(
|
||||
|
||||
fun lastRound() = max(1, games.size)
|
||||
|
||||
/**
|
||||
* Recompute DUDD for a specific game
|
||||
*/
|
||||
fun recomputeDUDD(round: Int, gameID: ID) {
|
||||
// Instantiate solver with game history
|
||||
val solver = pairing.solver(this, round, pairables.values.toList())
|
||||
val solver = pairing.solver(this, round, emptyList())
|
||||
|
||||
// Recomputes DUDD and hd
|
||||
val game = games(round)[gameID]!!
|
||||
val white = solver.pairables.find { p-> p.id == game.white }!!
|
||||
val black = solver.pairables.find { p-> p.id == game.black }!!
|
||||
val white = pairables[game.white]!!
|
||||
val black = pairables[game.black]!!
|
||||
game.drawnUpDown = solver.dudd(black, white)
|
||||
game.handicap = solver.hd(white = white, black = black)
|
||||
}
|
||||
@@ -115,17 +122,16 @@ sealed class Tournament <P: Pairable>(
|
||||
fun recomputeDUDD(round: Int) {
|
||||
if (pairables.isEmpty() || games(1).isEmpty()) return;
|
||||
// Instantiate solver with game history
|
||||
val solver = pairing.solver(this, round, pairables.values.toList())
|
||||
val solver = pairing.solver(this, round, emptyList())
|
||||
for (game in games(round).values) {
|
||||
if (game.black != 0 && game.white != 0) {
|
||||
val white = solver.pairables.find { p-> p.id == game.white }!!
|
||||
val black = solver.pairables.find { p-> p.id == game.black }!!
|
||||
val white = pairables[game.white]!!
|
||||
val black = pairables[game.black]!!
|
||||
game.drawnUpDown = solver.dudd(black, white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recompute DUDD for all rounds
|
||||
*/
|
||||
@@ -208,6 +214,22 @@ sealed class Tournament <P: Pairable>(
|
||||
}
|
||||
return excluded
|
||||
}
|
||||
|
||||
fun roundScore(score: Double): Double {
|
||||
val epsilon = 0.00001
|
||||
// Note: this works for now because we only have .0 and .5 fractional parts
|
||||
return if (pairing.pairingParams.main.roundDownScore) floor(score + epsilon)
|
||||
else round(2 * score) / 2
|
||||
}
|
||||
|
||||
fun Pairable.mmBase(): Double {
|
||||
if (pairing !is MacMahon) throw Error("invalid call: tournament is not Mac Mahon")
|
||||
return min(max(rank, pairing.mmFloor), pairing.mmBar) + MacMahonSolver.mmsZero + mmsCorrection
|
||||
}
|
||||
|
||||
fun historyHelper(round: Int): HistoryHelper {
|
||||
return pairing.solver(this, round, emptyList()).history
|
||||
}
|
||||
}
|
||||
|
||||
// standard tournament of individuals
|
||||
@@ -263,7 +285,7 @@ class TeamTournament(
|
||||
override fun individualGames(round: Int): Map<ID, Game> {
|
||||
val teamGames = games(round)
|
||||
return if (type.individual) {
|
||||
return teamGames.values.flatMap { game ->
|
||||
teamGames.values.flatMap { game ->
|
||||
if (game.white == 0 || game.black == 0 ) listOf()
|
||||
else individualGames[game.id]?.toList() ?: listOf()
|
||||
}.associateBy { it.id }
|
||||
@@ -272,8 +294,8 @@ class TeamTournament(
|
||||
}
|
||||
}
|
||||
|
||||
override fun pair(round: Int, pairables: List<Pairable>) =
|
||||
super.pair(round, pairables).also { games ->
|
||||
override fun pair(round: Int, pairables: List<Pairable>, legacyMode: Boolean, listener: PairingListener?) =
|
||||
super.pair(round, pairables, legacyMode, listener).also { games ->
|
||||
if (type.individual) {
|
||||
games.forEach { game ->
|
||||
pairIndividualGames(round, game)
|
||||
|
@@ -5,26 +5,17 @@ import org.jeudego.pairgoth.model.*
|
||||
abstract class BasePairingHelper(
|
||||
val round: Int,
|
||||
val totalRounds: Int,
|
||||
history: List<List<Game>>, // History of all games played for each round
|
||||
val history: HistoryHelper, // Digested history of all games played for each round
|
||||
var pairables: List<Pairable>, // All pairables for this round, it may include the bye player
|
||||
val pairablesMap: Map<ID, Pairable>, // Map of all known pairables for this tournament
|
||||
val pairing: PairingParams,
|
||||
val placement: PlacementParams,
|
||||
) {
|
||||
|
||||
abstract val scores: Map<ID, Pair<Double, Double>>
|
||||
abstract val scoresX: Map<ID, Double>
|
||||
val historyHelper =
|
||||
if (pairables.first().let { it is TeamTournament.Team && it.teamOfIndividuals }) TeamOfIndividualsHistoryHelper(
|
||||
history
|
||||
) { scores }
|
||||
else HistoryHelper(history) { scores }
|
||||
|
||||
// Extend pairables with members from all rounds
|
||||
|
||||
// The main criterion that will be used to define the groups should be defined by subclasses
|
||||
// SOS and variants will be computed based on this score
|
||||
val Pairable.main: Double get() = scores[id]?.second ?: 0.0
|
||||
val Pairable.main: Double get() = score ?: 0.0
|
||||
abstract val mainLimits: Pair<Double, Double>
|
||||
|
||||
// pairables sorted using overloadable sort function
|
||||
@@ -84,28 +75,28 @@ abstract class BasePairingHelper(
|
||||
}
|
||||
|
||||
// already paired players map
|
||||
protected fun Pairable.played(other: Pairable) = historyHelper.playedTogether(this, other)
|
||||
protected fun Pairable.played(other: Pairable) = history.playedTogether(this, other)
|
||||
|
||||
// color balance (nw - nb)
|
||||
protected val Pairable.colorBalance: Int get() = historyHelper.colorBalance(this) ?: 0
|
||||
protected val Pairable.colorBalance: Int get() = history.colorBalance[id] ?: 0
|
||||
|
||||
protected val Pairable.group: Int get() = _groups[id]!!
|
||||
|
||||
protected val Pairable.drawnUpDown: Pair<Int, Int> get() = historyHelper.drawnUpDown(this) ?: Pair(0, 0)
|
||||
protected val Pairable.drawnUpDown: Pair<Int, Int> get() = history.drawnUpDown[id] ?: Pair(0, 0)
|
||||
|
||||
protected val Pairable.nbBye: Int get() = historyHelper.nbPlayedWithBye(this) ?: 0
|
||||
protected val Pairable.nbBye: Int get() = history.nbPlayedWithBye(this) ?: 0
|
||||
|
||||
// score (number of wins)
|
||||
val Pairable.nbW: Double get() = historyHelper.nbW(this) ?: 0.0
|
||||
|
||||
val Pairable.sos: Double get() = historyHelper.sos[id] ?: 0.0
|
||||
val Pairable.sosm1: Double get() = historyHelper.sosm1[id] ?: 0.0
|
||||
val Pairable.sosm2: Double get() = historyHelper.sosm2[id] ?: 0.0
|
||||
val Pairable.sosos: Double get() = historyHelper.sosos[id] ?: 0.0
|
||||
val Pairable.sodos: Double get() = historyHelper.sodos[id] ?: 0.0
|
||||
val Pairable.cums: Double get() = historyHelper.cumScore[id] ?: 0.0
|
||||
val Pairable.score: Double get() = history.scores[id] ?: 0.0
|
||||
val Pairable.scoreX: Double get() = history.scoresX[id] ?: 0.0
|
||||
val Pairable.nbW: Double get() = history.wins[id] ?: 0.0
|
||||
val Pairable.sos: Double get() = history.sos[id] ?: 0.0
|
||||
val Pairable.sosm1: Double get() = history.sosm1[id] ?: 0.0
|
||||
val Pairable.sosm2: Double get() = history.sosm2[id] ?: 0.0
|
||||
val Pairable.sosos: Double get() = history.sosos[id] ?: 0.0
|
||||
val Pairable.sodos: Double get() = history.sodos[id] ?: 0.0
|
||||
val Pairable.cums: Double get() = history.cumScore[id] ?: 0.0
|
||||
fun Pairable.missedRounds(): Int = (1 until round).map { round ->
|
||||
if (historyHelper.playersPerRound.getOrNull(round - 1)
|
||||
if (history.playersPerRound.getOrNull(round - 1)
|
||||
?.contains(id) == true
|
||||
) 0 else 1
|
||||
}.sum()
|
||||
|
@@ -2,12 +2,22 @@ package org.jeudego.pairgoth.pairing
|
||||
|
||||
import org.jeudego.pairgoth.model.*
|
||||
import org.jeudego.pairgoth.model.Game.Result.*
|
||||
import org.jeudego.pairgoth.model.TeamTournament.Team
|
||||
import org.jeudego.pairgoth.pairing.solver.Solver
|
||||
|
||||
typealias ScoreMap = Map<ID, Double>
|
||||
typealias ScoreMapFactory = () -> ScoreMap
|
||||
|
||||
open class HistoryHelper(
|
||||
protected val history: List<List<Game>>,
|
||||
// scoresGetter() returns Pair(sos value for missed rounds, score) where score is nbw for Swiss, mms for MM, ...
|
||||
scoresGetter: HistoryHelper.()-> Map<ID, Pair<Double, Double>>) {
|
||||
protected val history: List<List<Game>>
|
||||
) {
|
||||
|
||||
lateinit var scoresFactory: ScoreMapFactory
|
||||
lateinit var scoresXFactory: ScoreMapFactory
|
||||
lateinit var missedRoundsSosFactory: ScoreMapFactory
|
||||
|
||||
val scores by lazy { scoresFactory() }
|
||||
val scoresX by lazy { scoresXFactory() }
|
||||
val missedRoundsSos by lazy { missedRoundsSosFactory() }
|
||||
|
||||
private val Game.blackScore get() = when (result) {
|
||||
BLACK, BOTHWIN -> 1.0
|
||||
@@ -19,23 +29,10 @@ open class HistoryHelper(
|
||||
else -> 0.0
|
||||
}
|
||||
|
||||
private val scores by lazy {
|
||||
scoresGetter()
|
||||
}
|
||||
|
||||
val scoresX by lazy {
|
||||
scoresGetter().mapValues { entry ->
|
||||
entry.value.first + (wins[entry.key] ?: 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
// Generic helper functions
|
||||
open fun playedTogether(p1: Pairable, p2: Pairable) = paired.contains(Pair(p1.id, p2.id))
|
||||
open fun colorBalance(p: Pairable) = colorBalance[p.id]
|
||||
open fun nbPlayedWithBye(p: Pairable) = nbPlayedWithBye[p.id]
|
||||
open fun nbW(p: Pairable) = wins[p.id]
|
||||
|
||||
fun drawnUpDown(p: Pairable) = drawnUpDown[p.id]
|
||||
|
||||
protected val paired: Set<Pair<ID, ID>> by lazy {
|
||||
(history.flatten().map { game ->
|
||||
@@ -47,7 +44,7 @@ open class HistoryHelper(
|
||||
|
||||
// Returns the number of games played as white minus the number of games played as black
|
||||
// Only count games without handicap
|
||||
private val colorBalance: Map<ID, Int> by lazy {
|
||||
val colorBalance: Map<ID, Int> by lazy {
|
||||
history.flatten().filter { game ->
|
||||
game.handicap == 0
|
||||
}.filter { game ->
|
||||
@@ -72,14 +69,14 @@ open class HistoryHelper(
|
||||
}
|
||||
}
|
||||
|
||||
// Set of all implied players for each round (warning: does comprise games with BIP)
|
||||
// Set of all implied players for each round
|
||||
val playersPerRound: List<Set<ID>> by lazy {
|
||||
history.map {
|
||||
it.fold(mutableSetOf<ID>()) { acc, next ->
|
||||
if(next.white != 0) acc.add(next.white)
|
||||
if (next.black != 0) acc.add(next.black)
|
||||
acc
|
||||
}
|
||||
history.map { roundGames ->
|
||||
roundGames.flatMap {
|
||||
game -> listOf(game.white, game.black)
|
||||
}.filter { id ->
|
||||
id != ByePlayer.id
|
||||
}.toSet()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,67 +97,89 @@ open class HistoryHelper(
|
||||
}
|
||||
|
||||
// define mms to be a synonym of scores
|
||||
val mms by lazy { scores.mapValues { it -> it.value.second } }
|
||||
val mms by lazy { scores }
|
||||
|
||||
val sos by lazy {
|
||||
// SOS for played games against a real opponent or BIP
|
||||
val historySos = (history.flatten().map { game ->
|
||||
Pair(
|
||||
game.black,
|
||||
if (game.white == 0) scores[game.black]?.first ?: 0.0
|
||||
else scores[game.white]?.second?.let { it - game.handicap } ?: 0.0
|
||||
if (game.white == 0) missedRoundsSos[game.black] ?: 0.0
|
||||
else scores[game.white]?.let { it - game.handicap } ?: 0.0
|
||||
)
|
||||
} + history.flatten().map { game ->
|
||||
Pair(
|
||||
game.white,
|
||||
if (game.black == 0) scores[game.white]?.first ?: 0.0
|
||||
else scores[game.black]?.second?.let { it + game.handicap } ?: 0.0
|
||||
if (game.black == 0) missedRoundsSos[game.white] ?: 0.0
|
||||
else scores[game.black]?.let { it + game.handicap } ?: 0.0
|
||||
)
|
||||
}).groupingBy {
|
||||
it.first
|
||||
}.fold(0.0) { acc, next ->
|
||||
acc + next.second
|
||||
}
|
||||
|
||||
scores.mapValues { (id, pair) ->
|
||||
// plus SOS for missed rounds
|
||||
missedRoundsSos.mapValues { (id, pseudoSos) ->
|
||||
(historySos[id] ?: 0.0) + playersPerRound.sumOf {
|
||||
if (it.contains(id)) 0.0 else pair.first
|
||||
if (it.contains(id)) 0.0 else pseudoSos
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// sos-1
|
||||
val sosm1 by lazy {
|
||||
// SOS for played games against a real opponent or BIP
|
||||
(history.flatten().map { game ->
|
||||
Pair(game.black, scores[game.white]?.second?.let { it - game.handicap } ?: 0.0)
|
||||
Pair(
|
||||
game.black,
|
||||
if (game.white == 0) missedRoundsSos[game.black] ?: 0.0
|
||||
else scores[game.white]?.let { it - game.handicap } ?: 0.0
|
||||
)
|
||||
} + history.flatten().map { game ->
|
||||
Pair(game.white, scores[game.black]?.second?.let { it + game.handicap } ?: 0.0)
|
||||
Pair(
|
||||
game.white,
|
||||
if (game.black == 0) missedRoundsSos[game.white] ?: 0.0
|
||||
else scores[game.black]?.let { it + game.handicap } ?: 0.0
|
||||
)
|
||||
}).groupBy {
|
||||
it.first
|
||||
}.mapValues { (id, pairs) ->
|
||||
val oppScores = pairs.map { it.second }.sortedDescending()
|
||||
// minus greatest SOS
|
||||
oppScores.sum() - (oppScores.firstOrNull() ?: 0.0) +
|
||||
// plus SOS for missed rounds
|
||||
playersPerRound.sumOf { players ->
|
||||
if (players.contains(id)) 0.0
|
||||
else scores[id]?.first ?: 0.0
|
||||
else missedRoundsSos[id] ?: 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sos-2
|
||||
val sosm2 by lazy {
|
||||
// SOS for played games against a real opponent or BIP
|
||||
(history.flatten().map { game ->
|
||||
Pair(game.black, scores[game.white]?.second?.let { it - game.handicap } ?: 0.0)
|
||||
Pair(
|
||||
game.black,
|
||||
if (game.white == 0) missedRoundsSos[game.black] ?: 0.0
|
||||
else scores[game.white]?.let { it - game.handicap } ?: 0.0
|
||||
)
|
||||
} + history.flatten().map { game ->
|
||||
Pair(game.white, scores[game.black]?.second?.let { it + game.handicap } ?: 0.0)
|
||||
Pair(
|
||||
game.white,
|
||||
if (game.black == 0) missedRoundsSos[game.white] ?: 0.0
|
||||
else scores[game.black]?.let { it + game.handicap } ?: 0.0
|
||||
)
|
||||
}).groupBy {
|
||||
it.first
|
||||
}.mapValues { (id, pairs) ->
|
||||
val oppScores = pairs.map { it.second }.sorted()
|
||||
val oppScores = pairs.map { it.second }.sortedDescending()
|
||||
// minus two greatest SOS
|
||||
oppScores.sum() - oppScores.getOrElse(0) { 0.0 } - oppScores.getOrElse(1) { 0.0 } +
|
||||
// plus SOS for missed rounds
|
||||
playersPerRound.sumOf { players ->
|
||||
if (players.contains(id)) 0.0
|
||||
else scores[id]?.first ?: 0.0
|
||||
else missedRoundsSos[id] ?: 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,16 +189,17 @@ open class HistoryHelper(
|
||||
(history.flatten().filter { game ->
|
||||
game.white != 0 // Remove games against byePlayer
|
||||
}.map { game ->
|
||||
Pair(game.black, if (game.result == Game.Result.BLACK) scores[game.white]?.second?.let { it - game.handicap } ?: 0.0 else 0.0)
|
||||
Pair(game.black, if (game.result == Game.Result.BLACK) scores[game.white]?.let { it - game.handicap } ?: 0.0 else 0.0)
|
||||
} + history.flatten().filter { game ->
|
||||
game.white != 0 // Remove games against byePlayer
|
||||
}.map { game ->
|
||||
Pair(game.white, if (game.result == Game.Result.WHITE) scores[game.black]?.second?.let { it + game.handicap } ?: 0.0 else 0.0)
|
||||
Pair(game.white, if (game.result == Game.Result.WHITE) scores[game.black]?.let { it + game.handicap } ?: 0.0 else 0.0)
|
||||
}).groupingBy { it.first }.fold(0.0) { acc, next ->
|
||||
acc + next.second
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// sosos
|
||||
val sosos by lazy {
|
||||
val currentRound = history.size
|
||||
@@ -193,9 +213,9 @@ open class HistoryHelper(
|
||||
acc + next.second
|
||||
}
|
||||
|
||||
scores.mapValues { (id, pair) ->
|
||||
missedRoundsSos.mapValues { (id, missedRoundSos) ->
|
||||
(historySosos[id] ?: 0.0) + playersPerRound.sumOf {
|
||||
if (it.contains(id)) 0.0 else pair.first * currentRound
|
||||
if (it.contains(id)) 0.0 else missedRoundSos * currentRound
|
||||
}
|
||||
|
||||
}
|
||||
@@ -241,11 +261,4 @@ open class HistoryHelper(
|
||||
else null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// CB TODO - a big problem with the current naive implementation is that the team score is -for now- the sum of team members individual scores
|
||||
|
||||
class TeamOfIndividualsHistoryHelper(history: List<List<Game>>, scoresGetter: () -> Map<ID, Pair<Double, Double>>):
|
||||
HistoryHelper(history, { scoresGetter() }) {
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package org.jeudego.pairgoth.pairing
|
||||
|
||||
import org.jeudego.pairgoth.model.Pairable
|
||||
import org.jeudego.pairgoth.pairing.solver.BaseSolver
|
||||
|
||||
fun detRandom(max: Double, p1: Pairable, p2: Pairable, symmetric: Boolean): Double {
|
||||
var inverse = false
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.jeudego.pairgoth.pairing.solver
|
||||
|
||||
import org.jeudego.pairgoth.model.*
|
||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
@@ -8,46 +9,41 @@ import kotlin.math.roundToInt
|
||||
|
||||
class MacMahonSolver(round: Int,
|
||||
totalRounds: Int,
|
||||
history: List<List<Game>>,
|
||||
history: HistoryHelper,
|
||||
pairables: List<Pairable>,
|
||||
pairablesMap: Map<ID, Pairable>,
|
||||
allPairablesMap: Map<ID, Pairable>,
|
||||
pairingParams: PairingParams,
|
||||
placementParams: PlacementParams,
|
||||
usedTables: BitSet,
|
||||
private val mmFloor: Int, private val mmBar: Int) :
|
||||
BaseSolver(round, totalRounds, history, pairables, pairablesMap, pairingParams, placementParams, usedTables) {
|
||||
Solver(round, totalRounds, history, pairables, allPairablesMap, pairingParams, placementParams, usedTables) {
|
||||
|
||||
override val scores: Map<ID, Pair<Double, Double>> by lazy {
|
||||
require (mmBar > mmFloor) { "MMFloor is higher than MMBar" }
|
||||
pairablesMap.mapValues {
|
||||
it.value.let { pairable ->
|
||||
val score = roundScore(pairable.mmBase +
|
||||
override fun mainScoreMapFactory() =
|
||||
allPairablesMap.mapValues { (id, pairable) ->
|
||||
roundScore(pairable.mmBase +
|
||||
pairable.nbW +
|
||||
pairable.missedRounds() * pairingParams.main.mmsValueAbsent)
|
||||
Pair(
|
||||
if (pairingParams.main.sosValueAbsentUseBase) pairable.mmBase
|
||||
else roundScore(pairable.mmBase + round/2),
|
||||
score
|
||||
)
|
||||
}
|
||||
}
|
||||
pairable.missedRounds() * pairing.main.mmsValueAbsent)
|
||||
}
|
||||
|
||||
override val scoresX: Map<ID, Double> by lazy {
|
||||
require (mmBar > mmFloor) { "MMFloor is higher than MMBar" }
|
||||
pairablesMap.mapValues {
|
||||
it.value.let { pairable ->
|
||||
override fun scoreXMapFactory() =
|
||||
allPairablesMap.mapValues { (id, pairable) ->
|
||||
roundScore(pairable.mmBase + pairable.nbW)
|
||||
}
|
||||
|
||||
override fun missedRoundSosMapFactory() =
|
||||
allPairablesMap.mapValues { (id, pairable) ->
|
||||
if (pairing.main.sosValueAbsentUseBase) {
|
||||
pairable.mmBase
|
||||
} else {
|
||||
roundScore(pairable.mmBase + round/2)
|
||||
}
|
||||
}
|
||||
|
||||
override fun computeWeightForBye(p: Pairable): Double{
|
||||
return 2*scores[p.id]!!.second
|
||||
return 2 * p.score
|
||||
}
|
||||
|
||||
override fun SecondaryCritParams.apply(p1: Pairable, p2: Pairable): Double {
|
||||
|
||||
override fun SecondaryCritParams.playersMeetCriteria(p1: Pairable, p2: Pairable): Int {
|
||||
// playersMeetCriteria = 0 : No player is above thresholds -> apply the full weight
|
||||
// playersMeetCriteria = 1 : 1 player is above thresholds -> apply half the weight
|
||||
// playersMeetCriteria = 2 : Both players are above thresholds -> do not apply weight
|
||||
@@ -70,7 +66,7 @@ class MacMahonSolver(round: Int,
|
||||
|| barThresholdActive && (p2.mmBase >= mmBar - Pairable.MIN_RANK)
|
||||
|| p2.mms >= rankSecThreshold - Pairable.MIN_RANK) playersMeetCriteria++
|
||||
|
||||
return pairing.geo.apply(p1, p2, playersMeetCriteria)
|
||||
return playersMeetCriteria
|
||||
}
|
||||
|
||||
override fun HandicapParams.pseudoRank(pairable: Pairable): Int {
|
||||
@@ -84,8 +80,7 @@ class MacMahonSolver(round: Int,
|
||||
// mmBase: starting Mac-Mahon score of the pairable
|
||||
val Pairable.mmBase: Double get() = min(max(rank, mmFloor), mmBar) + mmsZero + mmsCorrection
|
||||
// mms: current Mac-Mahon score of the pairable
|
||||
val Pairable.mms: Double get() = scores[id]?.second ?: 0.0
|
||||
val Pairable.scoreX: Double get() = scoresX[id] ?: 0.0
|
||||
val Pairable.mms: Double get() = score
|
||||
|
||||
// CB TODO - configurable criteria
|
||||
val mainScoreMin = mmFloor + PLA_SMMS_SCORE_MIN - Pairable.MIN_RANK
|
||||
|
@@ -0,0 +1,75 @@
|
||||
package org.jeudego.pairgoth.pairing.solver
|
||||
|
||||
import org.jeudego.pairgoth.model.ID
|
||||
import org.jeudego.pairgoth.model.Pairable
|
||||
import org.jeudego.pairgoth.model.Tournament
|
||||
import java.io.PrintWriter
|
||||
|
||||
interface PairingListener {
|
||||
|
||||
fun start(round: Int) {}
|
||||
fun startPair(white: Pairable, black: Pairable) {}
|
||||
fun endPair(white: Pairable, black: Pairable) {}
|
||||
fun addWeight(name: String, weight: Double)
|
||||
fun end() {}
|
||||
}
|
||||
|
||||
class LoggingListener(val out: PrintWriter) : PairingListener {
|
||||
|
||||
var currentOpenGothaWeight: Double = 0.0
|
||||
|
||||
override fun start(round: Int) {
|
||||
out.println("Round $round")
|
||||
out.println("Costs")
|
||||
}
|
||||
|
||||
override fun startPair(white: Pairable, black: Pairable) {
|
||||
currentOpenGothaWeight = 0.0
|
||||
out.println("Player1Name=${white.fullName()}")
|
||||
out.println("Player2Name=${black.fullName()}")
|
||||
}
|
||||
|
||||
override fun addWeight(name: String, weight: Double) {
|
||||
// Try hard to stay in sync with current reference files of OpenGotha conformance tests
|
||||
val key = when (name) {
|
||||
// TODO - Change to propagate to test reference files
|
||||
"baseColorBalance" -> "baseBWBalance"
|
||||
// Pairgoth-specific part of the color balance, not considered in conformance tests
|
||||
"secColorBalance" -> return
|
||||
else -> name
|
||||
}
|
||||
val value = when (name) {
|
||||
// TODO - This cost is always zero in reference files, seems unused
|
||||
"secHandi" -> 0.0
|
||||
else -> weight
|
||||
}
|
||||
currentOpenGothaWeight += value
|
||||
out.println("${key}Cost=$value")
|
||||
}
|
||||
|
||||
override fun endPair(white: Pairable, black: Pairable) {
|
||||
out.println("totalCost=$currentOpenGothaWeight")
|
||||
}
|
||||
|
||||
override fun end() {
|
||||
out.flush()
|
||||
}
|
||||
}
|
||||
|
||||
class CollectingListener() : PairingListener {
|
||||
|
||||
val out = mutableMapOf<Pair<ID, ID>, MutableMap<String, Double>>()
|
||||
var white: Pairable? = null
|
||||
var black: Pairable? = null
|
||||
|
||||
override fun startPair(white: Pairable, black: Pairable) {
|
||||
this.white = white
|
||||
this.black = black
|
||||
}
|
||||
|
||||
override fun addWeight(name: String, weight: Double) {
|
||||
val key = Pair(white!!.id, black!!.id)
|
||||
val weights = out.computeIfAbsent(key) { mutableMapOf() }
|
||||
weights[name] = weight
|
||||
}
|
||||
}
|
@@ -4,6 +4,7 @@ import org.jeudego.pairgoth.model.*
|
||||
import org.jeudego.pairgoth.model.MainCritParams.DrawUpDown
|
||||
import org.jeudego.pairgoth.model.MainCritParams.SeedMethod.*
|
||||
import org.jeudego.pairgoth.pairing.BasePairingHelper
|
||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||
import org.jeudego.pairgoth.pairing.detRandom
|
||||
import org.jeudego.pairgoth.pairing.nonDetRandom
|
||||
import org.jeudego.pairgoth.store.nextGameId
|
||||
@@ -18,26 +19,48 @@ import java.text.DecimalFormat
|
||||
import java.util.*
|
||||
import kotlin.math.*
|
||||
|
||||
sealed class BaseSolver(
|
||||
sealed class Solver(
|
||||
round: Int,
|
||||
totalRounds: Int,
|
||||
history: List<List<Game>>, // History of all games played for each round
|
||||
pairables: List<Pairable>, // All pairables for this round, it may include the bye player
|
||||
pairablesMap: Map<ID, Pairable>, // Map of all known pairables in this tournament
|
||||
history: HistoryHelper, // Digested history of all games played for each round
|
||||
pairables: List<Pairable>, // Pairables to pair together
|
||||
val allPairablesMap: Map<ID, Pairable>, // Map of all known pairables
|
||||
pairing: PairingParams,
|
||||
placement: PlacementParams,
|
||||
val usedTables: BitSet
|
||||
) : BasePairingHelper(round, totalRounds, history, pairables, pairablesMap, pairing, placement) {
|
||||
) : BasePairingHelper(round, totalRounds, history, pairables, pairing, placement) {
|
||||
|
||||
companion object {
|
||||
val rand = Random(/* seed from properties - TODO */)
|
||||
// Used in tests
|
||||
var weightsLogger: PrintWriter? = null
|
||||
var legacy_mode = false
|
||||
}
|
||||
|
||||
// For tests and explain feature
|
||||
var legacyMode = false
|
||||
var pairingListener: PairingListener? = null
|
||||
|
||||
init {
|
||||
history.scoresFactory = this::mainScoreMapFactory
|
||||
history.scoresXFactory = this::scoreXMapFactory
|
||||
history.missedRoundsSosFactory = this::missedRoundSosMapFactory
|
||||
}
|
||||
|
||||
/**
|
||||
* Main score map factory (NBW for Swiss, MMS for MacMahon, ...).
|
||||
*/
|
||||
abstract fun mainScoreMapFactory(): Map<ID, Double>
|
||||
|
||||
/**
|
||||
* ScoreX map factory (NBW for Swiss, MMSBase + MMS for MacMahon, ...).
|
||||
*/
|
||||
abstract fun scoreXMapFactory(): Map<ID, Double>
|
||||
|
||||
/**
|
||||
* SOS for missed rounds factory (0 for Swiss, mmBase or mmBase+rounds/2 for MacMahon depending on pairing option sosValueAbsentUseBase)
|
||||
*/
|
||||
abstract fun missedRoundSosMapFactory(): Map<ID, Double>
|
||||
|
||||
open fun openGothaWeight(p1: Pairable, p2: Pairable) =
|
||||
1.0 + // 1 is minimum value because 0 means "no matching allowed"
|
||||
1.0 + // 1 is the minimum value because 0 means "no matching allowed"
|
||||
pairing.base.apply(p1, p2) +
|
||||
pairing.main.apply(p1, p2) +
|
||||
pairing.secondary.apply(p1, p2)
|
||||
@@ -49,20 +72,26 @@ sealed class BaseSolver(
|
||||
else 0.0
|
||||
}
|
||||
|
||||
open fun weight(p1: Pairable, p2: Pairable) =
|
||||
open fun weight(p1: Pairable, p2: Pairable): Double {
|
||||
pairingListener?.startPair(p1, p2)
|
||||
return (
|
||||
openGothaWeight(p1, p2) +
|
||||
pairgothBlackWhite(p1, p2) +
|
||||
pairgothBlackWhite(p1, p2).also { pairingListener?.addWeight("secColorBalance", it) } +
|
||||
// pairing.base.applyByeWeight(p1, p2) +
|
||||
pairing.handicap.color(p1, p2)
|
||||
pairing.handicap.color(p1, p2).also { pairingListener?.addWeight("secHandi", it) }
|
||||
).also {
|
||||
pairingListener?.endPair(p1, p2)
|
||||
}
|
||||
}
|
||||
|
||||
open fun computeWeightForBye(p: Pairable): Double{
|
||||
open fun computeWeightForBye(p: Pairable): Double {
|
||||
// The weightForBye function depends on the system type (Mac-Mahon or Swiss), default value is 0.0
|
||||
return 0.0
|
||||
}
|
||||
|
||||
fun pair(): List<Game> {
|
||||
// check that at this stage, we have an even number of pairables
|
||||
// The BYE player should have been added beforehand to make a number of pairables even.
|
||||
// The BYE player should have been added beforehand to make the number of pairables even.
|
||||
if (pairables.size % 2 != 0) throw Error("expecting an even number of pairables")
|
||||
val builder = GraphBuilder(SimpleDirectedWeightedGraph<Pairable, DefaultWeightedEdge>(DefaultWeightedEdge::class.java))
|
||||
|
||||
@@ -70,21 +99,18 @@ sealed class BaseSolver(
|
||||
val logger = LoggerFactory.getLogger("debug")
|
||||
val debug = false
|
||||
|
||||
weightsLogger?.apply {
|
||||
this.println("Round $round")
|
||||
this.println("Costs")
|
||||
}
|
||||
pairingListener?.start(round)
|
||||
|
||||
var chosenByePlayer: Pairable = ByePlayer
|
||||
// Choose bye player and remove from pairables
|
||||
if (ByePlayer in nameSortedPairables){
|
||||
nameSortedPairables.remove(ByePlayer)
|
||||
var minWeight = 1000.0*round + (Pairable.MAX_RANK - Pairable.MIN_RANK) + 1;
|
||||
var minWeight = 1000.0 * round + (Pairable.MAX_RANK - Pairable.MIN_RANK) + 1;
|
||||
var weightForBye : Double
|
||||
var byePlayerIndex = 0
|
||||
for (p in nameSortedPairables){
|
||||
weightForBye = computeWeightForBye(p)
|
||||
if (p.id in historyHelper.byePlayers) weightForBye += 1000
|
||||
if (p.id in history.byePlayers) weightForBye += 1000
|
||||
if (weightForBye <= minWeight){
|
||||
minWeight = weightForBye
|
||||
chosenByePlayer = p
|
||||
@@ -102,21 +128,6 @@ sealed class BaseSolver(
|
||||
val q = nameSortedPairables[j]
|
||||
weight(p, q).let { if (it != Double.NaN) builder.addEdge(p, q, it/1e6) }
|
||||
weight(q, p).let { if (it != Double.NaN) builder.addEdge(q, p, it/1e6) }
|
||||
weightsLogger?.apply {
|
||||
this.println("Player1Name=${p.fullName()}")
|
||||
this.println("Player2Name=${q.fullName()}")
|
||||
this.println("baseDuplicateGameCost=${dec.format(pairing.base.avoidDuplicatingGames(p, q))}")
|
||||
this.println("baseRandomCost=${dec.format(pairing.base.applyRandom(p, q))}")
|
||||
this.println("baseBWBalanceCost=${dec.format(pairing.base.applyColorBalance(p, q))}")
|
||||
this.println("mainCategoryCost=${dec.format(pairing.main.avoidMixingCategory(p, q))}")
|
||||
this.println("mainScoreDiffCost=${dec.format(pairing.main.minimizeScoreDifference(p, q))}")
|
||||
this.println("mainDUDDCost=${dec.format(pairing.main.applyDUDD(p, q))}")
|
||||
this.println("mainSeedCost=${dec.format(pairing.main.applySeeding(p, q))}")
|
||||
this.println("secHandiCost=${dec.format(pairing.handicap.handicap(p, q))}")
|
||||
this.println("secGeoCost=${dec.format(pairing.secondary.apply(p, q))}")
|
||||
this.println("totalCost=${dec.format(openGothaWeight(p,q))}")
|
||||
//File(WEIGHTS_FILE).appendText("ByeCost="+dec.format(pairing.base.applyByeWeight(p,q))+"\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
val graph = builder.build()
|
||||
@@ -131,6 +142,8 @@ sealed class BaseSolver(
|
||||
// add game for ByePlayer
|
||||
if (chosenByePlayer != ByePlayer) result += Game(id = nextGameId, table = 0, white = chosenByePlayer.id, black = ByePlayer.id, result = Game.Result.fromSymbol('w'))
|
||||
|
||||
pairingListener?.end()
|
||||
|
||||
if (debug) {
|
||||
var sumOfWeights = 0.0
|
||||
|
||||
@@ -144,8 +157,8 @@ sealed class BaseSolver(
|
||||
for (p in sortedPairables) {
|
||||
logger.info(String.format("%-20s", p.name.substring(0, min(p.name.length, 18)))
|
||||
+ " " + String.format("%-4s", p.id)
|
||||
+ " " + String.format("%-4s", scores[p.id]?.first)
|
||||
+ " " + String.format("%-4s", scores[p.id]?.second)
|
||||
+ " " + String.format("%-4s", history.missedRoundsSos[p.id])
|
||||
+ " " + String.format("%-4s", history.scores[p.id])
|
||||
+ " " + String.format("%-4s", p.sos)
|
||||
)
|
||||
}
|
||||
@@ -194,11 +207,11 @@ sealed class BaseSolver(
|
||||
var score = 0.0
|
||||
// Base Criterion 1 : Avoid Duplicating Game
|
||||
// Did p1 and p2 already play ?
|
||||
score += avoidDuplicatingGames(p1, p2)
|
||||
score += avoidDuplicatingGames(p1, p2).also { pairingListener?.addWeight("baseDuplicateGame", it) }
|
||||
// Base Criterion 2 : Random
|
||||
score += applyRandom(p1, p2)
|
||||
score += applyRandom(p1, p2).also { pairingListener?.addWeight("baseRandom", it) }
|
||||
// Base Criterion 3 : Balance W and B
|
||||
score += applyColorBalance(p1, p2)
|
||||
score += applyColorBalance(p1, p2).also { pairingListener?.addWeight("baseColorBalance", it) }
|
||||
|
||||
return score
|
||||
}
|
||||
@@ -258,16 +271,16 @@ sealed class BaseSolver(
|
||||
var score = 0.0
|
||||
|
||||
// Main criterion 1 avoid mixing category is moved to Swiss with category
|
||||
score += avoidMixingCategory(p1, p2)
|
||||
score += avoidMixingCategory(p1, p2).also { pairingListener?.addWeight("mainCategory", it) }
|
||||
|
||||
// Main criterion 2 minimize score difference
|
||||
score += minimizeScoreDifference(p1, p2)
|
||||
score += minimizeScoreDifference(p1, p2).also { pairingListener?.addWeight("mainScoreDiff", it) }
|
||||
|
||||
// Main criterion 3 If different groups, make a directed Draw-up/Draw-down
|
||||
score += applyDUDD(p1, p2)
|
||||
score += applyDUDD(p1, p2).also { pairingListener?.addWeight("mainDUDD", it) }
|
||||
|
||||
// Main criterion 4 seeding
|
||||
score += applySeeding(p1, p2)
|
||||
score += applySeeding(p1, p2).also { pairingListener?.addWeight("mainSeed", it) }
|
||||
|
||||
return score
|
||||
}
|
||||
@@ -389,7 +402,7 @@ sealed class BaseSolver(
|
||||
val randRange = maxSeedingWeight * 0.2
|
||||
// for old tests to pass
|
||||
val rand =
|
||||
if (legacy_mode && p1.fullName() > p2.fullName()) {
|
||||
if (legacyMode && p1.fullName() > p2.fullName()) {
|
||||
// for old tests to pass
|
||||
detRandom(randRange, p2, p1, false)
|
||||
} else {
|
||||
@@ -405,8 +418,7 @@ sealed class BaseSolver(
|
||||
return Math.round(score).toDouble()
|
||||
}
|
||||
|
||||
open fun SecondaryCritParams.apply(p1: Pairable, p2: Pairable): Double {
|
||||
|
||||
open fun SecondaryCritParams.playersMeetCriteria(p1: Pairable, p2: Pairable): Int {
|
||||
// playersMeetCriteria = 0 : No player is above thresholds -> apply secondary criteria
|
||||
// playersMeetCriteria = 1 : 1 player is above thresholds -> apply half the weight
|
||||
// playersMeetCriteria = 2 : Both players are above thresholds -> apply the full weight
|
||||
@@ -419,7 +431,11 @@ sealed class BaseSolver(
|
||||
if (2*p1.nbW >= nbw2Threshold) playersMeetCriteria++
|
||||
if (2*p2.nbW >= nbw2Threshold) playersMeetCriteria++
|
||||
|
||||
return pairing.geo.apply(p1, p2, playersMeetCriteria)
|
||||
return playersMeetCriteria
|
||||
}
|
||||
|
||||
fun SecondaryCritParams.apply(p1: Pairable, p2: Pairable): Double {
|
||||
return pairing.geo.apply(p1, p2, playersMeetCriteria(p1, p2))
|
||||
}
|
||||
|
||||
fun GeographicalParams.apply(p1: Pairable, p2: Pairable, playersMeetCriteria: Int): Double {
|
||||
@@ -427,11 +443,11 @@ sealed class BaseSolver(
|
||||
|
||||
val geoMaxCost = pairing.geo.avoidSameGeo
|
||||
|
||||
val countryFactor: Int = if (legacy_mode || biggestCountrySize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
||||
val countryFactor: Int = if (legacyMode || biggestCountrySize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
||||
preferMMSDiffRatherThanSameCountry
|
||||
else
|
||||
0
|
||||
val clubFactor: Int = if (legacy_mode || biggestClubSize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
||||
val clubFactor: Int = if (legacyMode || biggestClubSize.toDouble() / pairables.size <= proportionMainClubThreshold)
|
||||
preferMMSDiffRatherThanSameClub
|
||||
else
|
||||
0
|
||||
@@ -489,7 +505,7 @@ sealed class BaseSolver(
|
||||
2 -> geoMaxCost
|
||||
1 -> 0.5 * (geoNominalCost + geoMaxCost)
|
||||
else -> geoNominalCost
|
||||
}
|
||||
}.also { pairingListener?.addWeight("secGeo", it) }
|
||||
}
|
||||
|
||||
// Handicap functions
|
||||
@@ -537,7 +553,7 @@ sealed class BaseSolver(
|
||||
} else if (p1.colorBalance < p2.colorBalance) {
|
||||
score = 1.0
|
||||
} else { // choose color from a det random
|
||||
if (detRandom(1.0, p1, p2, false) === 0.0) {
|
||||
if (detRandom(1.0, p1, p2, false) == 0.0) {
|
||||
score = 1.0
|
||||
} else {
|
||||
score = -1.0
|
@@ -1,31 +1,35 @@
|
||||
package org.jeudego.pairgoth.pairing.solver
|
||||
|
||||
import org.jeudego.pairgoth.model.*
|
||||
import org.jeudego.pairgoth.pairing.HistoryHelper
|
||||
import java.util.*
|
||||
|
||||
class SwissSolver(round: Int,
|
||||
totalRounds: Int,
|
||||
history: List<List<Game>>,
|
||||
history: HistoryHelper,
|
||||
pairables: List<Pairable>,
|
||||
pairablesMap: Map<ID, Pairable>,
|
||||
allPairablesMap: Map<ID, Pairable>,
|
||||
pairingParams: PairingParams,
|
||||
placementParams: PlacementParams,
|
||||
usedTables: BitSet
|
||||
):
|
||||
BaseSolver(round, totalRounds, history, pairables, pairablesMap, pairingParams, placementParams, usedTables) {
|
||||
Solver(round, totalRounds, history, pairables, allPairablesMap, pairingParams, placementParams, usedTables) {
|
||||
|
||||
// In a Swiss tournament the main criterion is the number of wins and already computed
|
||||
override fun mainScoreMapFactory() =
|
||||
allPairablesMap.mapValues { (id, pairable) ->
|
||||
history.wins[id] ?: 0.0
|
||||
}
|
||||
|
||||
override val scores by lazy {
|
||||
pairablesMap.mapValues {
|
||||
Pair(0.0, historyHelper.wins[it.value.id] ?: 0.0)
|
||||
override fun scoreXMapFactory() = mainScoreMapFactory()
|
||||
|
||||
override fun missedRoundSosMapFactory() =
|
||||
allPairablesMap.mapValues { (id, pairable) ->
|
||||
0.0
|
||||
}
|
||||
}
|
||||
override val scoresX: Map<ID, Double> get() = scores.mapValues { it.value.second }
|
||||
|
||||
override val mainLimits = Pair(0.0, round - 1.0)
|
||||
|
||||
override fun computeWeightForBye(p: Pairable): Double{
|
||||
return p.rank + 40*p.main
|
||||
return p.rank + 40 * p.main
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.republicate.kson.Json
|
||||
import org.apache.commons.io.input.BOMInputStream
|
||||
import org.jeudego.pairgoth.api.ApiHandler
|
||||
import org.jeudego.pairgoth.api.ExplainHandler
|
||||
import org.jeudego.pairgoth.api.PairingHandler
|
||||
import org.jeudego.pairgoth.api.PlayerHandler
|
||||
import org.jeudego.pairgoth.api.ResultsHandler
|
||||
@@ -99,6 +100,7 @@ class ApiServlet: HttpServlet() {
|
||||
if ("token" == selector) TokenHandler
|
||||
else when (subEntity) {
|
||||
null -> TournamentHandler
|
||||
"explain" -> ExplainHandler
|
||||
"part" -> PlayerHandler
|
||||
"pair" -> PairingHandler
|
||||
"res" -> ResultsHandler
|
||||
|
@@ -1,14 +1,12 @@
|
||||
package org.jeudego.pairgoth.test
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.pairing.solver.BaseSolver
|
||||
import org.jeudego.pairgoth.model.Game
|
||||
import org.jeudego.pairgoth.pairing.solver.Solver
|
||||
import org.jeudego.pairgoth.test.PairingTests.Companion.compare_weights
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
@@ -22,24 +20,22 @@ class BOSP2024Test: TestBase() {
|
||||
)!!.asObject()
|
||||
val resp = TestAPI.post("/api/tour", tournament).asObject()
|
||||
val tourId = resp.getInt("id")
|
||||
BaseSolver.weightsLogger = PrintWriter(FileWriter(getOutputFile("bosp2024-weights.txt")))
|
||||
|
||||
BaseSolver.legacy_mode = true
|
||||
TestAPI.post("/api/tour/$tourId/pair/3", Json.Array("all")).asArray()
|
||||
val outputFile = getOutputFile("bosp2024-weights.txt")
|
||||
TestAPI.post("/api/tour/$tourId/pair/3?legacy=true&weights_output=$outputFile", Json.Array("all")).asArray()
|
||||
|
||||
// compare weights
|
||||
assertTrue(compare_weights(getOutputFile("bosp2024-weights.txt"), getTestFile("opengotha/bosp2024/bosp2024_weights_R3.txt")), "Not matching opengotha weights for BOSP test")
|
||||
assertTrue(compare_weights(outputFile, getTestFile("opengotha/bosp2024/bosp2024_weights_R3.txt")), "Not matching opengotha weights for BOSP test")
|
||||
TestAPI.delete("/api/tour/$tourId/pair/3", Json.Array("all"))
|
||||
|
||||
BaseSolver.legacy_mode = false
|
||||
val games = TestAPI.post("/api/tour/$tourId/pair/3", Json.Array("all")).asArray()
|
||||
// Aksut Husrev is ID 18
|
||||
val solved = games.map { it as Json.Object }.filter { game ->
|
||||
val solved = games.map { it as Json.Object }.firstOrNull { game ->
|
||||
// build the two-elements set of players ids
|
||||
val players = game.entries.filter { (k, v) -> k == "b" || k == "w" }.map { (k, v) -> (v as Number).toInt() }.toSet()
|
||||
val players =
|
||||
game.entries.filter { (k, v) -> k == "b" || k == "w" }.map { (k, v) -> (v as Number).toInt() }.toSet()
|
||||
// keep game with Aksut Husrev
|
||||
players.contains(18)
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
assertNotNull(solved)
|
||||
|
||||
|
@@ -1,11 +1,8 @@
|
||||
package org.jeudego.pairgoth.test
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.pairing.solver.BaseSolver
|
||||
import org.jeudego.pairgoth.test.PairingTests.Companion.compare_weights
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
@@ -19,8 +16,8 @@ class MalavasiTest: TestBase() {
|
||||
)!!.asObject()
|
||||
val resp = TestAPI.post("/api/tour", tournament).asObject()
|
||||
val tourId = resp.getInt("id")
|
||||
BaseSolver.weightsLogger = PrintWriter(FileWriter(getOutputFile("malavasi-weights.txt")))
|
||||
val games = TestAPI.post("/api/tour/$tourId/pair/2", Json.Array("all")).asArray()
|
||||
val outputFile = getOutputFile("malavasi-weights.txt")
|
||||
val games = TestAPI.post("/api/tour/$tourId/pair/2?weights_output=$outputFile", Json.Array("all")).asArray()
|
||||
// Oceane is ID 548, Valentine 549
|
||||
val buggy = games.map { it as Json.Object }.filter { game ->
|
||||
// build the two-elements set of players ids
|
||||
@@ -33,6 +30,6 @@ class MalavasiTest: TestBase() {
|
||||
assertEquals(2, buggy.size)
|
||||
|
||||
// compare weights
|
||||
assertTrue(compare_weights(getOutputFile("malavasi-weights.txt"), getTestFile("opengotha/malavasi/malavasi_weights_R2.txt")), "Not matching opengotha weights for Malavasi test")
|
||||
assertTrue(compare_weights(outputFile, getTestFile("opengotha/malavasi/malavasi_weights_R2.txt")), "Not matching opengotha weights for Malavasi test")
|
||||
}
|
||||
}
|
||||
|
@@ -2,14 +2,12 @@ package org.jeudego.pairgoth.test
|
||||
|
||||
import com.republicate.kson.Json
|
||||
import org.jeudego.pairgoth.model.*
|
||||
import org.jeudego.pairgoth.pairing.solver.BaseSolver
|
||||
import org.jeudego.pairgoth.pairing.solver.Solver
|
||||
import org.jeudego.pairgoth.store.MemoryStore
|
||||
import org.jeudego.pairgoth.store.lastPlayerId
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.abs
|
||||
@@ -59,7 +57,6 @@ class PairingTests: TestBase() {
|
||||
}
|
||||
|
||||
fun compare_weights(file1: File, file2: File, skipSeeding: Boolean = false):Boolean {
|
||||
BaseSolver.weightsLogger!!.flush()
|
||||
// Maps to store name pairs and costs
|
||||
val map1 = create_weights_map(file1)
|
||||
val map2 = create_weights_map(file2)
|
||||
@@ -165,6 +162,7 @@ class PairingTests: TestBase() {
|
||||
}
|
||||
|
||||
fun test_from_XML(name: String, forcePairing:List<Int>) {
|
||||
// Let pairgoth use the legacy asymmetric detRandom()
|
||||
test_from_XML_internal(name, forcePairing, true)
|
||||
// Non-legacy tests inhibited for now: pairings differ for Toulouse and SimpleMM
|
||||
// test_from_XML_internal(name, forcePairing, false)
|
||||
@@ -172,11 +170,10 @@ class PairingTests: TestBase() {
|
||||
|
||||
fun test_from_XML_internal(name: String, forcePairing:List<Int>, legacy: Boolean) {
|
||||
// Let pairgoth use the legacy asymmetric detRandom()
|
||||
BaseSolver.legacy_mode = legacy
|
||||
// read tournament with pairing
|
||||
val file = getTestFile("opengotha/pairings/$name.xml")
|
||||
logger.info("read from file $file")
|
||||
val resource = file.readText(StandardCharsets.UTF_8)
|
||||
val tourFile = getTestFile("opengotha/pairings/$name.xml")
|
||||
logger.info("read from file $tourFile")
|
||||
val resource = tourFile.readText(StandardCharsets.UTF_8)
|
||||
var resp = TestAPI.post("/api/tour", resource)
|
||||
val id = resp.asObject().getInt("id")
|
||||
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
||||
@@ -203,15 +200,15 @@ class PairingTests: TestBase() {
|
||||
for (round in 1..tournament.getInt("rounds")!!) {
|
||||
val sumOfWeightsOG = compute_sumOfWeight_OG(getTestFile("opengotha/$name/$name" + "_weights_R$round.txt"), pairingsOG[round-1], players)
|
||||
|
||||
BaseSolver.weightsLogger = PrintWriter(FileWriter(getOutputFile("weights.txt")))
|
||||
val outputFile = getOutputFile("weights.txt")
|
||||
// Call Pairgoth pairing solver to generate games
|
||||
games = TestAPI.post("/api/tour/$id/pair/$round", Json.Array("all")).asArray()
|
||||
games = TestAPI.post("/api/tour/$id/pair/$round?legacy=$legacy&weights_output=$outputFile", Json.Array("all")).asArray()
|
||||
logger.info("sumOfWeightOG = " + dec.format(sumOfWeightsOG))
|
||||
logger.info("games for round $round: {}", games.toString())
|
||||
|
||||
// Compare weights with OpenGotha if legacy mode
|
||||
if (legacy) {
|
||||
assertTrue(compare_weights(getOutputFile("weights.txt"), getTestFile("opengotha/$name/$name"+"_weights_R$round.txt")), "Not matching opengotha weights for round $round")
|
||||
assertTrue(compare_weights(outputFile, getTestFile("opengotha/$name/$name"+"_weights_R$round.txt")), "Not matching opengotha weights for round $round")
|
||||
}
|
||||
|
||||
if (round in forcePairing) {
|
||||
@@ -223,7 +220,7 @@ class PairingTests: TestBase() {
|
||||
val gameOG = pairingsOG[round - 1].getJson(i)!!.asObject()// ["r"] as String?
|
||||
val whiteId = gameOG["w"] as Long?
|
||||
val blackId = gameOG["b"] as Long?
|
||||
TestAPI.put("/api/tour/$id/pair/$round", Json.parse("""{"id":$gameID,"w":$whiteId,"b":$blackId}""")).asObject()
|
||||
TestAPI.put("/api/tour/$id/pair/$round?legacy=$legacy&weights_output=$outputFile&append=true", Json.parse("""{"id":$gameID,"w":$whiteId,"b":$blackId}""")).asObject()
|
||||
}
|
||||
games = TestAPI.get("/api/tour/$id/res/$round").asArray()
|
||||
}
|
||||
@@ -273,11 +270,10 @@ class PairingTests: TestBase() {
|
||||
|
||||
@Test
|
||||
fun `SwissTest simpleSwiss`() {
|
||||
BaseSolver.legacy_mode = true
|
||||
// read tournament with pairing
|
||||
var file = getTestFile("opengotha/pairings/simpleswiss.xml")
|
||||
logger.info("read from file $file")
|
||||
val resource = file.readText(StandardCharsets.UTF_8)
|
||||
var tourFile = getTestFile("opengotha/pairings/simpleswiss.xml")
|
||||
logger.info("read from file $tourFile")
|
||||
val resource = tourFile.readText(StandardCharsets.UTF_8)
|
||||
var resp = TestAPI.post("/api/tour", resource)
|
||||
val id = resp.asObject().getInt("id")
|
||||
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
||||
@@ -315,10 +311,10 @@ class PairingTests: TestBase() {
|
||||
var firstGameID: Int
|
||||
|
||||
for (round in 1..7) {
|
||||
BaseSolver.weightsLogger = PrintWriter(FileWriter(getOutputFile("weights.txt")))
|
||||
games = TestAPI.post("/api/tour/$id/pair/$round", Json.Array("all")).asArray()
|
||||
val outputFile = getOutputFile("weights.txt")
|
||||
games = TestAPI.post("/api/tour/$id/pair/$round?legacy=true&weights_output=$outputFile", Json.Array("all")).asArray()
|
||||
logger.info("games for round $round: {}", games.toString().slice(0..50) + "...")
|
||||
assertTrue(compare_weights(getOutputFile("weights.txt"), getTestFile("opengotha/simpleswiss/simpleswiss_weights_R$round.txt")), "Not matching opengotha weights for round $round")
|
||||
assertTrue(compare_weights(outputFile, getTestFile("opengotha/simpleswiss/simpleswiss_weights_R$round.txt")), "Not matching opengotha weights for round $round")
|
||||
assertTrue(compare_games(games, Json.parse(pairingsOG[round - 1])!!.asArray()),"pairings for round $round differ")
|
||||
logger.info("Pairings for round $round match OpenGotha")
|
||||
|
||||
@@ -354,12 +350,12 @@ class PairingTests: TestBase() {
|
||||
@Test
|
||||
fun `SwissTest KPMCSplitbug`() {
|
||||
// Let pairgoth use the legacy asymmetric detRandom()
|
||||
BaseSolver.legacy_mode = true
|
||||
val legacy = true
|
||||
// read tournament with pairing
|
||||
val name = "20240921-KPMC-Splitbug"
|
||||
val file = getTestFile("opengotha/pairings/$name.xml")
|
||||
logger.info("read from file $file")
|
||||
val resource = file.readText(StandardCharsets.UTF_8)
|
||||
val tourFile = getTestFile("opengotha/pairings/$name.xml")
|
||||
logger.info("read from file $tourFile")
|
||||
val resource = tourFile.readText(StandardCharsets.UTF_8)
|
||||
var resp = TestAPI.post("/api/tour", resource)
|
||||
val id = resp.asObject().getInt("id")
|
||||
val tournament = TestAPI.get("/api/tour/$id").asObject()
|
||||
@@ -387,13 +383,13 @@ class PairingTests: TestBase() {
|
||||
|
||||
var games: Json.Array
|
||||
var firstGameID: Int
|
||||
val outputFile = getOutputFile("weights.txt")
|
||||
|
||||
for (round in minRound..maxRound) {
|
||||
val sumOfWeightsOG = compute_sumOfWeight_OG(getTestFile("opengotha/$name/$name" + "_weights_R$round.txt"), pairingsOG[round - minRound], players)
|
||||
|
||||
BaseSolver.weightsLogger = PrintWriter(FileWriter(getOutputFile("weights.txt")))
|
||||
// Call Pairgoth pairing solver to generate games
|
||||
games = TestAPI.post("/api/tour/$id/pair/$round", Json.Array("all")).asArray()
|
||||
games = TestAPI.post("/api/tour/$id/pair/$round?legacy=$legacy&weights_ouput=$outputFile&append=${round > 1}", Json.Array("all")).asArray()
|
||||
|
||||
logger.info("sumOfWeightOG = " + dec.format(sumOfWeightsOG))
|
||||
logger.info("games for round $round: {}", games.toString().slice(0..50) + "...")
|
||||
@@ -401,7 +397,7 @@ class PairingTests: TestBase() {
|
||||
// Compare weights with OpenGotha
|
||||
assertTrue(
|
||||
compare_weights(
|
||||
getOutputFile("weights.txt"),
|
||||
outputFile,
|
||||
getTestFile("opengotha/$name/$name" + "_weights_R$round.txt")
|
||||
), "Not matching opengotha weights for round $round"
|
||||
)
|
||||
|
@@ -7,6 +7,8 @@ import org.jeudego.pairgoth.server.SSEServlet
|
||||
import org.jeudego.pairgoth.server.WebappManager
|
||||
import org.mockito.kotlin.*
|
||||
import java.io.*
|
||||
import java.net.URL
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
import javax.servlet.ReadListener
|
||||
@@ -21,20 +23,45 @@ object TestAPI {
|
||||
|
||||
fun Any?.toUnit() = Unit
|
||||
|
||||
fun parseURL(url: String): Pair<String, Map<String, String>> {
|
||||
val qm = url.indexOf('?')
|
||||
if (qm == -1) {
|
||||
return url to emptyMap()
|
||||
}
|
||||
val uri = url.substring(0, qm)
|
||||
val params = url.substring(qm + 1)
|
||||
.split('&')
|
||||
.map { it.split('=') }
|
||||
.mapNotNull {
|
||||
when (it.size) {
|
||||
1 -> it[0].decodeUTF8() to ""
|
||||
2 -> it[0].decodeUTF8() to it[1].decodeUTF8()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
.toMap()
|
||||
return uri to params
|
||||
}
|
||||
|
||||
private fun String.decodeUTF8() = URLDecoder.decode(this, "UTF-8") // decode page=%22ABC%22 to page="ABC"
|
||||
|
||||
private val apiServlet = ApiServlet()
|
||||
private val sseServlet = SSEServlet()
|
||||
|
||||
private fun <T> testRequest(reqMethod: String, uri: String, accept: String = "application/json", payload: T? = null): String {
|
||||
private fun <T> testRequest(reqMethod: String, url: String, accept: String = "application/json", payload: T? = null): String {
|
||||
|
||||
WebappManager.properties["auth"] = "none"
|
||||
WebappManager.properties["store"] = "memory"
|
||||
WebappManager.properties["webapp.env"] = "test"
|
||||
|
||||
val (uri, parameters) = parseURL(url)
|
||||
|
||||
// mock request
|
||||
val myHeaderNames = if (reqMethod == "GET") emptyList() else listOf("Content-Type")
|
||||
val selector = argumentCaptor<String>()
|
||||
val subSelector = argumentCaptor<String>()
|
||||
val reqPayload = argumentCaptor<String>()
|
||||
val parameter = argumentCaptor<String>()
|
||||
val myInputStream = payload?.let { DelegatingServletInputStream(payload.toString().byteInputStream(StandardCharsets.UTF_8)) }
|
||||
val myReader = payload?.let { BufferedReader(StringReader(payload.toString())) }
|
||||
val req = mock<HttpServletRequest> {
|
||||
@@ -59,6 +86,7 @@ object TestAPI {
|
||||
}
|
||||
on { headerNames } doReturn Collections.enumeration(myHeaderNames)
|
||||
on { getHeader(eq("Accept")) } doReturn accept
|
||||
on { getParameter(parameter.capture()) } doAnswer { parameters[parameter.lastValue] }
|
||||
}
|
||||
|
||||
// mock response
|
||||
@@ -77,7 +105,7 @@ object TestAPI {
|
||||
"DELETE" -> apiServlet.doDelete(req, resp)
|
||||
}
|
||||
|
||||
return buffer.toString() ?: throw Error("no response payload")
|
||||
return buffer.toString()
|
||||
}
|
||||
|
||||
fun get(uri: String): Json = Json.parse(testRequest<Void>("GET", uri)) ?: throw Error("no payload")
|
||||
|
23
build.sh
Executable file
23
build.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
mkdir pairgoth
|
||||
cp application/target/pairgoth-engine.jar pairgoth/
|
||||
|
||||
echo "#!/bin/bash" > pairgoth/run.sh
|
||||
echo "./jdk-11.0.28+6-jre/bin/java -jar pairgoth-engine.jar" >> pairgoth/run.sh
|
||||
|
||||
echo "#!/bin/bash" > pairgoth/get_java.sh
|
||||
echo "wget https://github.com/adoptium/temurin11-binaries/releases/download/jdk-11.0.28%2B6/OpenJDK11U-jre_x64_linux_hotspot_11.0.28_6.tar.gz" >> pairgoth/get_java.sh
|
||||
echo "tar -xzf OpenJDK11U-jre_x64_linux_hotspot_11.0.28_6.tar.gz" >> pairgoth/get_java.sh
|
||||
|
||||
chmod +x pairgoth/run.sh
|
||||
chmod +x pairgoth/get_java.sh
|
||||
|
||||
|
||||
echo "" > pairgoth/pairgoth.properties
|
||||
echo "webapp.env = prod" >> pairgoth/pairgoth.properties
|
||||
echo "webapp.url = http://localhost:8080" >> pairgoth/pairgoth.properties
|
||||
echo "auth = none" >> pairgoth/pairgoth.properties
|
||||
echo "logger.level = info" >> pairgoth/pairgoth.properties
|
||||
echo "rating.ffg.enable = false" >> pairgoth/pairgoth.properties
|
||||
echo "webapp.protocol = http" >> pairgoth/pairgoth.properties
|
||||
|
||||
tar -czvf pairgoth.tar.gz pairgoth/
|
143
view-webapp/src/main/sass/explain.scss
Normal file
143
view-webapp/src/main/sass/explain.scss
Normal file
@@ -0,0 +1,143 @@
|
||||
@layer pairgoth {
|
||||
/* explain section */
|
||||
|
||||
#pairing-table-wrapper {
|
||||
padding: 8em 2em 1em 2em;
|
||||
}
|
||||
#pairing-table {
|
||||
border-collapse: collapse;
|
||||
width: max-content;
|
||||
thead {
|
||||
//border-collapse: collapse;
|
||||
tr {
|
||||
&:first-child {
|
||||
min-height: 8rem;
|
||||
th:first-child {
|
||||
background: linear-gradient(to right top, #ffffff 0%,#ffffff 49.9%,#000000 50%,#000000 51%,#ffffff 51.1%,#ffffff 100%);
|
||||
position: relative;
|
||||
min-width: 8rem;
|
||||
label.top-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
label.bottom-left {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
th:not(:first-child) {
|
||||
z-index: 5;
|
||||
width: 55px;
|
||||
height: 140px;
|
||||
white-space: nowrap;
|
||||
padding-bottom: 10px;
|
||||
position: relative;
|
||||
> div {
|
||||
transform: translate(35px, 51px) rotate(315deg);
|
||||
width: 30px;
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
> span {
|
||||
background-color: rgba(0, 0, 255, 0.2) !important;
|
||||
}
|
||||
}
|
||||
> span {
|
||||
border-bottom: 1px solid gray;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
> pre {
|
||||
display: none;
|
||||
font-size: smaller;
|
||||
line-height: 1em;
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
left: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
> pre {
|
||||
display: block;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
tr {
|
||||
th {
|
||||
text-align: left;
|
||||
padding-right: 1em;
|
||||
position: relative;
|
||||
> pre {
|
||||
display: none;
|
||||
font-size: smaller;
|
||||
line-height: 1em;
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
left: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 255, 0.2) !important;
|
||||
pre {
|
||||
display: block;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
height: 55px;
|
||||
width: 55px;
|
||||
border: solid 1px gray;
|
||||
position: relative;
|
||||
&.game::after {
|
||||
position: absolute;
|
||||
content: "X";
|
||||
color: blue;
|
||||
text-align: center;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
.weights {
|
||||
display: none;
|
||||
font-size: smaller;
|
||||
line-height: 1em;
|
||||
text-align: left;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
left: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 255, 0.7) !important;
|
||||
cursor: pointer;
|
||||
.weights {
|
||||
display: block;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#captures {
|
||||
padding: 1em;
|
||||
}
|
||||
}
|
||||
|
@@ -56,6 +56,10 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* header, center, footer */
|
||||
|
||||
#header {
|
||||
@@ -566,7 +570,7 @@
|
||||
}
|
||||
|
||||
/* TODO - plenty of those elements could just use the .noprint class */
|
||||
#header, #logo, #lang, .steps, #filter-box, #reglist-mode, #footer, #unpairables, #pairing-buttons, button, #standings-params, #logout, .pairing-stats, .result-sheets, .toggle, #overview, .tables-exclusion, .button, .noprint {
|
||||
#header, #logo, #lang, .steps, #filter-box, #reglist-mode, #footer, #unpairables, #pairing-buttons, button, #standings-params, #logout, .pairing-stats, .pairing-post-actions, .toggle, #overview, .tables-exclusion, .button, .noprint {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
@@ -377,6 +377,9 @@
|
||||
width: 6em;
|
||||
}
|
||||
}
|
||||
&:empty + .pairing-post-actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
#print-pairables {
|
||||
width: 100%;
|
||||
@@ -418,8 +421,10 @@
|
||||
padding: 0.2em 0.8em;
|
||||
}
|
||||
|
||||
.result-sheets {
|
||||
.pairing-post-actions {
|
||||
margin-top: 0.2em;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.bottom-pairing-actions {
|
||||
|
@@ -184,7 +184,7 @@ unpairable, non disponibles,
|
||||
supports the implémente le système d’appariement
|
||||
white blanc
|
||||
White Blanc
|
||||
white vs. black blanc vs. Noir
|
||||
white vs. black Blanc vs. Noir
|
||||
confirmed. confirmé(s).
|
||||
Note that login to this instance is reserved to French federation actors plus several external people at our discretion. Send us La connexion à cette instance est réservée aux acteurs de la FFG et à quelques personnes extérieures, à notre discrétion. Envoyez-nous
|
||||
yyyymmdd-city aaaammjj-ville
|
||||
|
115
view-webapp/src/main/webapp/explain.html
Normal file
115
view-webapp/src/main/webapp/explain.html
Normal file
@@ -0,0 +1,115 @@
|
||||
#macro(rank $rank)#if( $rank<0 )#set( $k = -$rank )${k}k#else#set( $d=$rank+1 )${d}d#end#end
|
||||
#if (!$tour)
|
||||
<div class="section">
|
||||
<h2 class="error">Invalid tournament id</h2>
|
||||
</div>
|
||||
#stop
|
||||
#end
|
||||
#set($round = $math.toInteger($!params.round))
|
||||
#if(!$round)
|
||||
#set($round = 1)
|
||||
#else
|
||||
#set($round = $math.min($math.max($round, 1), $tour.rounds))
|
||||
#end
|
||||
#if($tour.type == 'INDIVIDUAL' || $tour.type.startsWith('TEAM'))
|
||||
#set($parts = $api.get("tour/${params.id}/part"))
|
||||
#else
|
||||
#set($parts = $api.get("tour/${params.id}/team"))
|
||||
#end
|
||||
#set($pmap = $utils.toMap($parts))
|
||||
#set($roundPairing = $api.get("tour/${params.id}/pair/$round"))
|
||||
#if($roundPairing.error)
|
||||
<script type="text/javascript">
|
||||
onLoad(() => {
|
||||
showError("$roundPairing.error")
|
||||
});
|
||||
</script>
|
||||
#stop
|
||||
#end
|
||||
#set($explain = $api.get("tour/${params.id}/explain/$round"))
|
||||
<div id="pairing-table-wrapper">
|
||||
<table id="pairing-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<label class="top-right">white</label>
|
||||
<label class="bottom-left">black</label>
|
||||
</th>
|
||||
#foreach($white in $explain.paired)
|
||||
<th data-white="$white.id">
|
||||
<div>
|
||||
<span>
|
||||
$white.name $white.firstname
|
||||
</span>
|
||||
</div>
|
||||
<pre>$white.toPrettyString()</pre>
|
||||
</th>
|
||||
#end
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
#foreach($black in $explain.paired)
|
||||
<tr>
|
||||
<th data-black="$black.id">
|
||||
<div>
|
||||
<span>
|
||||
$black.name $black.firstname
|
||||
</span>
|
||||
</div>
|
||||
<pre>$black.toPrettyString()</pre>
|
||||
</th>
|
||||
#foreach($white in $explain.paired)
|
||||
#if($white.id != $black.id)
|
||||
#set($key = "$white.id-$black.id")
|
||||
#set($weights = $explain.weights[$key])
|
||||
#if($weights)
|
||||
#set($toMax = $explain.max - $weights.total)
|
||||
#set($toMin = $weights.total - $explain.min)
|
||||
#if ($toMax > $toMin)
|
||||
## total is close to min
|
||||
#set($percent = ($weights.total - $explain.min) / ($explain.low - $explain.min) * 40)
|
||||
#else
|
||||
## total is close to max
|
||||
#set($percent = 60 + 40 * (1 - ($explain.max - $weights.total) / ($explain.max - $explain.high)) )
|
||||
#end
|
||||
#end
|
||||
#set($game = $explain.games[$key])
|
||||
<td data-wb="$white.id-$black.id" #if($game)class="game"#end #if($weights)style="background-color: color-mix(in srgb, rgb(0 255 0) ${percent}%, rgb(255 0 0));"#end>
|
||||
<div class="weights">
|
||||
<pre>#if($weights)$weights.toPrettyString()#{else}Bye Player#end</pre>
|
||||
</div>
|
||||
</td>
|
||||
#else
|
||||
<td></td>
|
||||
#end
|
||||
#end
|
||||
</tr>
|
||||
#end
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="captures"></div>
|
||||
<script type="text/javascript">
|
||||
// #[[
|
||||
onLoad(() => {
|
||||
$('#header').hide();
|
||||
$('td').on('click', e => {
|
||||
const td = e.target.closest('td');
|
||||
const ids = td.data('wb')?.split(/-/);
|
||||
if (ids) {
|
||||
const white = $(`th[data-white="${ids[0]}"] div span`)?.text();
|
||||
const black = $(`th[data-white="${ids[1]}"] div span`)?.text();
|
||||
const weights = td.find('.weights pre').text();
|
||||
const captures = $('#captures')[0];
|
||||
captures.insertAdjacentHTML('beforeend', `<div>${white} vs ${black}<pre>${weights}</pre></div>`);
|
||||
}
|
||||
})
|
||||
$('th[data-white], th[data-black]').on('click', e => {
|
||||
const th = e.target.closest('th');
|
||||
const name = th.find('span').text();
|
||||
const info = th.find('pre').text();
|
||||
captures.insertAdjacentHTML('beforeend', `<div>${name}<pre>${info}</pre></div>`);
|
||||
});
|
||||
});
|
||||
// ]]#
|
||||
</script>
|
@@ -1,8 +1,8 @@
|
||||
#macro(rank $rank)#if( $rank<0 )#set( $k = -$rank )${k}k#else#set( $d=$rank+1 )${d}d#end#end
|
||||
#if (!$tour)
|
||||
<div class="section">
|
||||
<h2 class="error">Invalid tournament id</h2>
|
||||
</div>
|
||||
#stop
|
||||
#end
|
||||
#set($round = $math.toInteger($!params.round))
|
||||
#if(!$round)
|
||||
@@ -27,60 +27,36 @@
|
||||
#stop
|
||||
#end
|
||||
#set($games = $utils.removeBye($roundPairing.games))
|
||||
#set($pages = ($games.size() + 3) / 4)
|
||||
#set($items = $pages * 4)
|
||||
#foreach($i in [1..$items])
|
||||
#set($j = ($i - 1) / 4 + (($i - 1) % 4) * $pages)
|
||||
#if($j < $games.size())
|
||||
#set($game = $games[$j])
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Table</th>
|
||||
<th>Black</th>
|
||||
<th>White</th>
|
||||
<th>Handicap</th>
|
||||
<th>Komi</th>
|
||||
</tr>
|
||||
|
||||
#set($numOfPlay = $games.size() - 1)
|
||||
#foreach($i in [0..$numOfPlay])
|
||||
|
||||
#set($game = $games[$i])
|
||||
#set($white = $pmap[$game.w])
|
||||
#set($black = $pmap[$game.b])
|
||||
#else
|
||||
#set($game = { 't': 'xxx', 'h': 'xxx' })
|
||||
#set($white = { 'name': 'xxx', 'firstname': 'xxx', 'rank': -99, 'country': 'XX', 'club': 'xxx' })
|
||||
#set($black = { 'name': 'xxx', 'firstname': 'xxx', 'rank': -99, 'country': 'XX', 'club': 'xxx' })
|
||||
#end
|
||||
#if($foreach.index % 4 == 0)
|
||||
<div class="page">
|
||||
#end
|
||||
|
||||
<div class="page-item">
|
||||
<div class="title">$tour.name</div>
|
||||
<div class="subtitle"></div>
|
||||
<div class="details">
|
||||
<div>Table $game.t</div>
|
||||
#set($komi = $tour.komi)
|
||||
#if($game.h) #set($komi = $komi - $math.floor($komi)) #end
|
||||
<div>Handicap $game.h ‐ Komi $komi</div>
|
||||
<div>Round $round</div>
|
||||
</div>
|
||||
<div class="instructions">
|
||||
Surround winner's name or ½-½
|
||||
</div>
|
||||
<div class="players">
|
||||
<div class="white player">
|
||||
<div class="color">White</div>
|
||||
<div class="name">$white.name $!white.firstname #rank($white.rank)<br/>#if($white.country)($white.country.toUpperCase()#if($white.club), $white.club#end)#end</div>
|
||||
## <div class="pin">$white.egf</div>
|
||||
</div>
|
||||
<div class="equal">½-½</div>
|
||||
<div class="black player">
|
||||
<div class="color">Black</div>
|
||||
<div class="name">$black.name $!black.firstname #rank($black.rank)<br/>#if($black.country)($black.country.toUpperCase()#if($black.club), $black.club#end)#end</div>
|
||||
## <div class="pin">$black.egf</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="signatures">
|
||||
<div class="signature">Signature:</div>
|
||||
<div class="equal"> </div>
|
||||
<div class="signature">Signature:</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#if($foreach.index % 4 == 3)
|
||||
</div>
|
||||
#end
|
||||
#set($table = $i + 1)
|
||||
<tr>
|
||||
<td>Table $table</td>
|
||||
<td>$black.name $!black.firstname</td>
|
||||
<td>$white.name $!white.firstname</td>
|
||||
<td>$game.h</td>
|
||||
<td>$komi</td>
|
||||
</tr>
|
||||
#end
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
onLoad(() => {
|
||||
|
@@ -69,7 +69,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div id="paired" class="multi-select" title="white vs. black">
|
||||
<div id="paired" class="multi-select" title="white vs. black">##
|
||||
#foreach($game in $games)
|
||||
#set($white = $pmap[$game.w])
|
||||
#set($black = $pmap[$game.b])
|
||||
@@ -80,10 +80,13 @@
|
||||
<div class="black" data-id="$game.b">#if($black)$black.name#if($black.firstname) $black.firstname#end#{else}BIP#end</div>
|
||||
<div class="handicap" data-value="$game.h">#if($game.h)h$game.h#{else} #end</div>
|
||||
</div>
|
||||
#end
|
||||
</div>
|
||||
#end##
|
||||
#* *#</div>
|
||||
#if(!$tour.type.startsWith('TEAM'))
|
||||
<div class="result-sheets"><a href="result-sheets?id=${tour.id}&round=${round}" target="_blank" class="ui mini floating icon button">result sheets <i class="fa fa-external-link"></i></a></div>
|
||||
<div class="pairing-post-actions">
|
||||
<a href="result-sheets?id=${tour.id}&round=${round}" target="_blank" class="ui mini floating icon button">result sheets <i class="fa fa-external-link"></i></a>
|
||||
<a href="explain?id=${tour.id}&round=${round}" target="_blank" class="ui mini floating icon button">explain pairing <i class="fa fa-external-link"></i></a>
|
||||
</div>
|
||||
#end
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -18,21 +18,21 @@
|
||||
<table id="results-table" class="ui celled striped table">
|
||||
<thead class="centered">
|
||||
<th data-sort-method="number">table</th>
|
||||
<th>white</th>
|
||||
<th>black</th>
|
||||
<th>white</th>
|
||||
<th>hd</th>
|
||||
<th>result</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
#set($dispRst = {'?':'?', 'w':'1-0', 'b':'0-1', '=':'½-½', 'X':'X', '#':'1-1', '0':'0-0'})
|
||||
#set($dispRst = {'?':'?', 'w':'0-1', 'b':'1-0', '=':'½-½', 'X':'X', '#':'1-1', '0':'0-0'})
|
||||
#foreach($game in $individualGames)
|
||||
#set($white = $plmap[$game.w])
|
||||
#set($black = $plmap[$game.b])
|
||||
#if($black && $white)
|
||||
<tr id="result-$game.id" data-id="$game.id">
|
||||
<td data-sort="$game.t">${game.t}.</td>
|
||||
<td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #rank($white.rank)#{else}BIP#end</span></td>
|
||||
<td class="black player #if($game.r == 'b' || $game.r == '#') winner #elseif($game.r == 'w' || $game.r == '0') looser #end" data-id="$black.id" data-sort="$black.name#if($black.firstname) $black.firstname#end"><span>#if($black)$black.name#if($black.firstname) $black.firstname#end #rank($black.rank)#{else}BIP#end</span></td>
|
||||
<td class="white player #if($game.r == 'w' || $game.r == '#') winner #elseif($game.r == 'b' || $game.r == '0') looser #end" data-id="$white.id" data-sort="$white.name#if($white.firstname) $white.firstname#end"><span>#if($white)$white.name#if($white.firstname) $white.firstname#end #rank($white.rank)#{else}BIP#end</span></td>
|
||||
<td class="handicap centered">$!game.h</td>
|
||||
<td class="result centered" data-sort="$game.r" data-result="$game.r">$dispRst[$game.r]</td>
|
||||
</tr>
|
||||
|
@@ -30,6 +30,9 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
#if($tour.type.startsWith('TEAM'))
|
||||
<div class="strong">Team Standings</div>
|
||||
#end
|
||||
<div id="standings-container" class="roundbox">
|
||||
#set($standings = $api.get("tour/${params.id}/standings/$round"))
|
||||
#if($standings.isObject() && ($standings.error || $standings.message))
|
||||
@@ -105,6 +108,78 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
#if($tour.type.startsWith('TEAM'))
|
||||
<div class="strong">Individual Standings</div>
|
||||
<div id="individual-standings-container" class="roundbox">
|
||||
#set($indvstandings = $api.get("tour/${params.id}/standings/$round?individual_standings=true"))
|
||||
#if($indvstandings.isObject() && ($indvstandings.error || $indvstandings.message))
|
||||
#if($indvstandings.error)
|
||||
#set($error = $indvstandings.error)
|
||||
#else
|
||||
#set($error = $indvstandings.message)
|
||||
#end
|
||||
<script type="text/javascript">
|
||||
onLoad(() => {
|
||||
showError("$error")
|
||||
});
|
||||
</script>
|
||||
#set($indvstandings = [])
|
||||
#end
|
||||
#set($indvsmap = {})
|
||||
#foreach($part in $indvstandings)
|
||||
#set($indvsmap[$part.num] = $part)
|
||||
#end
|
||||
<table id="individual-standings-table" class="ui striped table">
|
||||
<thead>
|
||||
<th>Num</th>
|
||||
<th>Plc</th>
|
||||
<th>Name</th>
|
||||
<th>Rank</th>
|
||||
<th>Ctr</th>
|
||||
<th>Nbw</th>
|
||||
#foreach($r in [1..$round])
|
||||
<th>R$r</th>
|
||||
#end
|
||||
#set($indvcriteres = ['NBW'])
|
||||
#foreach($crit in $indvcriteres)
|
||||
<th>$crit</th>
|
||||
#end
|
||||
</thead>
|
||||
<tbody>
|
||||
#foreach($part in $indvstandings)
|
||||
<tr data-id="$part.id">
|
||||
<td>$part.num</td>
|
||||
<td>$part.place</td>
|
||||
<td>$esc.html($part.name)#if($part.firstname) $esc.html($part.firstname)#end</td>
|
||||
<td data-sort="$part.rank">#rank($part.rank)</td>
|
||||
<td>#if($part.country)$part.country#end</td>
|
||||
<td>$number.format('0.#', $part.NBW)</td>
|
||||
#set($mx = $round - 1)
|
||||
#foreach($r in [0..$mx])
|
||||
#set($rst = $part.results[$r])
|
||||
#set($opp_num = $math.toLong($rst))
|
||||
#if($opp_num)
|
||||
#set($opponent = $!indvsmap[$opp_num])
|
||||
#else
|
||||
#set($opponent = false)
|
||||
#end
|
||||
#if($rst.contains('+'))
|
||||
#set($rst = "<b>$rst</b>")
|
||||
#elseif($rst.contains('-'))
|
||||
#set($rst = "<i>$rst</i>")
|
||||
#end
|
||||
<td class="nobreak game-result" #if($opponent)title="$esc.html($opponent.name)#if($opponent.firstname) $esc.html($opponent.firstname)#end #rank($opponent.rank)#if($opponent.country) $opponent.country#end"#end>$rst</td>
|
||||
#end
|
||||
#foreach($crit in $indvcriteres)
|
||||
#set($value = "$number.format('0.#', $part[$crit])")
|
||||
<td data-sort="$value">$value.replace('.5', '½')</td>
|
||||
#end
|
||||
</tr>
|
||||
#end
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
#end
|
||||
<div class="right form-actions">
|
||||
#if(!$tour.frozen && $round == $tour.rounds)
|
||||
<button id="freeze" class="ui orange floating right labeled icon button">
|
||||
|
Reference in New Issue
Block a user