Merge branch 'pairing2' of gitlab.jeudego.org:tournois/pairgoth into pairing2

This commit is contained in:
Quentin Rendu
2023-10-02 10:03:11 +02:00
7 changed files with 100 additions and 33 deletions

View File

@@ -12,9 +12,28 @@ import java.util.*
import javax.xml.datatype.XMLGregorianCalendar
private const val MILLISECONDS_PER_DAY = 86400000
fun XMLGregorianCalendar.toLocalDate() = LocalDate.fromEpochDays((toGregorianCalendar().time.time / MILLISECONDS_PER_DAY).toInt())
fun XMLGregorianCalendar.toLocalDate() = LocalDate(year, month, day)
object OpenGotha {
private fun parseDrawUpDownMode(str: String) = when (str) {
"BOT" -> MainCritParams.DrawUpDown.BOTTOM
"MID" -> MainCritParams.DrawUpDown.MIDDLE
"TOP" -> MainCritParams.DrawUpDown.TOP
else -> throw Error("Invalid drawUpDown mode: $str")
}
private fun parseSeedSystem(str: String) = when (str) {
"SPLITANDSLIP" -> MainCritParams.SeedMethod.SPLIT_AND_SLIP
"SPLITANDRANDOM" -> MainCritParams.SeedMethod.SPLIT_AND_RANDOM
"SPLITANDFOLD" -> MainCritParams.SeedMethod.SPLIT_AND_FOLD
else -> throw Error("Invalid seed system: $str")
}
private fun String.titlecase(locale: Locale = Locale.ROOT) = lowercase(locale).replaceFirstChar { it.titlecase(locale) }
private fun MainCritParams.SeedMethod.format() = toString().replace("_", "")
fun import(element: Element): Tournament<*> {
val context = JAXBContext.newInstance(ObjectFactory::class.java)
@@ -28,6 +47,57 @@ object OpenGotha {
val placmtParams = ogTournament.tournamentParameterSet.placementParameterSet
val pairParams = ogTournament.tournamentParameterSet.pairingParameterSet
val pairgothPairingParams = PairingParams(
base = BaseCritParams(
nx1 = pairParams.paiStandardNX1Factor.toDouble(),
dupWeight = pairParams.paiBaAvoidDuplGame.toDouble(),
random = pairParams.paiBaRandom.toDouble(),
deterministic = pairParams.paiBaDeterministic.toBoolean(),
colorBalanceWeight = pairParams.paiBaBalanceWB.toDouble()
),
main = MainCritParams(
categoriesWeight = pairParams.paiMaAvoidMixingCategories.toDouble(),
scoreWeight = pairParams.paiMaMinimizeScoreDifference.toDouble(),
drawUpDownWeight = pairParams.paiMaDUDDWeight.toDouble(),
compensateDrawUpDown = pairParams.paiMaCompensateDUDD.toBoolean(),
drawUpDownUpperMode = parseDrawUpDownMode(pairParams.paiMaDUDDUpperMode),
drawUpDownLowerMode = parseDrawUpDownMode(pairParams.paiMaDUDDLowerMode),
seedingWeight = pairParams.paiMaMaximizeSeeding.toDouble(),
lastRoundForSeedSystem1 = pairParams.paiMaLastRoundForSeedSystem1,
seedSystem1 = parseSeedSystem(pairParams.paiMaSeedSystem1),
seedSystem2 = parseSeedSystem(pairParams.paiMaSeedSystem2 ?: "SPLITANDSLIP"),
additionalPlacementCritSystem1 = Criterion.valueOf(pairParams.paiMaAdditionalPlacementCritSystem1.uppercase()),
additionalPlacementCritSystem2 = Criterion.valueOf(pairParams.paiMaAdditionalPlacementCritSystem2.uppercase().replace("NULL", "NONE"))
),
secondary = SecondaryCritParams(
barThresholdActive = pairParams.paiSeBarThresholdActive.toBoolean(),
rankThreshold = Pairable.parseRank(pairParams.paiSeRankThreshold),
nbWinsThresholdActive = pairParams.paiSeNbWinsThresholdActive.toBoolean(),
defSecCrit = pairParams.paiSeDefSecCrit.toDouble()
),
geo = GeographicalParams(
avoidSameGeo = pairParams.paiSeAvoidSameGeo.toDouble(),
preferMMSDiffRatherThanSameCountry = pairParams.paiSePreferMMSDiffRatherThanSameCountry,
preferMMSDiffRatherThanSameClubsGroup = 2,
preferMMSDiffRatherThanSameClub = pairParams.paiSePreferMMSDiffRatherThanSameClub
),
handicap = HandicapParams(
weight = pairParams.paiSeMinimizeHandicap.toDouble(),
useMMS = handParams.hdBasedOnMMS.toBoolean(),
rankThreshold = Pairable.parseRank(pairParams.paiSeRankThreshold),
correction = handParams.hdCorrection,
ceiling = handParams.hdCeiling
)
)
val pairgothPlacementParams = PlacementParams(
crit = placmtParams.placementCriteria.placementCriterion.filter {
it.name != "NULL"
}.map {
Criterion.valueOf(it.name)
}.toTypedArray()
)
val tournament = StandardTournament(
id = Store.nextTournamentId,
type = Tournament.Type.INDIVIDUAL, // CB for now, TODO
@@ -46,26 +116,8 @@ object OpenGotha {
else -> throw Error("missing byoyomi type")
},
pairing = when (handParams.hdCeiling) {
0 -> Swiss(
pairingParams = PairingParams(
),
placementParams = PlacementParams(
crit = placmtParams.placementCriteria.placementCriterion.filter {
it.name != "NULL"
}.map {
Criterion.valueOf(it.name)
}.toTypedArray()
)
) // TODO
else -> MacMahon(
pairingParams = PairingParams(
),
placementParams = PlacementParams(
)
) // TODO
0 -> Swiss(pairingParams = pairgothPairingParams, placementParams = pairgothPlacementParams)
else -> MacMahon(pairingParams = pairgothPairingParams, placementParams = pairgothPlacementParams)
},
rounds = genParams.numberOfRounds
)
@@ -123,7 +175,7 @@ object OpenGotha {
// method 2 (quick and dirty) is to rely on templating:
val xml = """
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Tournament dataVersion="201" externalIPAddress="88.122.144.219" fullVersionNumber="3.51" runningMode="SAL" saveDT="20210111180800">
<Tournament dataVersion="201" externalIPAddress="127.0.0.1" fullVersionNumber="3.51" runningMode="SAL" saveDT="20210111180800">
<Players>
${tournament.pairables.values.map { player ->
player as Player
@@ -200,7 +252,7 @@ object OpenGotha {
}
</PlacementCriteria>
</PlacementParameterSet>
<PairingParameterSet paiBaAvoidDuplGame="${tournament.pairing.pairingParams.base.dupWeight.toInt()}" paiBaBalanceWB="${tournament.pairing.pairingParams.base.colorBalanceWeight.toInt()}" paiBaDeterministic="${tournament.pairing.pairingParams.base.deterministic}" paiBaRandom="${tournament.pairing.pairingParams.base.random.toInt()}" paiMaAdditionalPlacementCritSystem1="Rating" paiMaAdditionalPlacementCritSystem2="Rating" paiMaAvoidMixingCategories="0" paiMaCompensateDUDD="true" paiMaDUDDLowerMode="MID" paiMaDUDDUpperMode="MID" paiMaDUDDWeight="100000000" paiMaLastRoundForSeedSystem1="2" paiMaMaximizeSeeding="5000000" paiMaMinimizeScoreDifference="100000000000" paiMaSeedSystem1="SPLITANDSLIP" paiMaSeedSystem2="SPLITANDSLIP" paiSeAvoidSameGeo="0" paiSeBarThresholdActive="true" paiSeDefSecCrit="20000000000000" paiSeMinimizeHandicap="0" paiSeNbWinsThresholdActive="true" paiSePreferMMSDiffRatherThanSameClub="0" paiSePreferMMSDiffRatherThanSameCountry="0" paiSeRankThreshold="30K" paiStandardNX1Factor="0.5"/>
<PairingParameterSet paiBaAvoidDuplGame="${tournament.pairing.pairingParams.base.dupWeight.toLong()}" paiBaBalanceWB="${tournament.pairing.pairingParams.base.colorBalanceWeight.toLong()}" paiBaDeterministic="${tournament.pairing.pairingParams.base.deterministic}" paiBaRandom="${tournament.pairing.pairingParams.base.random.toLong()}" paiMaAdditionalPlacementCritSystem1="${tournament.pairing.pairingParams.main.additionalPlacementCritSystem1.toString().titlecase()}" paiMaAdditionalPlacementCritSystem2="${tournament.pairing.pairingParams.main.additionalPlacementCritSystem2.toString().titlecase()}" paiMaAvoidMixingCategories="${tournament.pairing.pairingParams.main.categoriesWeight.toLong()}" paiMaCompensateDUDD="${tournament.pairing.pairingParams.main.compensateDrawUpDown}" paiMaDUDDLowerMode="${tournament.pairing.pairingParams.main.drawUpDownLowerMode.toString().substring(0, 3)}" paiMaDUDDUpperMode="${tournament.pairing.pairingParams.main.drawUpDownUpperMode.toString().substring(0, 3)}" paiMaDUDDWeight="${tournament.pairing.pairingParams.main.drawUpDownWeight.toLong()}" paiMaLastRoundForSeedSystem1="${tournament.pairing.pairingParams.main.lastRoundForSeedSystem1}" paiMaMaximizeSeeding="${tournament.pairing.pairingParams.main.seedingWeight.toLong()}" paiMaMinimizeScoreDifference="${tournament.pairing.pairingParams.main.scoreWeight.toLong()}" paiMaSeedSystem1="${tournament.pairing.pairingParams.main.seedSystem1.format()}" paiMaSeedSystem2="${tournament.pairing.pairingParams.main.seedSystem2.format()}" paiSeAvoidSameGeo="${tournament.pairing.pairingParams.geo.avoidSameGeo.toLong()}" paiSeBarThresholdActive="${tournament.pairing.pairingParams.secondary.barThresholdActive}" paiSeDefSecCrit="${tournament.pairing.pairingParams.secondary.defSecCrit.toLong()}" paiSeMinimizeHandicap="${tournament.pairing.pairingParams.handicap.weight.toLong()}" paiSeNbWinsThresholdActive="${tournament.pairing.pairingParams.secondary.nbWinsThresholdActive}" paiSePreferMMSDiffRatherThanSameClub="${tournament.pairing.pairingParams.geo.preferMMSDiffRatherThanSameClub}" paiSePreferMMSDiffRatherThanSameCountry="${tournament.pairing.pairingParams.geo.preferMMSDiffRatherThanSameCountry}" paiSeRankThreshold="${displayRank(tournament.pairing.pairingParams.secondary.rankThreshold).uppercase()}" paiStandardNX1Factor="${tournament.pairing.pairingParams.base.nx1}"/>
<DPParameterSet displayClCol="true" displayCoCol="true" displayIndGamesInMatches="true" displayNPPlayers="false" displayNumCol="true" displayPlCol="true" gameFormat="short" playerSortType="name" showByePlayer="true" showNotFinallyRegisteredPlayers="true" showNotPairedPlayers="true" showNotParticipatingPlayers="false" showPlayerClub="true" showPlayerCountry="false" showPlayerGrade="true"/>
<PublishParameterSet exportToLocalFile="true" htmlAutoScroll="false" print="false"/>
</TournamentParameterSet>

View File

@@ -42,9 +42,9 @@ fun Game.toJson() = Json.Object(
fun Game.Companion.fromJson(json: Json.Object) = Game(
id = json.getID("id") ?: throw Error("missing game id"),
white = json.getID("white") ?: throw Error("missing white player"),
black = json.getID("black") ?: throw Error("missing black player"),
handicap = json.getInt("handicap") ?: 0,
result = json.getChar("result")?.let { Game.Result.fromSymbol(it) } ?: UNKNOWN,
drawnUpDown = json.getInt("drawnUpDown") ?: 0
white = json.getID("w") ?: throw Error("missing white player"),
black = json.getID("b") ?: throw Error("missing black player"),
handicap = json.getInt("h") ?: 0,
result = json.getChar("r")?.let { Game.Result.fromSymbol(it) } ?: UNKNOWN,
drawnUpDown = json.getInt("dd") ?: 0
)

View File

@@ -32,7 +32,7 @@ open class HistoryHelper(protected val history: List<List<Game>>, scoresGetter:
}).toSet()
}
// Returns the number of games played as white
// 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 {
history.flatten().filter { game ->

View File

@@ -430,12 +430,15 @@ sealed class Solver(
// placeInGroup (of same score) : Pair(place, groupSize)
private val Pairable.placeInGroup: Pair<Int, Int> get() = _placeInGroup[id]!!
private val _placeInGroup by lazy {
// group by group number
sortedPairables.groupBy {
it.group
// get a list { id { placeInGroup, groupSize } }
}.values.flatMap { group ->
group.mapIndexed { index, pairable ->
Pair(pairable.id, Pair(index, group.size))
}
// get a map id -> { placeInGroup, groupSize }
}.toMap()
}

View File

@@ -276,7 +276,7 @@ object XmlUtils {
* @param xml xml string
* @return The document object
*/
fun parse(xml: String): Element = parse(StringReader(xml))
fun parse(xml: String): Element = parse(StringReader(xml.trim()))
/**
* Search for nodes using an XPath expression

View File

@@ -146,7 +146,7 @@
<xs:attribute type="xs:int" name="paiSePreferMMSDiffRatherThanSameClub"/>
<xs:attribute type="xs:int" name="paiSePreferMMSDiffRatherThanSameCountry"/>
<xs:attribute type="xs:string" name="paiSeRankThreshold"/>
<xs:attribute type="xs:float" name="paiStandardNX1Factor"/>
<xs:attribute type="xs:double" name="paiStandardNX1Factor"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>

View File

@@ -1,12 +1,19 @@
package org.jeudego.pairgoth.test
import org.jeudego.pairgoth.ext.OpenGotha
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.util.XmlUtils
import org.junit.jupiter.api.Test
import java.nio.charset.StandardCharsets
import kotlin.test.assertEquals
class ImportExportTests: TestBase() {
companion object {
val maskIdRegex = Regex("(?<=\"id\" ?: )\\d+")
}
/*
@Test
fun `001 test imports`() {
getTestResources("opengotha/tournamentfiles/").forEach { file ->
@@ -27,18 +34,23 @@ class ImportExportTests: TestBase() {
}
}
*/
@Test
fun `002 test opengotha import export`() {
// We import a tournament
// Check that after exporting and reimporting we get the same pairgoth tournament object
getTestResources("opengotha").forEach { file ->
getTestResources("opengotha/tournamentfiles").forEach { file ->
val resource = file.readText(StandardCharsets.UTF_8)
val root_xml = XmlUtils.parse(resource)
val tournament = OpenGotha.import(root_xml)
val jsonTournament = tournament.toJson().toPrettyString()!!.replace(maskIdRegex, "0")
val exported = OpenGotha.export(tournament)
val tournament2 = OpenGotha.import(XmlUtils.parse(exported))
assert(tournament == tournament2)
val jsonTournament2 = tournament2.toJson().toPrettyString()!!.replace(maskIdRegex, "0")
assertEquals(jsonTournament, jsonTournament2)
}
}
}