swiss WIP

swiss
Thibault Duplessis 2020-05-04 16:59:08 -06:00
parent efd3bdf72f
commit a9104e7f1f
16 changed files with 130 additions and 33 deletions

View File

@ -102,7 +102,14 @@ object replay {
main(cls := "analyse")(
st.aside(cls := "analyse__side")(
views.html.game
.side(pov, initialFen, none, simul = simul, userTv = userTv, bookmarked = bookmarked)
.side(
pov,
initialFen,
none,
simul = simul,
userTv = userTv,
bookmarked = bookmarked
)
),
chatOption.map(_ => views.html.chat.frag),
div(cls := "analyse__board main-board")(chessgroundBoard),

View File

@ -130,16 +130,13 @@ object side {
a(cls := "text", dataIcon := "g", href := routes.Tournament.show(t.tour.id))(t.tour.name()),
div(cls := "clock", dataTime := t.tour.secondsToFinish)(div(cls := "time")(t.tour.clockStatus))
)
} orElse {
game.tournamentId map { tourId =>
st.section(cls := "game__tournament-link")(
a(href := routes.Tournament.show(tourId), dataIcon := "g", cls := "text")(
tournamentIdToName(tourId)
)
)
}
},
simul.map { sim =>
} orElse game.tournamentId.map { tourId =>
st.section(cls := "game__tournament-link")(tournamentLink(tourId))
} orElse game.swissId.map { swissId =>
st.section(cls := "game__tournament-link")(
views.html.swiss.bits.link(lila.swiss.Swiss.Id(swissId))
)
} orElse simul.map { sim =>
st.section(cls := "game__simul-link")(
a(href := routes.Simul.show(sim.id))(sim.fullName)
)

View File

@ -15,6 +15,8 @@ object jsI18n {
g.variant.exotic ?? variantTranslations
} ++ {
g.isTournament ?? tournamentTranslations
} ++ {
g.isSwiss ?? swissTranslations
}
}
@ -38,6 +40,11 @@ object jsI18n {
trans.standing
).map(_.key)
private val swissTranslations = Vector(
trans.backToTournament,
trans.viewTournament
).map(_.key)
private val baseTranslations = Vector(
trans.flipBoard,
trans.aiNameLevelAiLevel,

View File

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

View File

@ -14,6 +14,7 @@ object Iso {
type IntIso[B] = Iso[Int, B]
type BooleanIso[B] = Iso[Boolean, B]
type DoubleIso[B] = Iso[Double, B]
type FloatIso[B] = Iso[Float, B]
def apply[A, B](f: A => B, t: B => A): Iso[A, B] = new Iso[A, B] {
val from = f
@ -23,6 +24,7 @@ object Iso {
def string[B](from: String => B, to: B => String): StringIso[B] = apply(from, to)
def int[B](from: Int => B, to: B => Int): IntIso[B] = apply(from, to)
def double[B](from: Double => B, to: B => Double): DoubleIso[B] = apply(from, to)
def float[B](from: Float => B, to: B => Float): FloatIso[B] = apply(from, to)
def strings(sep: String): StringIso[Strings] = Iso[String, Strings](
str => Strings(str.split(sep).iterator.map(_.trim).to(List)),

View File

@ -42,6 +42,11 @@ trait Handlers {
def doubleAnyValHandler[A](to: A => Double, from: Double => A): BSONHandler[A] =
doubleIsoHandler(Iso(from, to))
def floatIsoHandler[A](implicit iso: FloatIso[A]): BSONHandler[A] =
BSONFloatHandler.as[A](iso.from, iso.to)
def floatAnyValHandler[A](to: A => Float, from: Float => A): BSONHandler[A] =
floatIsoHandler(Iso(from, to))
def dateIsoHandler[A](implicit iso: Iso[DateTime, A]): BSONHandler[A] =
BSONJodaDateTimeHandler.as[A](iso.from, iso.to)

View File

@ -40,8 +40,11 @@ private object BsonHandlers {
},
v => BSONString(v.fen)
)
implicit val swissPointsHandler = intAnyValHandler[Swiss.Points](_.double, Swiss.Points.apply)
implicit val swissScoreHandler = doubleAnyValHandler[Swiss.Score](_.value, Swiss.Score.apply)
implicit val swissPointsHandler = intAnyValHandler[Swiss.Points](_.double, Swiss.Points.apply)
implicit val swissTieBreakHandler = doubleAnyValHandler[Swiss.TieBreak](_.value, Swiss.TieBreak.apply)
implicit val swissPerformanceHandler =
floatAnyValHandler[Swiss.Performance](_.value, Swiss.Performance.apply)
implicit val swissScoreHandler = intAnyValHandler[Swiss.Score](_.value, Swiss.Score.apply)
implicit val playerNumberHandler = intAnyValHandler[SwissPlayer.Number](_.value, SwissPlayer.Number.apply)
implicit val roundNumberHandler = intAnyValHandler[SwissRound.Number](_.value, SwissRound.Number.apply)
implicit val swissIdHandler = stringAnyValHandler[Swiss.Id](_.value, Swiss.Id.apply)
@ -57,6 +60,8 @@ private object BsonHandlers {
rating = r int rating,
provisional = r boolD provisional,
points = r.get[Swiss.Points](points),
tieBreak = r.get[Swiss.TieBreak](tieBreak),
performance = r.getO[Swiss.Performance](performance),
score = r.get[Swiss.Score](score)
)
def writes(w: BSON.Writer, o: SwissPlayer) = $doc(
@ -67,6 +72,8 @@ private object BsonHandlers {
rating -> o.rating,
provisional -> w.boolO(o.provisional),
points -> o.points,
tieBreak -> o.tieBreak,
performance -> o.performance,
score -> o.score
)
}

View File

@ -54,6 +54,16 @@ final class Env(
lazy val getName = new GetSwissName(cache.name.sync)
lila.common.Bus.subscribeFun(
"finishGame",
"adjustCheater",
"adjustBooster"
) {
case lila.game.actorApi.FinishGame(game, _, _) => api finishGame game
// case lila.hub.actorApi.mod.MarkCheater(userId, true) => api.ejectLame(userId, _)
// case lila.hub.actorApi.mod.MarkBooster(userId) => api.ejectLame(userId, Nil)
}
ResilientScheduler(
every = Every(2 seconds),
atMost = AtMost(15 seconds),

View File

@ -67,7 +67,7 @@ final private class PairingSystem(executable: String) {
val pairing = pairings get rn
List(
95 -> pairing.map(_ opponentOf p.number).??(_.toString),
97 -> pairing.map(_ colorOf p.number).??(_.fold("w", "n")),
97 -> pairing.map(_ colorOf p.number).??(_.fold("w", "b")),
99 -> pairing.flatMap(_.winner).map(p.number ==).fold("=") {
case true => "1"
case false => "0"

View File

@ -72,7 +72,13 @@ object Swiss {
def value: Float = double / 2f
def +(p: Points) = Points(double + p.double)
}
case class Score(value: Double) extends AnyVal
case class TieBreak(value: Double) extends AnyVal
case class Performance(value: Float) extends AnyVal
case class Score(value: Int) extends AnyVal
def makeScore(points: Points, tieBreak: TieBreak, perf: Performance) = Score(
(points.value * 10000000 + tieBreak.value * 10000 + perf.value).toInt
)
def makeId = Id(scala.util.Random.alphanumeric take 8 mkString)
}

View File

@ -3,11 +3,13 @@ package lila.swiss
import org.joda.time.DateTime
import ornicar.scalalib.Zero
import reactivemongo.api._
import reactivemongo.api.bson._
import scala.concurrent.duration._
import lila.common.{ GreatPlayer, WorkQueues }
import lila.db.dsl._
import lila.hub.LightTeam.TeamID
import lila.game.Game
import lila.user.User
final class SwissApi(
@ -88,6 +90,32 @@ final class SwissApi(
def featuredInTeam(teamId: TeamID): Fu[List[Swiss]] =
colls.swiss.ext.find($doc("teamId" -> teamId)).sort($sort desc "startsAt").list[Swiss](5)
private[swiss] def finishGame(game: Game): Funit = game.swissId ?? { swissId =>
Sequencing(Swiss.Id(swissId))(startedById) { swiss =>
colls.pairing.byId[SwissPairing](game.id) flatMap {
_ ?? { pairing =>
val winner = game.winnerColor
.map(_.fold(pairing.white, pairing.black))
.flatMap(playerNumberHandler.writeOpt)
colls.pairing.updateField($id(game.id), SwissPairing.Fields.status, winner | BSONNull).void >>
scoring.recompute(swiss) >>
isReadyForNextRound(swiss).flatMap {
_ ?? colls.swiss.updateField($id(swiss.id), "nextRoundAt", DateTime.now.plusSeconds(10)).void
} >>-
socket.reload(swiss.id)
}
}
}
}
private def isReadyForNextRound(swiss: Swiss) =
SwissPairing
.fields { f =>
!colls.pairing.exists(
$doc(f.swissId -> swiss.id, f.round -> swiss.round, f.status -> SwissPairing.ongoing)
)
}
private[swiss] def destroy(swiss: Swiss): Funit =
colls.swiss.delete.one($id(swiss.id)) >>
colls.pairing.delete.one($doc(SwissPairing.Fields.swissId -> swiss.id)) >>

View File

@ -26,6 +26,8 @@ object SwissPairing {
case object Ongoing extends Ongoing
type Status = Either[Ongoing, Option[SwissPlayer.Number]]
val ongoing: Status = Left(Ongoing)
case class Pending(
white: SwissPlayer.Number,
black: SwissPlayer.Number

View File

@ -11,11 +11,17 @@ case class SwissPlayer(
rating: Int,
provisional: Boolean,
points: Swiss.Points,
tieBreak: Swiss.TieBreak,
performance: Option[Swiss.Performance],
score: Swiss.Score
) {
def is(uid: User.ID): Boolean = uid == userId
def is(user: User): Boolean = is(user.id)
def is(other: SwissPlayer): Boolean = is(other.userId)
def recomputeScore = copy(
score = Swiss.makeScore(points, tieBreak, performance | Swiss.Performance(rating.toFloat))
)
}
object SwissPlayer {
@ -29,16 +35,19 @@ object SwissPlayer {
number: SwissPlayer.Number,
user: User,
perfLens: Perfs => Perf
): SwissPlayer = new SwissPlayer(
id = makeId(swissId, user.id),
swissId = swissId,
number = number,
userId = user.id,
rating = perfLens(user.perfs).intRating,
provisional = perfLens(user.perfs).provisional,
points = Swiss.Points(0),
score = Swiss.Score(0)
)
): SwissPlayer =
new SwissPlayer(
id = makeId(swissId, user.id),
swissId = swissId,
number = number,
userId = user.id,
rating = perfLens(user.perfs).intRating,
provisional = perfLens(user.perfs).provisional,
points = Swiss.Points(0),
tieBreak = Swiss.TieBreak(0),
performance = none,
score = Swiss.Score(0)
).recomputeScore
case class Number(value: Int) extends AnyVal with IntValue
@ -63,6 +72,8 @@ object SwissPlayer {
val rating = "r"
val provisional = "pr"
val points = "p"
val tieBreak = "t"
val performance = "e"
val score = "c"
}
def fields[A](f: Fields.type => A): A = f(Fields)

View File

@ -30,13 +30,23 @@ final class SwissScoring(
}
playerMap = SwissPlayer.toMap(playersWithPoints)
players = playersWithPoints.map { p =>
p.copy(score = Swiss.Score {
(~pairingMap.get(p.number)).values.foldLeft(0d) {
case (score, pairing) =>
def opponentPoints = playerMap.get(pairing opponentOf p.number).??(_.points.value)
score + pairing.winner.map(p.number.==).fold(opponentPoints / 2) { _ ?? opponentPoints }
}
})
val playerPairings = (~pairingMap.get(p.number)).values
val (tieBreak, perfSum) = playerPairings.foldLeft(0f -> 0f) {
case ((tieBreak, perfSum), pairing) =>
val opponent = playerMap.get(pairing opponentOf p.number)
val opponentPoints = opponent.??(_.points.value)
val result = pairing.winner.map(p.number.==)
val newTieBreak = tieBreak + result.fold(opponentPoints / 2) { _ ?? opponentPoints }
val newPerf = perfSum + opponent.??(_.rating) + result.?? { win =>
if (win) 500 else -500
}
newTieBreak -> newPerf
}
p.copy(
tieBreak = Swiss.TieBreak(tieBreak),
performance = playerPairings.nonEmpty option Swiss.Performance(perfSum / playerPairings.size)
)
.recomputeScore
}
_ <- SwissPlayer.fields { f =>
prevPlayers

View File

@ -106,5 +106,7 @@ final class SwissStandingApi(
bestWithRank(id, nb, (page - 1) * nb)
private[swiss] def best(id: Swiss.Id, nb: Int, skip: Int = 0): Fu[List[SwissPlayer]] =
colls.player.ext.find($doc("s" -> id)).sort($sort desc "s").skip(skip).list[SwissPlayer](nb)
SwissPlayer.fields { f =>
colls.player.ext.find($doc(f.swissId -> id)).sort($sort desc f.score).skip(skip).list[SwissPlayer](nb)
}
}

View File

@ -59,6 +59,8 @@
letter-spacing: .1em;
& > * {
display: inline-block;
margin: 0 .8em;
text-align: center;
}
score {
opacity: 0.7;