Add MacMahon 3.9 import support
- Add MacMahon39.kt parser for MM39 tournament format - Auto-detect MM39 format in tournament import - Import players, games, bye players, and tournament parameters - Uses default values for time system and director since MM39 lacks those
This commit is contained in:
@@ -5,6 +5,7 @@ import com.republicate.kson.toJsonObject
|
||||
import com.republicate.kson.toMutableJsonObject
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.PAYLOAD_KEY
|
||||
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
|
||||
import org.jeudego.pairgoth.ext.MacMahon39
|
||||
import org.jeudego.pairgoth.ext.OpenGotha
|
||||
import org.jeudego.pairgoth.model.BaseCritParams
|
||||
import org.jeudego.pairgoth.model.TeamTournament
|
||||
@@ -55,7 +56,7 @@ object TournamentHandler: PairgothApiHandler {
|
||||
override fun post(request: HttpServletRequest, response: HttpServletResponse): Json? {
|
||||
val tournament = when (val payload = request.getAttribute(PAYLOAD_KEY)) {
|
||||
is Json.Object -> Tournament.fromJson(getObjectPayload(request))
|
||||
is Element -> OpenGotha.import(payload)
|
||||
is Element -> if (MacMahon39.isFormat(payload)) MacMahon39.import(payload) else OpenGotha.import(payload)
|
||||
else -> badRequest("missing or invalid payload")
|
||||
}
|
||||
tournament.recomputeDUDD()
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
package org.jeudego.pairgoth.ext
|
||||
|
||||
import org.jeudego.pairgoth.model.*
|
||||
import org.jeudego.pairgoth.store.nextGameId
|
||||
import org.jeudego.pairgoth.store.nextPlayerId
|
||||
import org.jeudego.pairgoth.store.nextTournamentId
|
||||
import org.w3c.dom.Element
|
||||
import org.w3c.dom.Node
|
||||
import org.w3c.dom.NodeList
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* MacMahon 3.9 format import support
|
||||
* Ported from OpenGothaCustom (https://bitbucket.org/kamyszyn/opengothacustom)
|
||||
*/
|
||||
object MacMahon39 {
|
||||
|
||||
/**
|
||||
* Check if the XML element is in MacMahon 3.9 format
|
||||
*/
|
||||
fun isFormat(element: Element): Boolean {
|
||||
val tournament = element.getElementsByTagName("Tournament").item(0) ?: return false
|
||||
val typeVersion = tournament.attributes?.getNamedItem("typeversion")?.nodeValue
|
||||
return typeVersion != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a MacMahon 3.9 format tournament
|
||||
*/
|
||||
fun import(element: Element): Tournament<*> {
|
||||
val tournamentEl = element.getElementsByTagName("Tournament").item(0) as? Element
|
||||
?: throw Error("No Tournament element found")
|
||||
|
||||
// Parse tournament settings
|
||||
val name = extractValue("Name", tournamentEl, "Tournament")
|
||||
val numberOfRounds = extractValue("NumberOfRounds", tournamentEl, "5").toInt()
|
||||
val mmBarStr = extractValue("UpperMacMahonBarLevel", tournamentEl, "1d")
|
||||
val mmFloorStr = extractValue("LowerMacMahonBarLevel", tournamentEl, "30k")
|
||||
val isMMBar = extractValue("UpperMacMahonBar", tournamentEl, "true") == "true"
|
||||
val isMMFloor = extractValue("LowerMacMahonBar", tournamentEl, "true") == "true"
|
||||
val handicapUsed = extractValue("HandicapUsed", tournamentEl, "false").equals("true", ignoreCase = true)
|
||||
val handicapByRank = extractValue("HandicapByLevel", tournamentEl, "false").equals("true", ignoreCase = true)
|
||||
val handicapBelowStr = extractValue("HandicapBelowLevel", tournamentEl, "30k")
|
||||
val isHandicapBelow = extractValue("HandicapBelow", tournamentEl, "true").equals("true", ignoreCase = true)
|
||||
val handicapCorrectionStr = extractValue("HandicapAdjustmentValue", tournamentEl, "0")
|
||||
val isHandicapReduction = extractValue("HandicapAdjustment", tournamentEl, "true").equals("true", ignoreCase = true)
|
||||
val handicapCeilingStr = extractValue("HandicapLimitValue", tournamentEl, "9")
|
||||
val isHandicapLimit = extractValue("HandicapLimit", tournamentEl, "true").equals("true", ignoreCase = true)
|
||||
|
||||
// Parse placement criteria from Walllist
|
||||
val walllistEl = element.getElementsByTagName("Walllist").item(0) as? Element
|
||||
val breakers = if (walllistEl != null) extractValues("ShortName", walllistEl) else listOf("Score", "SOS", "SOSOS")
|
||||
|
||||
// Determine effective values
|
||||
val mmBar = if (isMMBar) parseRank(mmBarStr) else 8 // 9d
|
||||
val mmFloor = if (isMMFloor) parseRank(mmFloorStr) else -30 // 30k
|
||||
val handicapBelow = if (isHandicapBelow) parseRank(handicapBelowStr) else 8 // 9d
|
||||
val handicapCorrection = if (isHandicapReduction) -1 * handicapCorrectionStr.toInt() else 0
|
||||
val handicapCeiling = when {
|
||||
!handicapUsed -> 0
|
||||
!isHandicapLimit -> 30
|
||||
else -> handicapCeilingStr.toInt()
|
||||
}
|
||||
|
||||
// Create pairing parameters
|
||||
val pairingParams = PairingParams(
|
||||
base = BaseCritParams(),
|
||||
main = MainCritParams(),
|
||||
secondary = SecondaryCritParams(),
|
||||
geo = GeographicalParams(),
|
||||
handicap = HandicapParams(
|
||||
useMMS = !handicapByRank,
|
||||
rankThreshold = handicapBelow,
|
||||
correction = handicapCorrection,
|
||||
ceiling = handicapCeiling
|
||||
)
|
||||
)
|
||||
|
||||
// Create placement parameters from breakers
|
||||
val placementCrit = breakers.take(6).mapNotNull { translateBreaker(it, breakers.firstOrNull() == "Points") }.toTypedArray()
|
||||
val placementParams = PlacementParams(crit = if (placementCrit.isEmpty()) arrayOf(Criterion.MMS, Criterion.SOSM, Criterion.SOSOSM) else placementCrit)
|
||||
|
||||
// Create tournament
|
||||
val tournament = StandardTournament(
|
||||
id = nextTournamentId,
|
||||
type = Tournament.Type.INDIVIDUAL,
|
||||
name = name,
|
||||
shortName = name.take(20),
|
||||
startDate = LocalDate.now(),
|
||||
endDate = LocalDate.now(),
|
||||
director = "",
|
||||
country = "",
|
||||
location = "",
|
||||
online = false,
|
||||
timeSystem = SuddenDeath(3600), // Default: 1 hour sudden death
|
||||
pairing = MacMahon(
|
||||
pairingParams = pairingParams,
|
||||
placementParams = placementParams,
|
||||
mmFloor = mmFloor,
|
||||
mmBar = mmBar
|
||||
),
|
||||
rounds = numberOfRounds
|
||||
)
|
||||
|
||||
// Parse players
|
||||
val playerIdMap = mutableMapOf<String, ID>()
|
||||
val goPlayers = element.getElementsByTagName("GoPlayer")
|
||||
for (i in 0 until goPlayers.length) {
|
||||
val playerEl = goPlayers.item(i) as? Element ?: continue
|
||||
val parentEl = playerEl.parentNode as? Element ?: continue
|
||||
|
||||
val mm39Id = extractValue("Id", parentEl, "1")
|
||||
val egfPin = extractValue("EgdPin", playerEl, "").let { if (it.length < 8) "" else it }
|
||||
val firstname = extractValue("FirstName", playerEl, " ")
|
||||
val surname = extractValue("Surname", playerEl, " ")
|
||||
val club = extractValue("Club", playerEl, "")
|
||||
val country = extractValue("Country", playerEl, "").uppercase()
|
||||
val rankStr = extractValue("GoLevel", playerEl, "30k")
|
||||
val rank = parseRank(rankStr)
|
||||
val ratingStr = extractValue("Rating", playerEl, "-901")
|
||||
val rating = ratingStr.toInt()
|
||||
val superBarMember = extractValue("SuperBarMember", parentEl, "false") == "true"
|
||||
val preliminary = extractValue("PreliminaryRegistration", parentEl, "false") == "true"
|
||||
|
||||
val player = Player(
|
||||
id = nextPlayerId,
|
||||
name = surname,
|
||||
firstname = firstname,
|
||||
rating = rating,
|
||||
rank = rank,
|
||||
country = if (country == "GB") "UK" else country,
|
||||
club = club,
|
||||
final = !preliminary,
|
||||
mmsCorrection = if (superBarMember) 1 else 0
|
||||
).also {
|
||||
if (egfPin.isNotEmpty()) {
|
||||
it.externalIds[DatabaseId.EGF] = egfPin
|
||||
}
|
||||
// Parse not playing rounds
|
||||
val notPlayingRounds = extractValues("NotPlayingInRound", parentEl)
|
||||
for (roundStr in notPlayingRounds) {
|
||||
val round = roundStr.toIntOrNull() ?: continue
|
||||
it.skip.add(round)
|
||||
}
|
||||
}
|
||||
|
||||
playerIdMap[mm39Id] = player.id
|
||||
tournament.players[player.id] = player
|
||||
}
|
||||
|
||||
// Parse games (pairings)
|
||||
val pairings = element.getElementsByTagName("Pairing")
|
||||
for (i in 0 until pairings.length) {
|
||||
val pairingEl = pairings.item(i) as? Element ?: continue
|
||||
val parentEl = pairingEl.parentNode as? Element ?: continue
|
||||
|
||||
val isByeGame = extractValue("PairingWithBye", pairingEl, "false").equals("true", ignoreCase = true)
|
||||
val roundNumber = extractValue("RoundNumber", parentEl, "1").toInt()
|
||||
val boardNumber = extractValue("BoardNumber", pairingEl, "${i + 1}").toInt()
|
||||
|
||||
if (isByeGame) {
|
||||
// Bye player
|
||||
val blackId = extractValue("Black", pairingEl, "")
|
||||
val playerId = playerIdMap[blackId] ?: continue
|
||||
val game = Game(
|
||||
id = nextGameId,
|
||||
table = 0,
|
||||
white = playerId,
|
||||
black = 0,
|
||||
result = Game.Result.WHITE
|
||||
)
|
||||
tournament.games(roundNumber)[game.id] = game
|
||||
} else {
|
||||
// Regular game
|
||||
val whiteId = extractValue("White", pairingEl, "")
|
||||
val blackId = extractValue("Black", pairingEl, "")
|
||||
val whitePId = playerIdMap[whiteId] ?: continue
|
||||
val blackPId = playerIdMap[blackId] ?: continue
|
||||
val handicap = extractValue("Handicap", pairingEl, "0").toInt()
|
||||
val resultStr = extractValue("Result", pairingEl, "?-?")
|
||||
val resultByRef = extractValue("ResultByReferee", pairingEl, "false").equals("true", ignoreCase = true)
|
||||
|
||||
val game = Game(
|
||||
id = nextGameId,
|
||||
table = boardNumber,
|
||||
white = whitePId,
|
||||
black = blackPId,
|
||||
handicap = handicap,
|
||||
result = parseResult(resultStr, resultByRef)
|
||||
)
|
||||
tournament.games(roundNumber)[game.id] = game
|
||||
}
|
||||
}
|
||||
|
||||
return tournament
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
private fun extractValue(tag: String, element: Element, default: String): String {
|
||||
return try {
|
||||
val nodes = element.getElementsByTagName(tag).item(0)?.childNodes
|
||||
nodes?.item(0)?.nodeValue ?: default
|
||||
} catch (e: Exception) {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractValues(tag: String, element: Element): List<String> {
|
||||
val result = mutableListOf<String>()
|
||||
try {
|
||||
val nodeList = element.getElementsByTagName(tag)
|
||||
for (i in 0 until minOf(nodeList.length, 20)) {
|
||||
val nodes = nodeList.item(i)?.childNodes
|
||||
nodes?.item(0)?.nodeValue?.let { result.add(it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseRank(rankStr: String): Int {
|
||||
val regex = Regex("(\\d+)([kKdD])")
|
||||
val match = regex.matchEntire(rankStr) ?: return -20
|
||||
val (num, letter) = match.destructured
|
||||
val level = num.toIntOrNull() ?: return -20
|
||||
return when (letter.lowercase()) {
|
||||
"k" -> -level
|
||||
"d" -> level - 1
|
||||
else -> -20
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseResult(resultStr: String, byRef: Boolean): Game.Result {
|
||||
// MM39 result format: "1-0" (white wins), "0-1" (black wins), etc.
|
||||
// The format uses black-first convention (first number is black's score)
|
||||
return when (resultStr.removeSuffix("!")) {
|
||||
"0-1" -> Game.Result.WHITE
|
||||
"1-0" -> Game.Result.BLACK
|
||||
"\u00BD-\u00BD" -> Game.Result.JIGO
|
||||
"0-0" -> Game.Result.BOTHLOOSE
|
||||
"1-1" -> Game.Result.BOTHWIN
|
||||
else -> Game.Result.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
private fun translateBreaker(breaker: String, swiss: Boolean): Criterion? {
|
||||
return when (breaker) {
|
||||
"Points" -> Criterion.NBW
|
||||
"Score", "ScoreX" -> Criterion.MMS
|
||||
"SOS" -> if (swiss) Criterion.SOSW else Criterion.SOSM
|
||||
"SOSOS" -> if (swiss) Criterion.SOSOSW else Criterion.SOSOSM
|
||||
"SODOS" -> if (swiss) Criterion.SODOSW else Criterion.SODOSM
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package org.jeudego.pairgoth.test
|
||||
|
||||
import org.jeudego.pairgoth.ext.MacMahon39
|
||||
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
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class ImportExportTests: TestBase() {
|
||||
|
||||
@@ -56,4 +58,73 @@ class ImportExportTests: TestBase() {
|
||||
assertEquals(jsonTournament, jsonTournament2)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `003 test macmahon39 import`() {
|
||||
getTestResources("macmahon39")?.forEach { file ->
|
||||
logger.info("===== Testing MacMahon 3.9 import: ${file.name} =====")
|
||||
val resource = file.readText(StandardCharsets.UTF_8)
|
||||
val root_xml = XmlUtils.parse(resource)
|
||||
|
||||
// Verify format detection
|
||||
assertTrue(MacMahon39.isFormat(root_xml), "File should be detected as MacMahon 3.9 format")
|
||||
|
||||
// Import tournament
|
||||
val tournament = MacMahon39.import(root_xml)
|
||||
|
||||
// Verify basic tournament data
|
||||
logger.info("Tournament name: ${tournament.name}")
|
||||
logger.info("Number of rounds: ${tournament.rounds}")
|
||||
logger.info("Number of players: ${tournament.pairables.size}")
|
||||
|
||||
assertEquals("Test MacMahon Tournament", tournament.name)
|
||||
assertEquals(3, tournament.rounds)
|
||||
assertEquals(4, tournament.pairables.size)
|
||||
|
||||
// Verify players
|
||||
val players = tournament.pairables.values.toList()
|
||||
val alice = players.find { it.name == "Smith" }
|
||||
val bob = players.find { it.name == "Jones" }
|
||||
val carol = players.find { it.name == "White" }
|
||||
val david = players.find { it.name == "Brown" }
|
||||
|
||||
assertTrue(alice != null, "Alice should exist")
|
||||
assertTrue(bob != null, "Bob should exist")
|
||||
assertTrue(carol != null, "Carol should exist")
|
||||
assertTrue(david != null, "David should exist")
|
||||
|
||||
assertEquals(2, alice!!.rank) // 3d = rank 2
|
||||
assertEquals(1, bob!!.rank) // 2d = rank 1
|
||||
assertEquals(0, carol!!.rank) // 1d = rank 0
|
||||
assertEquals(-1, david!!.rank) // 1k = rank -1
|
||||
|
||||
// Carol is super bar member
|
||||
assertEquals(1, carol.mmsCorrection)
|
||||
|
||||
// David skips round 2
|
||||
assertTrue(david.skip.contains(2), "David should skip round 2")
|
||||
|
||||
// Verify games
|
||||
val round1Games = tournament.games(1).values.toList()
|
||||
val round2Games = tournament.games(2).values.toList()
|
||||
|
||||
logger.info("Round 1 games: ${round1Games.size}")
|
||||
logger.info("Round 2 games: ${round2Games.size}")
|
||||
|
||||
assertEquals(2, round1Games.size)
|
||||
assertEquals(2, round2Games.size) // 1 regular game + 1 bye
|
||||
|
||||
// Test via API
|
||||
val resp = TestAPI.post("/api/tour", resource)
|
||||
val id = resp.asObject().getInt("id")
|
||||
logger.info("Imported tournament id: $id")
|
||||
|
||||
val apiTournament = TestAPI.get("/api/tour/$id").asObject()
|
||||
assertEquals("Test MacMahon Tournament", apiTournament.getString("name"))
|
||||
assertEquals(3, apiTournament.getInt("rounds"))
|
||||
|
||||
val apiPlayers = TestAPI.get("/api/tour/$id/part").asArray()
|
||||
assertEquals(4, apiPlayers.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
132
api-webapp/src/test/resources/macmahon39/sample-tournament.xml
Normal file
132
api-webapp/src/test/resources/macmahon39/sample-tournament.xml
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Sample MacMahon 3.9 format tournament file for testing -->
|
||||
<TournamentData typeversion="3.9">
|
||||
<Tournament typeversion="3.9">
|
||||
<Name>Test MacMahon Tournament</Name>
|
||||
<NumberOfRounds>3</NumberOfRounds>
|
||||
<UpperMacMahonBar>true</UpperMacMahonBar>
|
||||
<UpperMacMahonBarLevel>1d</UpperMacMahonBarLevel>
|
||||
<LowerMacMahonBar>true</LowerMacMahonBar>
|
||||
<LowerMacMahonBarLevel>20k</LowerMacMahonBarLevel>
|
||||
<RatingDeterminesRank>false</RatingDeterminesRank>
|
||||
<HandicapUsed>false</HandicapUsed>
|
||||
<HandicapBelow>true</HandicapBelow>
|
||||
<HandicapBelowLevel>30k</HandicapBelowLevel>
|
||||
<HandicapAdjustment>true</HandicapAdjustment>
|
||||
<HandicapAdjustmentValue>0</HandicapAdjustmentValue>
|
||||
<HandicapLimit>true</HandicapLimit>
|
||||
<HandicapLimitValue>9</HandicapLimitValue>
|
||||
<HandicapByLevel>false</HandicapByLevel>
|
||||
</Tournament>
|
||||
|
||||
<Walllist>
|
||||
<Criterion><ShortName>Score</ShortName></Criterion>
|
||||
<Criterion><ShortName>SOS</ShortName></Criterion>
|
||||
<Criterion><ShortName>SOSOS</ShortName></Criterion>
|
||||
</Walllist>
|
||||
|
||||
<Playerlist>
|
||||
<Player>
|
||||
<Id>1</Id>
|
||||
<PreliminaryRegistration>false</PreliminaryRegistration>
|
||||
<SuperBarMember>false</SuperBarMember>
|
||||
<GoPlayer>
|
||||
<FirstName>Alice</FirstName>
|
||||
<Surname>Smith</Surname>
|
||||
<Club>Paris</Club>
|
||||
<Country>FR</Country>
|
||||
<GoLevel>3d</GoLevel>
|
||||
<Rating>2200</Rating>
|
||||
<EgdPin>12345678</EgdPin>
|
||||
</GoPlayer>
|
||||
</Player>
|
||||
<Player>
|
||||
<Id>2</Id>
|
||||
<PreliminaryRegistration>false</PreliminaryRegistration>
|
||||
<SuperBarMember>false</SuperBarMember>
|
||||
<GoPlayer>
|
||||
<FirstName>Bob</FirstName>
|
||||
<Surname>Jones</Surname>
|
||||
<Club>Lyon</Club>
|
||||
<Country>FR</Country>
|
||||
<GoLevel>2d</GoLevel>
|
||||
<Rating>2100</Rating>
|
||||
<EgdPin>23456789</EgdPin>
|
||||
</GoPlayer>
|
||||
</Player>
|
||||
<Player>
|
||||
<Id>3</Id>
|
||||
<PreliminaryRegistration>false</PreliminaryRegistration>
|
||||
<SuperBarMember>true</SuperBarMember>
|
||||
<GoPlayer>
|
||||
<FirstName>Carol</FirstName>
|
||||
<Surname>White</Surname>
|
||||
<Club>Berlin</Club>
|
||||
<Country>DE</Country>
|
||||
<GoLevel>1d</GoLevel>
|
||||
<Rating>2000</Rating>
|
||||
<EgdPin>34567890</EgdPin>
|
||||
</GoPlayer>
|
||||
</Player>
|
||||
<Player>
|
||||
<Id>4</Id>
|
||||
<PreliminaryRegistration>true</PreliminaryRegistration>
|
||||
<SuperBarMember>false</SuperBarMember>
|
||||
<NotPlayingInRound>2</NotPlayingInRound>
|
||||
<GoPlayer>
|
||||
<FirstName>David</FirstName>
|
||||
<Surname>Brown</Surname>
|
||||
<Club>London</Club>
|
||||
<Country>UK</Country>
|
||||
<GoLevel>1k</GoLevel>
|
||||
<Rating>1900</Rating>
|
||||
<EgdPin>45678901</EgdPin>
|
||||
</GoPlayer>
|
||||
</Player>
|
||||
</Playerlist>
|
||||
|
||||
<Roundlist>
|
||||
<Round>
|
||||
<RoundNumber>1</RoundNumber>
|
||||
<Pairing>
|
||||
<BoardNumber>1</BoardNumber>
|
||||
<PairingWithBye>false</PairingWithBye>
|
||||
<White>1</White>
|
||||
<Black>2</Black>
|
||||
<Handicap>0</Handicap>
|
||||
<Result>1-0</Result>
|
||||
<ResultByReferee>false</ResultByReferee>
|
||||
</Pairing>
|
||||
<Pairing>
|
||||
<BoardNumber>2</BoardNumber>
|
||||
<PairingWithBye>false</PairingWithBye>
|
||||
<White>3</White>
|
||||
<Black>4</Black>
|
||||
<Handicap>0</Handicap>
|
||||
<Result>0-1</Result>
|
||||
<ResultByReferee>false</ResultByReferee>
|
||||
</Pairing>
|
||||
</Round>
|
||||
<Round>
|
||||
<RoundNumber>2</RoundNumber>
|
||||
<Pairing>
|
||||
<BoardNumber>1</BoardNumber>
|
||||
<PairingWithBye>false</PairingWithBye>
|
||||
<White>2</White>
|
||||
<Black>1</Black>
|
||||
<Handicap>0</Handicap>
|
||||
<Result>0-1</Result>
|
||||
<ResultByReferee>false</ResultByReferee>
|
||||
</Pairing>
|
||||
<Pairing>
|
||||
<BoardNumber>0</BoardNumber>
|
||||
<PairingWithBye>true</PairingWithBye>
|
||||
<White>0</White>
|
||||
<Black>3</Black>
|
||||
<Handicap>0</Handicap>
|
||||
<Result>0-1</Result>
|
||||
<ResultByReferee>false</ResultByReferee>
|
||||
</Pairing>
|
||||
</Round>
|
||||
</Roundlist>
|
||||
</TournamentData>
|
||||
Reference in New Issue
Block a user