stream swiss trf for pairing and export

This commit is contained in:
Thibault Duplessis 2020-05-12 18:37:53 -06:00
parent 3fe4ac95cb
commit ceb186621f
6 changed files with 147 additions and 88 deletions

View file

@ -214,9 +214,11 @@ final class Swiss(
def exportTrf(id: String) =
Action.async {
env.swiss.api.byId(SwissId(id)) flatMap {
case None => NotFound("Tournament not found").fuccess
case Some(swiss) => env.swiss.trf(swiss) dmap { Ok(_) }
env.swiss.api.byId(SwissId(id)) map {
case None => NotFound("Tournament not found")
case Some(swiss) =>
Ok.chunked(env.swiss trf swiss intersperse "\n")
.withHeaders(CONTENT_DISPOSITION -> s"attachment; filename=lichess_swiss_$id.trf")
}
}

View file

@ -32,6 +32,8 @@ final class Env(
private val colls = wire[SwissColls]
private val sheetApi = wire[SwissSheetApi]
val trf: SwissTrf = wire[SwissTrf]
private val pairingSystem = new PairingSystem(trf, appConfig.get[String]("swiss.bbpairing"))

View file

@ -1,18 +1,21 @@
package lila.swiss
import akka.stream.scaladsl._
import java.io.{ File, PrintWriter }
import scala.util.chaining._
import scala.sys.process._
import scala.concurrent.blocking
import scala.sys.process._
final private class PairingSystem(trf: SwissTrf, executable: String) {
final private class PairingSystem(trf: SwissTrf, executable: String)(implicit
ec: scala.concurrent.ExecutionContext,
mat: akka.stream.Materializer
) {
def apply(
swiss: Swiss,
players: List[SwissPlayer],
pairings: List[SwissPairing]
): List[SwissPairing.ByeOrPending] =
trf(swiss, players, pairings) pipe invoke pipe reader
def apply(swiss: Swiss): Fu[List[SwissPairing.ByeOrPending]] =
trf(swiss)
.toMat(Sink.fold("") {
case (a, l) => s"$l\n$a"
})(Keep.right)
.run map invoke map reader
private def invoke(input: String): List[String] =
lila.mon.chronoSync(_.swiss.bbpairing) {

View file

@ -9,7 +9,6 @@ import lila.game.Game
final private class SwissDirector(
colls: SwissColls,
trf: SwissTrf,
pairingSystem: PairingSystem,
gameRepo: lila.game.GameRepo,
onStart: Game.ID => Unit
@ -21,52 +20,55 @@ final private class SwissDirector(
// sequenced by SwissApi
private[swiss] def startRound(from: Swiss): Fu[Option[(Swiss, List[SwissPairing])]] =
trf
.fetchData(from)
.flatMap {
case (players, prevPairings) =>
val pendings = pairingSystem(from, players, prevPairings)
if (pendings.isEmpty) fuccess(none) // terminate
else {
val swiss = from.startRound
for {
pairings <- pendings.collect {
case Right(SwissPairing.Pending(w, b)) =>
idGenerator.game dmap { id =>
SwissPairing(
id = id,
swissId = swiss.id,
round = swiss.round,
white = w,
black = b,
status = Left(SwissPairing.Ongoing)
)
}
}.sequenceFu
_ <-
colls.swiss.update
.one(
$id(swiss.id),
$unset("nextRoundAt") ++ $set(
"round" -> swiss.round,
"nbOngoing" -> pairings.size
)
pairingSystem(from)
.flatMap { pendings =>
if (pendings.isEmpty) fuccess(none) // terminate
else {
val swiss = from.startRound
for {
players <- SwissPlayer.fields { f =>
colls.player.ext
.find($doc(f.swissId -> swiss.id))
.sort($sort asc f.number)
.list[SwissPlayer]()
}
pairings <- pendings.collect {
case Right(SwissPairing.Pending(w, b)) =>
idGenerator.game dmap { id =>
SwissPairing(
id = id,
swissId = swiss.id,
round = swiss.round,
white = w,
black = b,
status = Left(SwissPairing.Ongoing)
)
.void
date = DateTime.now
byes = pendings.collect { case Left(bye) => bye.player }
_ <- SwissPlayer.fields { f =>
colls.player.update
.one($doc(f.number $in byes, f.swissId -> swiss.id), $addToSet(f.byes -> swiss.round))
.void
}
_ <- colls.pairing.insert.many(pairings).void
games = pairings.map(makeGame(swiss, SwissPlayer.toMap(players)))
_ <- lila.common.Future.applySequentially(games) { game =>
gameRepo.insertDenormalized(game) >>- onStart(game.id)
}
} yield Some(swiss -> pairings)
}
}
}.sequenceFu
_ <-
colls.swiss.update
.one(
$id(swiss.id),
$unset("nextRoundAt") ++ $set(
"round" -> swiss.round,
"nbOngoing" -> pairings.size
)
)
.void
date = DateTime.now
byes = pendings.collect { case Left(bye) => bye.player }
_ <- SwissPlayer.fields { f =>
colls.player.update
.one($doc(f.number $in byes, f.swissId -> swiss.id), $addToSet(f.byes -> swiss.round))
.void
}
_ <- colls.pairing.insert.many(pairings).void
games = pairings.map(makeGame(swiss, SwissPlayer.toMap(players)))
_ <- lila.common.Future.applySequentially(games) { game =>
gameRepo.insertDenormalized(game) >>- onStart(game.id)
}
} yield Some(swiss -> pairings)
}
}
.recover {
case PairingSystem.BBPairingException(msg, input) =>

View file

@ -57,4 +57,63 @@ private object SwissSheet {
}
}
}
}
final private class SwissSheetApi(colls: SwissColls)(implicit
mat: akka.stream.Materializer
) {
import akka.stream.scaladsl._
import org.joda.time.DateTime
import reactivemongo.akkastream.cursorProducer
import reactivemongo.api.ReadPreference
import lila.db.dsl._
import BsonHandlers._
def source(swiss: Swiss): Source[(SwissPlayer, Map[SwissRound.Number, SwissPairing], SwissSheet), _] =
SwissPlayer.fields { f =>
val readPreference =
if (swiss.finishedAt.exists(_ isBefore DateTime.now.minusSeconds(10)))
ReadPreference.secondaryPreferred
else ReadPreference.primary
colls.player
.aggregateWith[Bdoc](readPreference = readPreference) { implicit framework =>
import framework._
Match($doc(f.swissId -> swiss.id)) -> List(
PipelineOperator(
$doc(
"$lookup" -> $doc(
"from" -> colls.pairing.name,
"let" -> $doc("n" -> "$n"),
"pipeline" -> $arr(
$doc(
"$match" -> $doc(
"$expr" -> $doc(
"$and" -> $arr(
$doc("$eq" -> $arr("$s", swiss.id)),
$doc("$in" -> $arr("$$n", "$p"))
)
)
)
)
),
"as" -> "pairings"
)
)
)
)
}
.documentSource()
.mapConcat { doc =>
val result = for {
player <- playerHandler.readOpt(doc)
pairings <- doc.getAsOpt[List[SwissPairing]]("pairings")
pairingMap = pairings.map { p =>
p.round -> p
}.toMap
} yield (player, pairingMap, SwissSheet.one(swiss, pairingMap, player))
result.toList
}
}
}

View file

@ -1,49 +1,41 @@
package lila.swiss
import akka.stream.scaladsl._
import lila.db.dsl._
final class SwissTrf(
colls: SwissColls
)(implicit ec: scala.concurrent.ExecutionContext) {
colls: SwissColls,
sheetApi: SwissSheetApi
)(implicit ec: scala.concurrent.ExecutionContext, mat: akka.stream.Materializer) {
import BsonHandlers._
private type Bits = List[(Int, String)]
def apply(swiss: Swiss): Fu[String] =
fetchData(swiss) map {
case (players, pairings) => apply(swiss, players, pairings)
}
def apply(swiss: Swiss): Source[String, _] =
tournamentLines(swiss) concat sheetApi
.source(swiss)
.map((playerLine(swiss) _).tupled)
.map(formatLine)
def apply(swiss: Swiss, players: List[SwissPlayer], pairings: List[SwissPairing]): String = {
s"XXR ${swiss.settings.nbRounds}" ::
s"XXC ${chess.Color(scala.util.Random.nextBoolean).name}1" ::
players.map(player(swiss, SwissPairing.toMap(pairings))).map(format)
} mkString "\n"
def fetchData(swiss: Swiss): Fu[(List[SwissPlayer], List[SwissPairing])] =
SwissPlayer.fields { f =>
colls.player.ext
.find($doc(f.swissId -> swiss.id))
.sort($sort asc f.number)
.list[SwissPlayer]()
} zip
SwissPairing.fields { f =>
colls.pairing.ext
.find($doc(f.swissId -> swiss.id))
.sort($sort asc f.round)
.list[SwissPairing]()
}
private def tournamentLines(swiss: Swiss) =
Source(
List(
s"XXR ${swiss.settings.nbRounds}",
s"XXC ${chess.Color(scala.util.Random.nextBoolean).name}1"
)
)
// https://www.fide.com/FIDE/handbook/C04Annex2_TRF16.pdf
private def player(swiss: Swiss, pairingMap: SwissPairing.PairingMap)(p: SwissPlayer): Bits = {
val sheet = SwissSheet.one(swiss, ~pairingMap.get(p.number), p)
private def playerLine(
swiss: Swiss
)(p: SwissPlayer, pairings: Map[SwissRound.Number, SwissPairing], sheet: SwissSheet): Bits =
List(
3 -> "001",
8 -> p.number.toString,
47 -> p.userId,
84 -> f"${sheet.points.value}%1.1f"
) ::: {
val pairings = ~pairingMap.get(p.number)
swiss.allRounds.zip(sheet.outcomes).flatMap {
case (rn, outcome) =>
val pairing = pairings get rn
@ -71,9 +63,8 @@ final class SwissTrf(
99 -> "-"
).map { case (l, s) => (l + swiss.round.value * 10, s) }
}
}
private def format(bits: Bits): String =
private def formatLine(bits: Bits): String =
bits.foldLeft("") {
case (acc, (pos, txt)) => acc + (" " * (pos - txt.size - acc.size)) + txt
}