Tournament systems.
- Introduced the concept of tournament systems: a system is a combination of pairing and a scoring system. - Tournaments now store "events" in addition to the pairings. Events are an extensible meta-information mechanism. - Factored out current hardcoded tournament logic into the "Arena" system. Arena is the default system. - Added a "Swiss" system for FIDE-like tournaments. Pairing logic is based on the FIDE-sanctionned Burstein system. - The Swiss system pairs players as soon as all games in the previous round are completed. Players get paired regardless of whether they are present in the lobby.pull/91/head
parent
7638923d1c
commit
f09a2e9893
|
@ -4,3 +4,6 @@
|
|||
[submodule "public/vendor/tagmanager"]
|
||||
path = public/vendor/tagmanager
|
||||
url = https://github.com/max-favilli/tagmanager
|
||||
[submodule "modules/swisssystem"]
|
||||
path = modules/swisssystem
|
||||
url = https://github.com/psuter/swisssystem
|
||||
|
|
|
@ -4,6 +4,7 @@ package templating
|
|||
import chess.{ Mode, Variant, Speed }
|
||||
import lila.setup._
|
||||
import lila.api.Context
|
||||
import lila.tournament.System
|
||||
|
||||
trait SetupHelper { self: I18nHelper =>
|
||||
|
||||
|
@ -12,6 +13,11 @@ trait SetupHelper { self: I18nHelper =>
|
|||
Mode.Rated.id.toString -> trans.rated.str()
|
||||
)
|
||||
|
||||
def translatedSystemChoices(implicit ctx: Context) = List(
|
||||
System.Arena.id.toString -> trans.arena.str(),
|
||||
System.Swiss.id.toString -> trans.swiss.str()
|
||||
)
|
||||
|
||||
def translatedVariantChoices(implicit ctx: Context) = List(
|
||||
Variant.Standard.id.toString -> trans.standard.str(),
|
||||
Variant.Chess960.id.toString -> Variant.Chess960.name.capitalize
|
||||
|
|
|
@ -3,8 +3,8 @@ package templating
|
|||
|
||||
import controllers.routes
|
||||
import lila.api.Context
|
||||
import lila.tournament.Tournament
|
||||
import lila.user.User
|
||||
import lila.tournament.{ Tournament, System }
|
||||
import lila.user.{ User, UserContext }
|
||||
|
||||
import play.api.libs.json.Json
|
||||
import play.twirl.api.Html
|
||||
|
@ -28,4 +28,9 @@ trait TournamentHelper { self: I18nHelper =>
|
|||
val name = if (tour.scheduled) tour.name else trans.xTournament(tour.name)
|
||||
s"""<a data-icon="g" class="$cssClass" href="$url"> $name</a>"""
|
||||
}
|
||||
|
||||
def systemName(sys: System)(implicit ctx: UserContext) = sys match {
|
||||
case System.Arena => trans.arena.str()
|
||||
case System.Swiss => trans.swiss.str()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@(pov: Pov, tour: Option[lila.tournament.Tournament], withTourStanding: Boolean)(implicit ctx: Context)
|
||||
|
||||
@import pov._
|
||||
@import lila.tournament.arena
|
||||
|
||||
<div class="goodies">
|
||||
<div class="box">
|
||||
|
@ -69,10 +70,13 @@
|
|||
@t.rankedPlayers.map {
|
||||
case (rank, player) => {
|
||||
@defining((
|
||||
if(t.isFinished && rank == 1) "winner" else if (player.withdraw) "withdraw" else "",
|
||||
t scoreSheet player
|
||||
)) {
|
||||
case (flag, scoreSheet) => {
|
||||
t scoreSheet player,
|
||||
// TODO FIXME that's a little ugly I must say.
|
||||
t.system.scoringSystem match {
|
||||
case ss @ arena.ScoringSystem => ss.scoreSheet(t, player.id).onFire
|
||||
case _ => false
|
||||
})) {
|
||||
case (scoreSheet, onFire) => {
|
||||
<tr @if(ctx.userId.exists(player.id==)) { class="me" }>
|
||||
<td class="name">
|
||||
@if(player.withdraw) {
|
||||
|
@ -87,7 +91,7 @@
|
|||
@userInfosLink(player.id, none, withOnline = false)
|
||||
</td>
|
||||
<td class="total">
|
||||
<strong@if(scoreSheet.onFire) { class="is-gold" data-icon="Q" }>@scoreSheet.total</strong>
|
||||
<strong@if(onFire) { class="is-gold" data-icon="Q" }>@scoreSheet.total</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td class="around-bar" colspan="3"><div class="bar" data-value="@scoreSheet.total"></div></td></tr>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@(tour: lila.tournament.StartedOrFinished)(implicit ctx: Context)
|
||||
|
||||
@import lila.tournament.arena.ScoringSystem
|
||||
|
||||
<div class="standing_wrap scroll-shadow-soft">
|
||||
<table class="slist standing @if(tour.scheduled) { scheduled }">
|
||||
<thead>
|
||||
|
@ -17,7 +19,7 @@
|
|||
case (rank, player) => {
|
||||
@defining((
|
||||
if(tour.isFinished && rank == 1) "winner" else if (player.withdraw) "withdraw" else "",
|
||||
tour scoreSheet player
|
||||
ScoringSystem.scoreSheet(tour, player.id)
|
||||
)) {
|
||||
case (flag, scoreSheet) => {
|
||||
<tr @if(ctx.userId.exists(player.id==)) { class="me" }>
|
|
@ -31,6 +31,10 @@ moreJs = moreJs) {
|
|||
<p class="error">@error.message</p>
|
||||
}
|
||||
<table>
|
||||
<tr>
|
||||
<th><label for="@form("system").id">@trans.system()</label></th>
|
||||
<td>@base.select(form("system"), translatedSystemChoices)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="@form("variant").id">@trans.variant()</label></th>
|
||||
<td>@base.select(form("variant"), translatedVariantChoices)</td>
|
||||
|
|
|
@ -24,6 +24,8 @@
|
|||
@variantName(tour.variant).capitalize,
|
||||
@{ tour.rated.fold(trans.rated(), trans.casual()) }
|
||||
<br /><br />
|
||||
@trans.system(): @systemName(tour.system).capitalize
|
||||
<br /><br />
|
||||
@trans.duration(): @tour.minutes minutes
|
||||
@if(tour.isRunning) {
|
||||
<br /><br />
|
||||
|
|
|
@ -4,6 +4,14 @@
|
|||
|
||||
<h1 data-icon="g"> @if(tour.scheduled){@tour.name} else {@trans.xTournament(tour.name)}</h1>
|
||||
|
||||
@tournament.standing(tour)
|
||||
@tour.system match {
|
||||
case lila.tournament.System.Arena => {
|
||||
@tournament.arenaStanding(tour)
|
||||
}
|
||||
|
||||
case lila.tournament.System.Swiss => {
|
||||
@tournament.swissStanding(tour)
|
||||
}
|
||||
}
|
||||
|
||||
@tournament.games(games)
|
||||
|
|
|
@ -13,6 +13,13 @@
|
|||
</a>
|
||||
}
|
||||
|
||||
@tournament.standing(tour)
|
||||
@tour.system match {
|
||||
case lila.tournament.System.Arena => {
|
||||
@tournament.arenaStanding(tour)
|
||||
}
|
||||
case lila.tournament.System.Swiss => {
|
||||
@tournament.swissStanding(tour)
|
||||
}
|
||||
}
|
||||
|
||||
@tournament.games(games)
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
@(tour: lila.tournament.StartedOrFinished)(implicit ctx: Context)
|
||||
|
||||
@import lila.tournament.swiss.SwissSystem
|
||||
|
||||
<div class="standing_wrap scroll-shadow-soft">
|
||||
<table class="slist standing @if(tour.scheduled) { scheduled }">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="large">@trans.standing() (@tour.nbPlayers)</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@defining(SwissSystem.scoreSheets(tour)) {
|
||||
case scoreSheets => {
|
||||
@tour.rankedPlayers.map {
|
||||
case (rank, player) => {
|
||||
@defining(scoreSheets(player.id)) {
|
||||
case scoreSheet => {
|
||||
<tr @if(ctx.userId.exists(player.id==)) { class="me" }>
|
||||
<td class="name">
|
||||
@if(player.withdraw) {
|
||||
<span data-icon="b" title="@trans.withdraw()"></span>
|
||||
} else {
|
||||
@if(tour.isFinished && rank == 1) {
|
||||
<span data-icon="g" title="@trans.winner()"></span>
|
||||
} else {
|
||||
<span class="rank">@rank</span>
|
||||
}
|
||||
}
|
||||
@userInfosLink(player.id, none, withOnline = false)
|
||||
</td>
|
||||
<td class="sheet">
|
||||
@scoreSheet.scores.reverse.map { score =>
|
||||
<span class="normal">@score.repr</span>
|
||||
}
|
||||
</td>
|
||||
<td class="total">
|
||||
<strong>@scoreSheet.totalRepr</strong> (@scoreSheet.neustadtlRepr)
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td class="around-bar" colspan="3"><div class="bar" data-value="@scoreSheet.total"></div></td></tr>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -111,6 +111,9 @@ unlimited=Unlimited
|
|||
mode=Mode
|
||||
casual=Casual
|
||||
rated=Rated
|
||||
system=System
|
||||
arena=Arena
|
||||
swiss=Swiss
|
||||
thisGameIsRated=This game is rated
|
||||
rematch=Rematch
|
||||
rematchOfferSent=Rematch offer sent
|
||||
|
|
|
@ -135,6 +135,9 @@ final class I18nKeys(translator: Translator) {
|
|||
val `mode` = new Key("mode")
|
||||
val `casual` = new Key("casual")
|
||||
val `rated` = new Key("rated")
|
||||
val `system` = new Key("system")
|
||||
val `arena` = new Key("arena")
|
||||
val `swiss` = new Key("swiss")
|
||||
val `thisGameIsRated` = new Key("thisGameIsRated")
|
||||
val `rematch` = new Key("rematch")
|
||||
val `rematchOfferSent` = new Key("rematchOfferSent")
|
||||
|
|
|
@ -5,6 +5,7 @@ import chess.{ Game => ChessGame, Board, Situation, Variant, Clock, Speed }
|
|||
|
||||
import lila.game.{ GameRepo, Game, Pov }
|
||||
import lila.lobby.Color
|
||||
import lila.tournament.{ System => TournamentSystem }
|
||||
|
||||
private[setup] trait Config {
|
||||
|
||||
|
@ -81,6 +82,8 @@ trait Positional { self: Config =>
|
|||
object Config extends BaseConfig
|
||||
|
||||
trait BaseConfig {
|
||||
val systems = List(TournamentSystem.Arena.id, TournamentSystem.Swiss.id)
|
||||
val systemDefault = TournamentSystem.default
|
||||
|
||||
val variants = List(Variant.Standard.id, Variant.Chess960.id)
|
||||
val variantDefault = Variant.Standard
|
||||
|
|
|
@ -22,4 +22,5 @@ object Mappings {
|
|||
val level = number.verifying(AiConfig.levels contains _)
|
||||
val speed = number.verifying(Config.speeds contains _)
|
||||
val fen = optional(nonEmptyText)
|
||||
val system = number.verifying(Config.systems contains _)
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit a4168747954cb70f4bb34a45af0b72c5ccad9a9c
|
|
@ -35,6 +35,7 @@ final class DataForm(isDev: Boolean) {
|
|||
"clockIncrement" -> numberIn(clockIncrementChoices),
|
||||
"minutes" -> numberIn(minuteChoices),
|
||||
"minPlayers" -> numberIn(minPlayerChoices),
|
||||
"system" -> number.verifying(Set(System.Arena.id, System.Swiss.id) contains _),
|
||||
"variant" -> number.verifying(Set(Variant.Standard.id, Variant.Chess960.id) contains _),
|
||||
"mode" -> number.verifying(Mode.all map (_.id) contains _),
|
||||
"password" -> optional(nonEmptyText)
|
||||
|
@ -46,6 +47,7 @@ final class DataForm(isDev: Boolean) {
|
|||
clockIncrement = clockIncrementDefault,
|
||||
minutes = minuteDefault,
|
||||
minPlayers = minPlayerDefault,
|
||||
system = System.default.id,
|
||||
variant = Variant.Standard.id,
|
||||
password = none,
|
||||
mode = Mode.Casual.id)
|
||||
|
@ -60,6 +62,7 @@ private[tournament] case class TournamentSetup(
|
|||
clockIncrement: Int,
|
||||
minutes: Int,
|
||||
minPlayers: Int,
|
||||
system: Int,
|
||||
variant: Int,
|
||||
mode: Int,
|
||||
password: Option[String]) {
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package lila.tournament
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
// Metadata about running tournaments: who got byed, when a round completed, this sort of things.
|
||||
sealed abstract class Event(val id: Int) {
|
||||
def timestamp: DateTime
|
||||
def encode: RawEvent
|
||||
}
|
||||
|
||||
case class RoundEnd(timestamp: DateTime) extends Event(1) {
|
||||
def encode = RawEvent(id, timestamp, None)
|
||||
}
|
||||
|
||||
case class Bye(user: String, timestamp: DateTime) extends Event(10) {
|
||||
def encode = RawEvent(id, timestamp, Some(user))
|
||||
}
|
||||
|
||||
private[tournament] case class RawEvent(
|
||||
i: Int,
|
||||
t: DateTime,
|
||||
u: Option[String]) {
|
||||
|
||||
def decode: Option[Event] = roundEnd orElse bye
|
||||
|
||||
def roundEnd: Option[RoundEnd] = (i == 1) option RoundEnd(t)
|
||||
|
||||
def bye: Option[Bye] = for {
|
||||
usr <- u
|
||||
if i == 10
|
||||
} yield Bye(usr, t)
|
||||
}
|
||||
|
||||
private[tournament] object RawEvent {
|
||||
import lila.db.JsTube
|
||||
import JsTube.Helpers._
|
||||
import play.api.libs.json._
|
||||
|
||||
private def defaults = Json.obj(
|
||||
"u" -> none[String]
|
||||
)
|
||||
|
||||
private[tournament] val tube = JsTube(
|
||||
(__.json update merge(defaults)) andThen Json.reads[RawEvent],
|
||||
Json.writes[RawEvent]
|
||||
)
|
||||
}
|
|
@ -60,8 +60,12 @@ private[tournament] final class Organizer(
|
|||
if (!tour.isAlmostFinished) {
|
||||
withUserIds(tour.id) { ids =>
|
||||
(tour.activeUserIds intersect ids) |> { users =>
|
||||
Pairing.createNewPairings(users, tour.pairings, tour.nbActiveUsers).toNel foreach { pairings =>
|
||||
api.makePairings(tour, pairings)
|
||||
|
||||
tour.system.pairingSystem.createPairings(tour, users) onSuccess {
|
||||
case (pairings, events) =>
|
||||
pairings.toNel foreach { pairings =>
|
||||
api.makePairings(tour, pairings, events)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
package lila.tournament
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
import chess.Color
|
||||
import lila.game.{ PovRef, IdGenerator }
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
case class Pairing(
|
||||
gameId: String,
|
||||
status: chess.Status,
|
||||
user1: String,
|
||||
user2: String,
|
||||
winner: Option[String],
|
||||
turns: Option[Int]) {
|
||||
turns: Option[Int],
|
||||
pairedAt: Option[DateTime]) {
|
||||
|
||||
def encode: RawPairing = RawPairing(gameId, status.id, users, winner, turns)
|
||||
def encode: RawPairing = RawPairing(gameId, status.id, users, winner, turns, pairedAt)
|
||||
|
||||
def users = List(user1, user2)
|
||||
def usersPair = user1 -> user2
|
||||
|
@ -51,90 +52,29 @@ case class Pairing(
|
|||
}
|
||||
|
||||
private[tournament] object Pairing {
|
||||
|
||||
type P = (String, String)
|
||||
|
||||
def apply(users: P): Pairing = apply(users._1, users._2)
|
||||
def apply(user1: String, user2: String): Pairing = new Pairing(
|
||||
def apply(us: P): Pairing = apply(us._1, us._2)
|
||||
def apply(u1: String, u2: String): Pairing = apply(u1, u2, None)
|
||||
def apply(u1: String, u2: String, pa: DateTime): Pairing = apply(u1, u2, Some(pa))
|
||||
|
||||
def apply(u1: String, u2: String, pa: Option[DateTime]): Pairing = new Pairing(
|
||||
gameId = IdGenerator.game,
|
||||
status = chess.Status.Created,
|
||||
user1 = user1,
|
||||
user2 = user2,
|
||||
user1 = u1,
|
||||
user2 = u2,
|
||||
winner = none,
|
||||
turns = none)
|
||||
|
||||
def createNewPairings(users: List[String], pairings: Pairings, nbActiveUsers: Int): Pairings =
|
||||
|
||||
if (users.size < 2)
|
||||
Nil
|
||||
else {
|
||||
val idles: List[String] = Random shuffle {
|
||||
users.toSet diff { (pairings filter (_.playing) flatMap (_.users)).toSet } toList
|
||||
}
|
||||
|
||||
pairings.isEmpty.fold(
|
||||
naivePairings(idles),
|
||||
(idles.size > 12).fold(
|
||||
naivePairings(idles),
|
||||
smartPairings(idles, pairings, nbActiveUsers)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private def naivePairings(users: List[String]) =
|
||||
Random shuffle users grouped 2 collect {
|
||||
case List(u1, u2) => Pairing(u1, u2)
|
||||
} toList
|
||||
|
||||
private def smartPairings(users: List[String], pairings: Pairings, nbActiveUsers: Int): Pairings = {
|
||||
|
||||
def lastOpponent(user: String): Option[String] =
|
||||
pairings find (_ contains user) flatMap (_ opponentOf user)
|
||||
|
||||
def justPlayedTogether(u1: String, u2: String): Boolean =
|
||||
lastOpponent(u1) == u2.some && lastOpponent(u2) == u1.some
|
||||
|
||||
def timeSincePlay(u: String): Int =
|
||||
pairings.takeWhile(_ notContains u).size
|
||||
|
||||
// lower is better
|
||||
def score(pair: P): Int = pair match {
|
||||
case (a, b) => justPlayedTogether(a, b).fold(
|
||||
100,
|
||||
-timeSincePlay(a) - timeSincePlay(b))
|
||||
}
|
||||
|
||||
(users match {
|
||||
case x if x.size < 2 => Nil
|
||||
case List(u1, u2) if nbActiveUsers == 2 => List(u1 -> u2)
|
||||
case List(u1, u2) if justPlayedTogether(u1, u2) => Nil
|
||||
case List(u1, u2) => List(u1 -> u2)
|
||||
case us => allPairCombinations(us)
|
||||
.map(c => c -> c.map(score).sum)
|
||||
.sortBy(_._2)
|
||||
.headOption
|
||||
.map(_._1) | Nil
|
||||
}) map Pairing.apply
|
||||
}
|
||||
|
||||
def allPairCombinations(list: List[String]): List[List[(String, String)]] = list match {
|
||||
case a :: rest => for {
|
||||
b ← rest
|
||||
init = (a -> b)
|
||||
nps = allPairCombinations(rest filter (b !=))
|
||||
ps ← nps.isEmpty.fold(List(List(init)), nps map (np => init :: np))
|
||||
} yield ps
|
||||
case _ => Nil
|
||||
}
|
||||
turns = none,
|
||||
pairedAt = pa)
|
||||
}
|
||||
|
||||
private[tournament] case class RawPairing(g: String, s: Int, u: List[String], w: Option[String], t: Option[Int]) {
|
||||
private[tournament] case class RawPairing(g: String, s: Int, u: List[String], w: Option[String], t: Option[Int], p: Option[DateTime]) {
|
||||
|
||||
def decode: Option[Pairing] = for {
|
||||
status ← chess.Status(s)
|
||||
user1 ← u.lift(0)
|
||||
user2 ← u.lift(1)
|
||||
} yield Pairing(g, status, user1, user2, w, t)
|
||||
} yield Pairing(g, status, user1, user2, w, t, p)
|
||||
}
|
||||
|
||||
private[tournament] object RawPairing {
|
||||
|
@ -145,7 +85,8 @@ private[tournament] object RawPairing {
|
|||
|
||||
private def defaults = Json.obj(
|
||||
"w" -> none[String],
|
||||
"t" -> none[Int])
|
||||
"t" -> none[Int],
|
||||
"p" -> none[DateTime])
|
||||
|
||||
private[tournament] val tube = JsTube(
|
||||
(__.json update merge(defaults)) andThen Json.reads[RawPairing],
|
||||
|
|
|
@ -25,7 +25,7 @@ private[tournament] object Player {
|
|||
rating = user.rating)
|
||||
|
||||
private[tournament] def refresh(tour: Tournament): Players = tour.players map { p =>
|
||||
p.copy(score = Score.sheet(p.id, tour).total)
|
||||
p.copy(score = tour.system.scoringSystem.scoreSheet(tour, p.id).total)
|
||||
} sortBy { p =>
|
||||
p.withdraw.fold(Int.MaxValue, 0) - p.score
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
package lila.tournament
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
sealed abstract class System(val id: Int) {
|
||||
val pairingSystem: PairingSystem
|
||||
val scoringSystem: ScoringSystem
|
||||
}
|
||||
|
||||
object System {
|
||||
case object Arena extends System(id = 1) {
|
||||
val pairingSystem = arena.PairingSystem
|
||||
val scoringSystem = arena.ScoringSystem
|
||||
}
|
||||
|
||||
case object Swiss extends System(id = 2) {
|
||||
val pairingSystem = swiss.SwissSystem
|
||||
val scoringSystem = swiss.SwissSystem
|
||||
}
|
||||
|
||||
val default = Arena
|
||||
|
||||
val all = List(Arena, Swiss)
|
||||
|
||||
val byId = all map { s => (s.id -> s) } toMap
|
||||
|
||||
def apply(id: Int): Option[System] = byId get id
|
||||
def orDefault(id: Int): System = apply(id) getOrElse default
|
||||
}
|
||||
|
||||
trait PairingSystem {
|
||||
def createPairings(tournament: Tournament, users: List[String]): Future[(Pairings,Events)]
|
||||
}
|
||||
|
||||
trait Score {
|
||||
val value: Int
|
||||
}
|
||||
|
||||
trait ScoreSheet {
|
||||
def scores: List[Score]
|
||||
def total: Int
|
||||
}
|
||||
|
||||
trait ScoringSystem {
|
||||
type Sheet <: ScoreSheet
|
||||
type RankedPlayers = List[(Int, Player)]
|
||||
|
||||
// This should rank players by score, and rank all withdrawn players after active ones.
|
||||
def rank(tournament: Tournament, players: Players): RankedPlayers
|
||||
|
||||
// You must override either this one or scoreSheet!
|
||||
def scoreSheets(tournament: Tournament): Map[String,Sheet] = {
|
||||
tournament.players.map { p =>
|
||||
(p.id -> scoreSheet(tournament, p.id))
|
||||
} toMap
|
||||
}
|
||||
|
||||
// You must override either this one or scoreSheets!
|
||||
def scoreSheet(tournament: Tournament, player: String): Sheet = scoreSheets(tournament)(player)
|
||||
}
|
|
@ -8,8 +8,10 @@ import chess.{ Variant, Mode }
|
|||
import lila.game.PovRef
|
||||
import lila.user.User
|
||||
|
||||
|
||||
private[tournament] case class Data(
|
||||
name: String,
|
||||
system: System,
|
||||
clock: TournamentClock,
|
||||
minutes: Int,
|
||||
minPlayers: Int,
|
||||
|
@ -28,6 +30,7 @@ sealed trait Tournament {
|
|||
def players: Players
|
||||
def winner: Option[Player]
|
||||
def pairings: List[Pairing]
|
||||
def events: List[Event]
|
||||
def isOpen: Boolean = false
|
||||
def isRunning: Boolean = false
|
||||
def isFinished: Boolean = false
|
||||
|
@ -38,6 +41,7 @@ sealed trait Tournament {
|
|||
def minutes = data.minutes
|
||||
lazy val duration = new Duration(minutes * 60 * 1000)
|
||||
|
||||
def system = data.system
|
||||
def variant = data.variant
|
||||
def mode = data.mode
|
||||
def rated = mode.rated
|
||||
|
@ -72,7 +76,11 @@ sealed trait Tournament {
|
|||
|
||||
def userPairings(user: String) = pairings filter (_ contains user)
|
||||
|
||||
def scoreSheet(player: Player) = Score.sheet(player.id, this)
|
||||
def scoreSheet(player: Player) = system.scoringSystem.scoreSheet(this, player.id)
|
||||
|
||||
// Oldest first!
|
||||
def pairingsAndEvents: List[Either[Pairing,Event]] =
|
||||
(pairings.reverse.map(Left(_)) ::: events.map(Right(_))).sorted(Tournament.PairingEventOrdering)
|
||||
}
|
||||
|
||||
sealed trait Enterable extends Tournament {
|
||||
|
@ -100,16 +108,13 @@ sealed trait Enterable extends Tournament {
|
|||
}
|
||||
|
||||
sealed trait StartedOrFinished extends Tournament {
|
||||
type RankedPlayers = List[(Int,Player)]
|
||||
|
||||
def startedAt: DateTime
|
||||
def withPlayers(s: Players): StartedOrFinished
|
||||
def refreshPlayers: StartedOrFinished
|
||||
|
||||
type RankedPlayers = List[(Int, Player)]
|
||||
def rankedPlayers: RankedPlayers = players.foldLeft(Nil: RankedPlayers) {
|
||||
case (Nil, p) => (1, p) :: Nil
|
||||
case (list@((r0, p0) :: _), p) => ((p0.score == p.score).fold(r0, list.size + 1), p) :: list
|
||||
}.reverse
|
||||
def rankedPlayers: RankedPlayers = system.scoringSystem.rank(this, players)
|
||||
|
||||
def winner = players.headOption
|
||||
def winnerUserId = winner map (_.id)
|
||||
|
@ -121,6 +126,7 @@ sealed trait StartedOrFinished extends Tournament {
|
|||
id = id,
|
||||
status = status.id,
|
||||
name = data.name,
|
||||
system = data.system.id,
|
||||
clock = data.clock,
|
||||
minutes = data.minutes,
|
||||
minPlayers = data.minPlayers,
|
||||
|
@ -132,7 +138,8 @@ sealed trait StartedOrFinished extends Tournament {
|
|||
startedAt = startedAt.some,
|
||||
schedule = data.schedule,
|
||||
players = players,
|
||||
pairings = pairings map (_.encode))
|
||||
pairings = pairings map (_.encode),
|
||||
events = events map (_.encode))
|
||||
|
||||
def finishedAt = startedAt + duration
|
||||
}
|
||||
|
@ -154,12 +161,15 @@ case class Created(
|
|||
|
||||
def pairings = Nil
|
||||
|
||||
def events = Nil
|
||||
|
||||
def winner = None
|
||||
|
||||
def encode = new RawTournament(
|
||||
id = id,
|
||||
status = Status.Created.id,
|
||||
name = data.name,
|
||||
system = data.system.id,
|
||||
clock = data.clock,
|
||||
variant = data.variant.id,
|
||||
mode = data.mode.id,
|
||||
|
@ -180,7 +190,7 @@ case class Created(
|
|||
|
||||
def startIfReady = enoughPlayersToStart option start
|
||||
|
||||
def start = Started(id, data, DateTime.now, players, Nil)
|
||||
def start = Started(id, data, DateTime.now, players, Nil, Nil)
|
||||
|
||||
def asScheduled = schedule map { Scheduled(this, _) }
|
||||
|
||||
|
@ -201,7 +211,8 @@ case class Started(
|
|||
data: Data,
|
||||
startedAt: DateTime,
|
||||
players: Players,
|
||||
pairings: List[Pairing]) extends StartedOrFinished with Enterable {
|
||||
pairings: List[Pairing],
|
||||
events: List[Event]) extends StartedOrFinished with Enterable {
|
||||
|
||||
override def isRunning = true
|
||||
|
||||
|
@ -212,6 +223,9 @@ case class Started(
|
|||
pairings = pairings map { p => (p.gameId == gameId).fold(f(p), p) }
|
||||
)
|
||||
|
||||
def addEvents(es: Events) =
|
||||
copy(events = es ::: events)
|
||||
|
||||
def readyToFinish = (remainingSeconds == 0) || (nbActiveUsers < 2)
|
||||
|
||||
def remainingSeconds: Float = math.max(0f,
|
||||
|
@ -240,7 +254,8 @@ case class Started(
|
|||
data = tour.data,
|
||||
startedAt = tour.startedAt,
|
||||
players = tour.players,
|
||||
pairings = tour.pairings filterNot (_.playing))
|
||||
pairings = tour.pairings filterNot (_.playing),
|
||||
events = tour.events)
|
||||
}
|
||||
|
||||
def withdraw(userId: String): Valid[Started] = contains(userId).fold(
|
||||
|
@ -277,7 +292,8 @@ case class Finished(
|
|||
data: Data,
|
||||
startedAt: DateTime,
|
||||
players: Players,
|
||||
pairings: List[Pairing]) extends StartedOrFinished {
|
||||
pairings: List[Pairing],
|
||||
events: List[Event]) extends StartedOrFinished {
|
||||
|
||||
override def isFinished = true
|
||||
|
||||
|
@ -324,12 +340,14 @@ object Tournament {
|
|||
clock: TournamentClock,
|
||||
minutes: Int,
|
||||
minPlayers: Int,
|
||||
system: System,
|
||||
variant: Variant,
|
||||
mode: Mode,
|
||||
password: Option[String]): Created = Created(
|
||||
id = Random nextStringUppercase 8,
|
||||
data = Data(
|
||||
name = RandomName(),
|
||||
system = system,
|
||||
clock = clock,
|
||||
createdBy = createdBy.id,
|
||||
createdAt = DateTime.now,
|
||||
|
@ -345,6 +363,7 @@ object Tournament {
|
|||
id = Random nextStringUppercase 8,
|
||||
data = Data(
|
||||
name = sched.name,
|
||||
system = System.default,
|
||||
clock = Schedule clockFor sched,
|
||||
createdBy = "lichess",
|
||||
createdAt = DateTime.now,
|
||||
|
@ -355,11 +374,29 @@ object Tournament {
|
|||
schedule = Some(sched),
|
||||
minPlayers = 0),
|
||||
players = List())
|
||||
|
||||
// To sort combined sequences of pairings and events.
|
||||
// This sorts all pairings/events in chronological order. Pairings without a timestamp
|
||||
// are assumed to have happened at the origin of time (i.e. before anything else).
|
||||
object PairingEventOrdering extends Ordering[Either[Pairing,Event]] {
|
||||
def compare(x: Either[Pairing,Event], y: Either[Pairing,Event]): Int = {
|
||||
val ot1: Option[DateTime] = x.fold(_.pairedAt, e => Some(e.timestamp))
|
||||
val ot2: Option[DateTime] = y.fold(_.pairedAt, e => Some(e.timestamp))
|
||||
|
||||
(ot1,ot2) match {
|
||||
case (None,None) => 0
|
||||
case (None,Some(_)) => -1
|
||||
case (Some(_),None) => 1
|
||||
case (Some(t1),Some(t2)) => if(t1 equals t2) 0 else if(t1 isBefore t2) -1 else 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private[tournament] case class RawTournament(
|
||||
id: String,
|
||||
name: String,
|
||||
system: Int = System.default.id,
|
||||
clock: TournamentClock,
|
||||
minutes: Int,
|
||||
minPlayers: Int,
|
||||
|
@ -370,6 +407,7 @@ private[tournament] case class RawTournament(
|
|||
startedAt: Option[DateTime] = None,
|
||||
players: List[Player] = Nil,
|
||||
pairings: List[RawPairing] = Nil,
|
||||
events: List[RawEvent] = Nil,
|
||||
variant: Int = Variant.Standard.id,
|
||||
mode: Int = Mode.Casual.id,
|
||||
schedule: Option[Schedule] = None) {
|
||||
|
@ -389,7 +427,8 @@ private[tournament] case class RawTournament(
|
|||
data = data,
|
||||
startedAt = stAt,
|
||||
players = players,
|
||||
pairings = decodePairings)
|
||||
pairings = decodePairings,
|
||||
events = decodeEvents)
|
||||
|
||||
def finished: Option[Finished] = for {
|
||||
stAt ← startedAt
|
||||
|
@ -399,12 +438,14 @@ private[tournament] case class RawTournament(
|
|||
data = data,
|
||||
startedAt = stAt,
|
||||
players = players,
|
||||
pairings = decodePairings)
|
||||
pairings = decodePairings,
|
||||
events = decodeEvents)
|
||||
|
||||
def enterable: Option[Enterable] = created orElse started
|
||||
|
||||
private def data = Data(
|
||||
name,
|
||||
System orDefault system,
|
||||
clock,
|
||||
minutes,
|
||||
minPlayers,
|
||||
|
@ -417,6 +458,8 @@ private[tournament] case class RawTournament(
|
|||
|
||||
private def decodePairings = pairings map (_.decode) flatten
|
||||
|
||||
private def decodeEvents = events map (_.decode) flatten
|
||||
|
||||
def any: Option[Tournament] = Status(status) flatMap {
|
||||
case Status.Created => created
|
||||
case Status.Started => started
|
||||
|
@ -431,6 +474,7 @@ private[tournament] object RawTournament {
|
|||
import play.api.libs.json._
|
||||
|
||||
private implicit def pairingTube = RawPairing.tube
|
||||
private implicit def eventTube = RawEvent.tube
|
||||
private implicit def clockTube = TournamentClock.tube
|
||||
private implicit def scheduleTube = Schedule.tube
|
||||
private implicit def PlayerTube = Player.tube
|
||||
|
@ -440,6 +484,8 @@ private[tournament] object RawTournament {
|
|||
"startedAt" -> none[DateTime],
|
||||
"players" -> List[Player](),
|
||||
"pairings" -> List[RawPairing](),
|
||||
"events" -> List[RawEvent](),
|
||||
"system" -> System.default.id,
|
||||
"variant" -> Variant.Standard.id,
|
||||
"mode" -> Mode.Casual.id,
|
||||
"schedule" -> none[Schedule])
|
||||
|
|
|
@ -32,12 +32,14 @@ private[tournament] final class TournamentApi(
|
|||
lobby: ActorSelection,
|
||||
roundMap: ActorRef) {
|
||||
|
||||
def makePairings(oldTour: Started, pairings: NonEmptyList[Pairing]) {
|
||||
def makePairings(oldTour: Started, pairings: NonEmptyList[Pairing], postEvents: Events) {
|
||||
sequence(oldTour.id) {
|
||||
TournamentRepo startedById oldTour.id flatMap {
|
||||
case Some(tour) =>
|
||||
val tour2 = tour addPairings pairings
|
||||
$update(tour2) >> (pairings map autoPairing(tour2)).sequence map {
|
||||
val tour3 = if(postEvents.isEmpty) tour2 else { tour2 addEvents postEvents }
|
||||
|
||||
$update(tour3) >> (pairings map autoPairing(tour3)).sequence map {
|
||||
_.list foreach { game =>
|
||||
game.tournamentId foreach { tid =>
|
||||
sendTo(tid, StartGame(game))
|
||||
|
@ -58,6 +60,7 @@ private[tournament] final class TournamentApi(
|
|||
minPlayers = setup.minPlayers,
|
||||
mode = Mode orDefault setup.mode,
|
||||
password = setup.password,
|
||||
system = System orDefault setup.system,
|
||||
variant = Variant orDefault setup.variant)
|
||||
$insert(created) >>-
|
||||
(withdrawIds foreach socketReload) >>-
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
package lila.tournament
|
||||
package arena
|
||||
|
||||
import lila.tournament.{ PairingSystem => AbstractPairingSystem }
|
||||
|
||||
import scala.util.Random
|
||||
import scala.concurrent.Future
|
||||
|
||||
object PairingSystem extends AbstractPairingSystem {
|
||||
type P = (String, String)
|
||||
|
||||
def createPairings(tour: Tournament, users: List[String]): Future[(Pairings,Events)] = {
|
||||
val pairings = tour.pairings
|
||||
val nbActiveUsers = tour.nbActiveUsers
|
||||
|
||||
if (users.size < 2)
|
||||
Future.successful((Nil,Nil))
|
||||
else {
|
||||
val idles: List[String] = Random shuffle {
|
||||
users.toSet diff { (pairings filter (_.playing) flatMap (_.users)).toSet } toList
|
||||
}
|
||||
|
||||
val ps = pairings.isEmpty.fold(
|
||||
naivePairings(idles),
|
||||
(idles.size > 12).fold(
|
||||
naivePairings(idles),
|
||||
smartPairings(idles, pairings, nbActiveUsers)
|
||||
)
|
||||
)
|
||||
|
||||
Future.successful((ps,Nil))
|
||||
}
|
||||
}
|
||||
|
||||
private def naivePairings(users: List[String]) =
|
||||
Random shuffle users grouped 2 collect {
|
||||
case List(u1, u2) => Pairing(u1, u2)
|
||||
} toList
|
||||
|
||||
private def smartPairings(users: List[String], pairings: Pairings, nbActiveUsers: Int): Pairings = {
|
||||
|
||||
def lastOpponent(user: String): Option[String] =
|
||||
pairings find (_ contains user) flatMap (_ opponentOf user)
|
||||
|
||||
def justPlayedTogether(u1: String, u2: String): Boolean =
|
||||
lastOpponent(u1) == u2.some && lastOpponent(u2) == u1.some
|
||||
|
||||
def timeSincePlay(u: String): Int =
|
||||
pairings.takeWhile(_ notContains u).size
|
||||
|
||||
// lower is better
|
||||
def score(pair: P): Int = pair match {
|
||||
case (a, b) => justPlayedTogether(a, b).fold(
|
||||
100,
|
||||
-timeSincePlay(a) - timeSincePlay(b))
|
||||
}
|
||||
|
||||
(users match {
|
||||
case x if x.size < 2 => Nil
|
||||
case List(u1, u2) if nbActiveUsers == 2 => List(u1 -> u2)
|
||||
case List(u1, u2) if justPlayedTogether(u1, u2) => Nil
|
||||
case List(u1, u2) => List(u1 -> u2)
|
||||
case us => allPairCombinations(us)
|
||||
.map(c => c -> c.map(score).sum)
|
||||
.sortBy(_._2)
|
||||
.headOption
|
||||
.map(_._1) | Nil
|
||||
}) map Pairing.apply
|
||||
}
|
||||
|
||||
private def allPairCombinations(list: List[String]): List[List[(String, String)]] = list match {
|
||||
case a :: rest => for {
|
||||
b ← rest
|
||||
init = (a -> b)
|
||||
nps = allPairCombinations(rest filter (b !=))
|
||||
ps ← nps.isEmpty.fold(List(List(init)), nps map (np => init :: np))
|
||||
} yield ps
|
||||
case _ => Nil
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +1,41 @@
|
|||
package lila.tournament
|
||||
package arena
|
||||
|
||||
case class Score(
|
||||
win: Option[Boolean],
|
||||
flag: Score.Flag) {
|
||||
|
||||
val value = this match {
|
||||
case Score(Some(true), Score.Double) => 4
|
||||
case Score(Some(true), _) => 2
|
||||
case Score(None, Score.Double) => 2
|
||||
case Score(None, _) => 1
|
||||
case _ => 0
|
||||
}
|
||||
}
|
||||
|
||||
object Score {
|
||||
|
||||
case class Sheet(scores: List[Score]) {
|
||||
val total = scores.foldLeft(0)(_ + _.value)
|
||||
def onFire = Score firstTwoAreWins scores
|
||||
}
|
||||
import lila.tournament.{ ScoringSystem => AbstractScoringSystem }
|
||||
import lila.tournament.{ Score => AbstractScore }
|
||||
|
||||
object ScoringSystem extends AbstractScoringSystem {
|
||||
sealed trait Flag
|
||||
case object StreakStarter extends Flag
|
||||
case object Double extends Flag
|
||||
case object Normal extends Flag
|
||||
|
||||
def sheet(user: String, tour: Tournament) = Sheet {
|
||||
case class Score(
|
||||
win: Option[Boolean],
|
||||
flag: Flag) extends AbstractScore {
|
||||
|
||||
val value = this match {
|
||||
case Score(Some(true), Double) => 4
|
||||
case Score(Some(true), _) => 2
|
||||
case Score(None, Double) => 2
|
||||
case Score(None, _) => 1
|
||||
case _ => 0
|
||||
}
|
||||
}
|
||||
|
||||
case class Sheet(scores: List[Score]) extends ScoreSheet {
|
||||
val total = scores.foldLeft(0)(_ + _.value)
|
||||
def onFire = firstTwoAreWins(scores)
|
||||
}
|
||||
|
||||
override def rank(tour: Tournament, players: Players): RankedPlayers = {
|
||||
players.foldLeft(Nil: RankedPlayers) {
|
||||
case (Nil, p) => (1, p) :: Nil
|
||||
case (list@((r0, p0) :: _), p) => ((p0.score == p.score).fold(r0, list.size + 1), p) :: list
|
||||
}.reverse
|
||||
}
|
||||
|
||||
override def scoreSheet(tour: Tournament, user: String) = Sheet {
|
||||
val filtered = tour userPairings user filter (_.finished) reverse
|
||||
val nexts = (filtered drop 1 map Some.apply) :+ None
|
||||
filtered.zip(nexts).foldLeft(List[Score]()) {
|
|
@ -18,9 +18,11 @@ package object tournament extends PackageObject with WithPlay with WithSocket {
|
|||
}
|
||||
}
|
||||
|
||||
private[tournament] type Players = List[tournament.Player]
|
||||
|
||||
private[tournament] type Pairings = List[tournament.Pairing]
|
||||
|
||||
private[tournament] type Players = List[tournament.Player]
|
||||
private[tournament] type Events = List[tournament.Event]
|
||||
|
||||
private[tournament] object RandomName {
|
||||
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
package lila.tournament
|
||||
package swiss
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.Duration
|
||||
|
||||
import lila.tournament.{ Score => AbstractScore }
|
||||
import lila.game.{ Game, GameRepo }
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.util.Try
|
||||
|
||||
import scalaz.NonEmptyList
|
||||
|
||||
object SwissSystem extends PairingSystem with ScoringSystem {
|
||||
private val MinTimeBetweenRounds = Duration.standardSeconds(10L)
|
||||
|
||||
sealed abstract class Score(val value: Int, val repr: String) extends AbstractScore
|
||||
case object Win extends Score(2, "1")
|
||||
case object Loss extends Score(0, "0")
|
||||
case object Draw extends Score(1, "½")
|
||||
case object Byed extends Score(2, "B")
|
||||
case object Absent extends Score(0, "—")
|
||||
case object Ongoing extends Score(0, "*")
|
||||
|
||||
case class Sheet(scores: List[Score], total: Int, neustadtl: Int) extends ScoreSheet {
|
||||
private def f(d: Int): String = d match {
|
||||
case 0 => ""
|
||||
case 1 => "¼"
|
||||
case 2 => "½"
|
||||
case 3 => "¾"
|
||||
}
|
||||
lazy val totalRepr: String = (total/2) + f(2*(total%2))
|
||||
lazy val neustadtlRepr: String = (neustadtl/4) + f(neustadtl%4)
|
||||
|
||||
def compare(other: Sheet): Int = {
|
||||
if(total > other.total) 1
|
||||
else if(total < other.total) -1
|
||||
else if(neustadtl > other.neustadtl) 1
|
||||
else if(neustadtl < other.neustadtl) -1
|
||||
else 0
|
||||
}
|
||||
}
|
||||
private val BlankSheet = Sheet(Nil, 0, 0)
|
||||
|
||||
private type STour = swisssystem.Tournament[String]
|
||||
private type Sheets = Map[String,Sheet]
|
||||
|
||||
private object SheetOrdering extends Ordering[Sheet] {
|
||||
def compare(s1: Sheet, s2: Sheet): Int = s1 compare s2
|
||||
}
|
||||
|
||||
// I feel like this must exist in the stdlib somewhere...
|
||||
// Maybe in scalaz? It's like String.split, but for lists...
|
||||
private def split[T](l: List[T], p: T=>Boolean): List[List[T]] = {
|
||||
def s(l: List[T], p: T=>Boolean): NonEmptyList[List[T]] = l match {
|
||||
case Nil => NonEmptyList(Nil, Nil)
|
||||
case x :: xs if p(x) => NonEmptyList(Nil, s(xs, p).list: _*)
|
||||
case x :: xs =>
|
||||
val rec = s(xs, p)
|
||||
val (r, rs) = (rec.head, rec.tail)
|
||||
NonEmptyList(x :: r, rs: _*)
|
||||
}
|
||||
s(l,p).list.filterNot(_.isEmpty)
|
||||
}
|
||||
|
||||
override def scoreSheets(tour: Tournament): Map[String,Sheet] = {
|
||||
fromHistory(tour)._2
|
||||
}
|
||||
|
||||
override def rank(tour: Tournament, players: Players): RankedPlayers = {
|
||||
val ss = scoreSheets(tour)
|
||||
|
||||
def r(ps: Players) = {
|
||||
val withSheets = ps map { p =>
|
||||
(p, ss.getOrElse(p.id, BlankSheet))
|
||||
}
|
||||
val sorted = withSheets.sortBy(_._2)(SheetOrdering).reverse
|
||||
|
||||
// yeurk.
|
||||
val ranked = sorted.foldLeft[(RankedPlayers,Sheet)]((Nil,BlankSheet)) {
|
||||
case ((Nil,_),(p,s)) => ((1, p) :: Nil, s)
|
||||
case ((l@(r0::_),s0), (p,s)) if s0.compare(s) == 0 => ((r0._1, p) :: l, s0)
|
||||
case ((l,_),(p, s)) => ((l.size + 1, p) :: l, s)
|
||||
}
|
||||
ranked._1.reverse
|
||||
}
|
||||
|
||||
val (active,inactive) = players.partition(_.active)
|
||||
r(active) ::: r(inactive)
|
||||
}
|
||||
|
||||
override def createPairings(tour: Tournament, users: List[String]): Future[(Pairings,Events)] = {
|
||||
val failed = (Nil,Nil)
|
||||
val failedFuture = Future.successful(failed) // Oh the irony.
|
||||
|
||||
// Notice how this doesn't use users: we get to pair players who haven't returned to the lobby yet.
|
||||
val toPair = tour.activePlayers.map(_.id).toSet
|
||||
|
||||
if(toPair.size < 2) {
|
||||
failedFuture
|
||||
} else if(tour.pairings.exists(_.playing)) {
|
||||
// Can't pair if games are still going on.
|
||||
failedFuture
|
||||
} else {
|
||||
val now = DateTime.now
|
||||
|
||||
val pairingTimes: Option[NonEmptyList[DateTime]] = tour.pairings.flatMap(_.pairedAt).toNel
|
||||
|
||||
val lastRoundGames: Future[List[Game]] = pairingTimes.fold(Future.successful(Nil:List[Game])) { pts =>
|
||||
val mostRecentPairingTime: DateTime = pts.maxBy(_.getMillis) // safe !
|
||||
val lastRoundGameIds: List[String] = tour.pairings.collect {
|
||||
case p if p.pairedAt == Some(mostRecentPairingTime) => p.gameId
|
||||
}
|
||||
GameRepo.games(lastRoundGameIds)
|
||||
}
|
||||
|
||||
lastRoundGames map { games =>
|
||||
val updateTimes: List[DateTime] = games.flatMap(_.updatedAt)
|
||||
if(updateTimes.exists(t => t.plus(MinTimeBetweenRounds).isAfter(now))) {
|
||||
// Too soon!
|
||||
failed
|
||||
} else {
|
||||
val (tt, _) = fromHistory(tour)
|
||||
|
||||
tt flatMap { t =>
|
||||
t.pairings(toPair) map { p =>
|
||||
val ps = p.pairs.map {
|
||||
case (p1,p2) => Pairing(p1,p2,now)
|
||||
}
|
||||
val roundEnd = RoundEnd(now.plusMillis(1))
|
||||
val events = p.unpaired map { u =>
|
||||
Bye(u, now) :: roundEnd :: Nil
|
||||
} getOrElse {
|
||||
roundEnd :: Nil
|
||||
}
|
||||
(ps,events)
|
||||
}
|
||||
} getOrElse {
|
||||
failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure you don't run that too often. For example, use scoreSheets rather than scoreSheet...
|
||||
private def fromHistory(tour: Tournament): (Try[STour],Sheets) = {
|
||||
val players: Map[String,Int] = tour.players.map(p => (p.id -> p.rating)).toMap
|
||||
|
||||
val history = tour.pairingsAndEvents
|
||||
|
||||
val historyByRound = split[Either[Pairing,Event]](history, _ match {
|
||||
case Right(RoundEnd(_)) => true
|
||||
case _ => false
|
||||
})
|
||||
|
||||
val listsByRound: List[(Pairings,Events)] = historyByRound.map { r =>
|
||||
val (ls,rs) = r.partition(_.isLeft)
|
||||
(ls.map(_.left.get), rs.map(_.right.get))
|
||||
}
|
||||
|
||||
val pairingsByRound: List[List[Pairing]] = listsByRound.map(_._1)
|
||||
val eventsByRound: List[List[Event]] = listsByRound.map(_._2)
|
||||
|
||||
// Creating the swisssystem instance...
|
||||
val t = Try(swisssystem.Tournament.create(players))
|
||||
val t2 = pairingsByRound.flatten.filter(_.finished).foldLeft(t) { (tt, p) =>
|
||||
tt flatMap { t =>
|
||||
val p1 = p.user1
|
||||
val p2 = p.user2
|
||||
val (v1,v2) = if(p.draw) (1,1) else if(p.wonBy(p1)) (2,0) else (0,2)
|
||||
t.withResult(p1, v1, p2, v2)
|
||||
}
|
||||
}
|
||||
val t3 = eventsByRound.flatten.collect { case Bye(u, _) => u }.foldLeft(t2) { (tt, p) =>
|
||||
tt flatMap { t => t.withBye(p, 2) }
|
||||
}
|
||||
|
||||
val ss: Map[String,Sheet] = players.keySet.toList map { player =>
|
||||
val scores = listsByRound.map { r =>
|
||||
r._1 find(_.contains(player)) map { p =>
|
||||
if(p.playing) Ongoing
|
||||
else if(p.wonBy(player)) Win
|
||||
else if(p.draw) Draw else Loss
|
||||
} getOrElse {
|
||||
if(r._2.exists(e => e match {
|
||||
case Bye(u, _) if u == player => true
|
||||
case _ => false
|
||||
})) Byed else Absent
|
||||
}
|
||||
} reverse // reverse is for compatibility... the view reverses it again.
|
||||
|
||||
val total = scores.map(_.value).sum
|
||||
val neustadtl = t3.map(_.performances.getOrElse(player, 0)).getOrElse(0)
|
||||
val sheet = Sheet(scores, total, neustadtl)
|
||||
|
||||
(player -> sheet)
|
||||
} toMap
|
||||
|
||||
(t3, ss)
|
||||
}
|
||||
}
|
||||
|
|
@ -40,7 +40,8 @@ object ApplicationBuild extends Build {
|
|||
gameSearch, timeline, forum, forumSearch, team, teamSearch,
|
||||
ai, analyse, mod, monitor, site, round, lobby, setup,
|
||||
importer, tournament, pool, relation, report, pref, // simulation,
|
||||
evaluation, chat, puzzle, tv, coordinate, blog, donation, qa)
|
||||
evaluation, chat, puzzle, tv, coordinate, blog, donation, qa,
|
||||
swisssystem)
|
||||
|
||||
lazy val moduleRefs = modules map projectToRef
|
||||
lazy val moduleCPDeps = moduleRefs map { new sbt.ClasspathDependency(_, None) }
|
||||
|
@ -155,7 +156,7 @@ object ApplicationBuild extends Build {
|
|||
)
|
||||
|
||||
lazy val setup = project("setup", Seq(
|
||||
common, db, memo, hub, socket, chess, game, user, lobby)).settings(
|
||||
common, db, memo, hub, socket, chess, game, tournament, user, lobby)).settings(
|
||||
libraryDependencies ++= provided(play.api, RM, PRM)
|
||||
)
|
||||
|
||||
|
@ -164,7 +165,7 @@ object ApplicationBuild extends Build {
|
|||
)
|
||||
|
||||
lazy val tournament = project("tournament", Seq(
|
||||
common, hub, socket, chess, game, round, security, chat, memo)).settings(
|
||||
common, hub, socket, chess, game, round, security, chat, memo, swisssystem)).settings(
|
||||
libraryDependencies ++= provided(play.api, RM, PRM)
|
||||
)
|
||||
|
||||
|
@ -255,4 +256,6 @@ object ApplicationBuild extends Build {
|
|||
)
|
||||
|
||||
lazy val chess = project("chess")
|
||||
|
||||
lazy val swisssystem = project("swisssystem")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue