swiss WIP

swiss
Thibault Duplessis 2020-04-29 15:14:35 -06:00
parent 3d3085d62f
commit 391046a7e7
8 changed files with 207 additions and 118 deletions

View File

@ -18,11 +18,11 @@ final class Swiss(
env.swiss.api.byId(SwissModel.Id(id)) flatMap {
_.fold(swissNotFound.fuccess) { swiss =>
for {
version <- env.swiss.version(swiss.id)
// rounds <- env.swiss.roundsOf(swiss)
version <- env.swiss.version(swiss.id)
leaderboard <- env.swiss.api.leaderboard(swiss, page = 1)
json <- env.swiss.json(
swiss = swiss,
// rounds = rounds,
leaderboard = leaderboard,
me = ctx.me,
socketVersion = version.some
)

View File

@ -1,2 +1,4 @@
db.swiss.ensureIndex({teamId:1,startsAt:1})
db.swiss_player.ensureIndex({s:1,c:-1})
db.swiss_pairing.ensureIndex({s:1,r:1})
db.swiss_pairing.ensureIndex({s:1,u:1,r:1})

View File

@ -45,68 +45,61 @@ private object BsonHandlers {
},
v => BSONString(v.fen)
)
implicit val swissPointsHandler = intAnyValHandler[Swiss.Points](_.double, Swiss.Points.apply)
implicit val swissPointsHandler = intAnyValHandler[Swiss.Points](_.double, Swiss.Points.apply)
implicit val swissScoreHandler = intAnyValHandler[Swiss.Score](_.double, Swiss.Score.apply)
implicit val playerNumberHandler = intAnyValHandler[SwissPlayer.Number](_.value, SwissPlayer.Number.apply)
implicit val playerIdHandler = tryHandler[SwissPlayer.Id](
{
case BSONString(v) =>
(v split ':' match {
case Array(swissId, number) =>
number.toIntOption map { n =>
SwissPlayer.Id(Swiss.Id(swissId), SwissPlayer.Number(n))
}
case _ => None
}) toTry s"Invalid player ID $v"
},
id => BSONString(s"${id.swissId}:${id.number}")
)
implicit val roundNumberHandler = intAnyValHandler[SwissRound.Number](_.value, SwissRound.Number.apply)
implicit val swissIdHandler = stringAnyValHandler[Swiss.Id](_.value, Swiss.Id.apply)
implicit val pairingIdHandler = stringAnyValHandler[SwissPairing.Id](_.value, SwissPairing.Id.apply)
implicit val playerIdHandler = stringAnyValHandler[SwissPlayer.Id](_.value, SwissPlayer.Id.apply)
implicit val playerHandler = new BSON[SwissPlayer] {
def reads(r: BSON.Reader) = SwissPlayer(
id = r.get[SwissPlayer.Id]("_id"),
userId = r str "uid",
_id = r.get[SwissPlayer.Id]("_id"),
swissId = r.get[Swiss.Id]("s"),
number = r.get[SwissPlayer.Number]("n"),
userId = r str "u",
rating = r int "r",
provisional = r boolD "pr",
points = r.get[Swiss.Points]("p")
points = r.get[Swiss.Points]("p"),
score = r.get[Swiss.Score]("c")
)
def writes(w: BSON.Writer, o: SwissPlayer) = $doc(
"_id" -> o.id,
"uid" -> o.userId,
"_id" -> o._id,
"s" -> o.swissId,
"n" -> o.number,
"u" -> o.userId,
"r" -> o.rating,
"pr" -> w.boolO(o.provisional),
"p" -> o.points
"p" -> o.points,
"c" -> o.score
)
}
implicit val swissIdHandler = stringAnyValHandler[Swiss.Id](_.value, Swiss.Id.apply)
implicit val pairingIdHandler = stringAnyValHandler[SwissPairing.Id](_.value, SwissPairing.Id.apply)
implicit val roundNumberHandler = intAnyValHandler[SwissRound.Number](_.value, SwissRound.Number.apply)
implicit val pairingHandler = new BSON[SwissPairing] {
def reads(r: BSON.Reader) = {
val white = r.get[SwissPlayer.Number]("w")
val black = r.get[SwissPlayer.Number]("b")
SwissPairing(
_id = r.get[SwissPairing.Id]("_id"),
swissId = r.get[Swiss.Id]("s"),
round = r.get[SwissRound.Number]("r"),
gameId = r str "g",
white = white,
black = black,
winner = r boolO "w" map {
case true => white
case _ => black
}
)
}
def reads(r: BSON.Reader) =
r.get[List[SwissPlayer.Number]]("u") match {
case List(white, black) =>
SwissPairing(
_id = r.get[SwissPairing.Id]("_id"),
swissId = r.get[Swiss.Id]("s"),
round = r.get[SwissRound.Number]("r"),
gameId = r str "g",
white = white,
black = black,
winner = r boolO "w" map {
case true => white
case _ => black
}
)
case _ => sys error "Invalid swiss pairing users"
}
def writes(w: BSON.Writer, o: SwissPairing) = $doc(
"_id" -> o._id,
"s" -> o.swissId,
"r" -> o.round,
"g" -> o.gameId,
"w" -> o.white,
"b" -> o.black,
"u" -> o.players,
"w" -> o.winner.map(o.white ==)
)
}

View File

@ -32,7 +32,7 @@ final private class PairingSystem(executable: String) {
private object writer {
private type Bits = List[(Int, String)]
private type PairingMap = Map[SwissPlayer.Number, Map[Int, SwissPairing]]
private type PairingMap = Map[SwissPlayer.Number, Map[SwissRound.Number, SwissPairing]]
def apply(swiss: Swiss, players: List[SwissPlayer], pairings: List[SwissPairing]): String = {
val pairingMap: PairingMap = pairings.foldLeft[PairingMap](Map.empty) {
@ -40,22 +40,22 @@ final private class PairingSystem(executable: String) {
pairing.players.foldLeft(acc) {
case (acc, player) =>
acc.updatedWith(player) { acc =>
(~acc).updated(pairing.round.value, pairing).some
(~acc).updated(pairing.round, pairing).some
}
}
}
s"XXR ${swiss.nbRounds}" :: players.map(player(pairingMap, swiss.round)).map(format)
s"XXR ${swiss.nbRounds}" :: players.map(player(swiss, pairingMap)).map(format)
} mkString "\n"
// https://www.fide.com/FIDE/handbook/C04Annex2_TRF16.pdf
private def player(pairingMap: PairingMap, rounds: SwissRound.Number)(p: SwissPlayer): Bits =
private def player(swiss: Swiss, pairingMap: PairingMap)(p: SwissPlayer): Bits =
List(
3 -> "001",
8 -> p.number.toString,
84 -> f"${p.points.value}%1.1f"
) ::: {
val pairings = ~pairingMap.get(p.number)
(1 to rounds.value).toList.flatMap { rn =>
swiss.finishedRounds.flatMap { rn =>
val pairing = pairings get rn
List(
95 -> pairing.map(_ opponentOf p.number).??(_.toString),
@ -64,7 +64,7 @@ final private class PairingSystem(executable: String) {
case true => "1"
case false => "0"
}
).map { case (l, s) => (l + (rn - 1) * 10, s) }
).map { case (l, s) => (l + (rn.value - 1) * 10, s) }
}
}

View File

@ -5,7 +5,7 @@ import chess.Speed
import org.joda.time.DateTime
import scala.concurrent.duration._
import lila.game.{ Game, PerfPicker }
import lila.game.PerfPicker
import lila.hub.LightTeam.TeamID
import lila.rating.PerfType
import lila.user.User
@ -17,7 +17,7 @@ case class Swiss(
clock: ClockConfig,
variant: chess.variant.Variant,
rated: Boolean,
round: SwissRound.Number,
round: SwissRound.Number, // ongoing round
nbRounds: Int,
nbPlayers: Int,
createdAt: DateTime,
@ -37,6 +37,9 @@ case class Swiss(
def isNowOrSoon = startsAt.isBefore(DateTime.now plusMinutes 15) && !isFinished
def allRounds: List[SwissRound.Number] = (1 to round.value).toList.map(SwissRound.Number.apply)
def finishedRounds: List[SwissRound.Number] = (1 to (round.value - 1)).toList.map(SwissRound.Number.apply)
def speed = Speed(clock)
def perfType: Option[PerfType] = PerfPicker.perfType(speed, variant, none)
@ -60,65 +63,9 @@ object Swiss {
case class Points(double: Int) extends AnyVal {
def value: Float = double / 2f
}
case class Score(double: Int) extends AnyVal {
def value: Float = double / 2f
}
def makeId = Id(scala.util.Random.alphanumeric take 8 mkString)
}
case class SwissPlayer(
id: SwissPlayer.Id,
userId: User.ID,
rating: Int,
provisional: Boolean,
points: Swiss.Points
) {
def number = id.number
}
object SwissPlayer {
case class Id(swissId: Swiss.Id, number: Number)
case class Number(value: Int) extends AnyVal with IntValue
}
// case class SwissRound(
// number: SwissRound.Number,
// pairings: List[SwissPairing]
// )
object SwissRound {
case class Number(value: Int) extends AnyVal with IntValue
}
case class SwissPairing(
_id: SwissPairing.Id, // random
swissId: Swiss.Id,
round: SwissRound.Number,
gameId: Game.ID,
white: SwissPlayer.Number,
black: SwissPlayer.Number,
winner: Option[SwissPlayer.Number]
) {
def players = List(white, black)
def has(number: SwissPlayer.Number) = white == number || black == number
def colorOf(number: SwissPlayer.Number) = chess.Color(white == number)
def opponentOf(number: SwissPlayer.Number) = if (white == number) black else white
}
object SwissPairing {
case class Id(value: String) extends AnyVal with StringValue
def makeId = Id(scala.util.Random.alphanumeric take 8 mkString)
case class Pending(
white: SwissPlayer.Number,
black: SwissPlayer.Number
)
}
case class SwissBye(
round: SwissRound.Number,
player: SwissPlayer.Number
)

View File

@ -1,11 +1,12 @@
package lila.swiss
import org.joda.time.DateTime
import reactivemongo.api._
import lila.common.GreatPlayer
import lila.db.dsl._
import lila.hub.LightTeam.TeamID
import lila.user.User
import lila.db.dsl._
import lila.common.GreatPlayer
final class SwissApi(
colls: SwissColls
@ -37,6 +38,48 @@ final class SwissApi(
colls.swiss.insert.one(swiss) inject swiss
}
def leaderboard(swiss: Swiss, page: Int): Fu[List[LeaderboardPlayer]] =
colls.player
.aggregateList(maxDocs = 10, readPreference = ReadPreference.secondaryPreferred) { implicit framework =>
import framework._
Match($doc("s" -> swiss.id)) -> List(
Sort(Descending("t")),
Skip((page - 1) * 10),
Limit(10),
PipelineOperator(
$doc(
"$lookup" -> $doc(
"from" -> colls.pairing.name,
"let" -> $doc("n" -> "$n"),
"pipeline" -> $arr(
$doc(
"$match" -> $doc(
"$expr" -> $doc(
"$and" -> $arr(
$doc("s" -> swiss.id),
$doc("u" -> "$$n")
)
)
)
)
),
"as" -> "pairings"
)
)
)
)
}
.map {
_ map { doc =>
LeaderboardPlayer(
playerHandler.read(doc),
(~doc.getAsOpt[List[SwissPairing]]("pairings")).map { p =>
p.round -> p
}.toMap
)
}
}
def pairingsOf(swiss: Swiss) =
colls.pairing.ext.find($doc("s" -> swiss.id)).sort($sort asc "r").list[SwissPairing]()

View File

@ -15,12 +15,12 @@ import lila.socket.Socket.SocketVersion
import lila.user.User
final class SwissJson(
// lightUserApi: lila.user.LightUserApi
lightUserApi: lila.user.LightUserApi
)(implicit ec: ExecutionContext) {
def apply(
swiss: Swiss,
// rounds: List[SwissRound],
leaderboard: List[LeaderboardPlayer],
me: Option[User],
socketVersion: Option[SocketVersion]
)(implicit lang: Lang): Fu[JsObject] = fuccess {
@ -34,7 +34,26 @@ final class SwissJson(
"clock" -> swiss.clock,
"variant" -> swiss.variant.key,
"nbRounds" -> swiss.nbRounds,
"nbPlayers" -> swiss.nbPlayers
"nbPlayers" -> swiss.nbPlayers,
"leaderboard" -> leaderboard.map { l =>
Json.obj(
"player" -> Json.obj(
"user" -> lightUserApi.sync(l.player.userId),
"rating" -> l.player.rating,
"points" -> l.player.points,
"score" -> l.player.score
),
"pairings" -> swiss.allRounds.map(l.pairings.get).map {
_.fold[JsValue](JsNull) { p =>
Json.obj(
"o" -> p.opponentOf(l.player.number),
"g" -> p.gameId,
"w" -> p.winner.map(l.player.number.==)
)
}
}
)
}
)
.add("isStarted" -> swiss.isStarted)
.add("isFinished" -> swiss.isFinished)
@ -45,6 +64,25 @@ final class SwissJson(
private def formatDate(date: DateTime) = ISODateTimeFormat.dateTime print date
implicit private val playerNumberWriter: Writes[SwissPlayer.Number] = Writes[SwissPlayer.Number] { n =>
JsNumber(n.value)
}
implicit private val pointsWriter: Writes[Swiss.Points] = Writes[Swiss.Points] { p =>
JsNumber(p.value)
}
implicit private val scoreWriter: Writes[Swiss.Score] = Writes[Swiss.Score] { s =>
JsNumber(s.value)
}
implicit private val pairingWrites: OWrites[SwissPairing] = OWrites { p =>
Json.obj(
"gameId" -> p.gameId,
"white" -> p.white,
"black" -> p.black,
"winner" -> p.winner
)
}
implicit private val clockWrites: OWrites[chess.Clock.Config] = OWrites { clock =>
Json.obj(
"limit" -> clock.limitSeconds,

View File

@ -0,0 +1,66 @@
package lila.swiss
import lila.user.User
import lila.game.Game
case class SwissPlayer(
_id: SwissPlayer.Id, // random
swissId: Swiss.Id,
number: SwissPlayer.Number,
userId: User.ID,
rating: Int,
provisional: Boolean,
points: Swiss.Points,
score: Swiss.Score
)
object SwissPlayer {
case class Id(value: String) extends AnyVal with StringValue
def makeId = Id(scala.util.Random.alphanumeric take 8 mkString)
case class Number(value: Int) extends AnyVal with IntValue
}
object SwissRound {
case class Number(value: Int) extends AnyVal with IntValue
}
case class SwissPairing(
_id: SwissPairing.Id, // random
swissId: Swiss.Id,
round: SwissRound.Number,
gameId: Game.ID,
white: SwissPlayer.Number,
black: SwissPlayer.Number,
winner: Option[SwissPlayer.Number]
) {
def players = List(white, black)
def has(number: SwissPlayer.Number) = white == number || black == number
def colorOf(number: SwissPlayer.Number) = chess.Color(white == number)
def opponentOf(number: SwissPlayer.Number) = if (white == number) black else white
}
object SwissPairing {
case class Id(value: String) extends AnyVal with StringValue
def makeId = Id(scala.util.Random.alphanumeric take 8 mkString)
case class Pending(
white: SwissPlayer.Number,
black: SwissPlayer.Number
)
}
case class SwissBye(
round: SwissRound.Number,
player: SwissPlayer.Number
)
case class LeaderboardPlayer(
player: SwissPlayer,
pairings: Map[SwissRound.Number, SwissPairing]
)