stream swiss trf for pairing and export
This commit is contained in:
parent
3fe4ac95cb
commit
ceb186621f
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue