Skip to content

Commit

Permalink
start testing the swiss scoring
Browse files Browse the repository at this point in the history
  • Loading branch information
ornicar committed Jun 25, 2023
1 parent 9dbfd70 commit c9eec5d
Show file tree
Hide file tree
Showing 10 changed files with 107 additions and 43 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ lazy val tournament = module("tournament",

lazy val swiss = module("swiss",
Seq(gathering, round),
Seq(scalatags, lettuce) ++ reactivemongo.bundle
Seq(scalatags, lettuce) ++ reactivemongo.bundle ++ tests.bundle
)

lazy val simul = module("simul",
Expand Down
2 changes: 1 addition & 1 deletion modules/swiss/src/main/BsonHandlers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ object BsonHandlers:

given BSONHandler[chess.variant.Variant] = variantByKeyHandler
given BSONHandler[chess.Clock.Config] = clockConfigHandler
given BSONHandler[SwissPoints] = intAnyValHandler(_.doubled, SwissPoints.fromDouble)
given BSONHandler[SwissPoints] = intAnyValHandler(_.doubled, SwissPoints.fromDoubled)

given BSON[SwissPlayer] with
import SwissPlayer.Fields.*
Expand Down
2 changes: 1 addition & 1 deletion modules/swiss/src/main/Env.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ final class Env(

lazy val cache: SwissCache = wire[SwissCache]

lazy val getName = new GetSwissName(cache.name)
lazy val getName = GetSwissName(cache.name)

private lazy val officialSchedule = wire[SwissOfficialSchedule]

Expand Down
8 changes: 3 additions & 5 deletions modules/swiss/src/main/SwissApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,12 @@ final class SwissApi(
SwissPairing.fields: F =>
mongo.pairing
.list[SwissPairing]($doc(F.swissId -> swiss.id, F.players -> userId))
.flatMap {
.flatMap:
_.filter(p => p.isDraw || userId.is(p.winner))
.map { pairing =>
.map: pairing =>
mongo.pairing.update.one($id(pairing.id), pairing forfeit userId)
}
.parallel
.void
}

private[swiss] def finishGame(game: Game): Funit =
game.swissId.so: swissId =>
Expand Down Expand Up @@ -499,7 +497,7 @@ final class SwissApi(
def teamOf(id: SwissId): Fu[Option[TeamId]] =
mongo.swiss.primitiveOne[TeamId]($id(id), "teamId")

private def recomputeAndUpdateAll(id: SwissId): Funit =
def recomputeAndUpdateAll(id: SwissId): Funit =
scoring(id).flatMapz { res =>
rankingApi.update(res)
standingApi.update(res) >>
Expand Down
9 changes: 3 additions & 6 deletions modules/swiss/src/main/SwissPairing.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,7 @@ object SwissPairing:
def fields[A](f: Fields.type => A): A = f(Fields)

def toMap(pairings: List[SwissPairing]): PairingMap =
pairings.foldLeft[PairingMap](Map.empty) { (acc, pairing) =>
pairing.players.foldLeft(acc) { (acc, player) =>
acc.updatedWith(player) { playerPairings =>
pairings.foldLeft[PairingMap](Map.empty): (acc, pairing) =>
pairing.players.foldLeft(acc): (acc, player) =>
acc.updatedWith(player): playerPairings =>
(~playerPairings).updated(pairing.round, pairing).some
}
}
}
2 changes: 1 addition & 1 deletion modules/swiss/src/main/SwissPlayer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ object SwissPlayer:
userId = user.id,
rating = user.perfs(perf).intRating,
provisional = user.perfs(perf).provisional,
points = SwissPoints fromDouble 0,
points = SwissPoints fromDoubled 0,
tieBreak = Swiss.TieBreak(0),
performance = none,
score = Swiss.Score(0),
Expand Down
42 changes: 22 additions & 20 deletions modules/swiss/src/main/SwissScoring.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package lila.swiss

import reactivemongo.api.bson.*
import cats.syntax.all.*

import lila.db.dsl.{ *, given }

final private class SwissScoring(mongo: SwissMongo)(using
scheduler: Scheduler,
ec: Executor
):
final private class SwissScoring(mongo: SwissMongo)(using Scheduler, Executor):

import BsonHandlers.given

Expand All @@ -29,22 +27,7 @@ final private class SwissScoring(mongo: SwissMongo)(using
sheets = SwissSheet.many(swiss, prevPlayers, pairingMap)
withPoints = (prevPlayers zip sheets).map: (player, sheet) =>
player.copy(points = sheet.points)
playerMap = withPoints.mapBy(_.userId)
players = withPoints.map: p =>
val playerPairings = (~pairingMap.get(p.userId)).values
val (tieBreak, perfSum) = playerPairings.foldLeft(0f -> 0f):
case ((tieBreak, perfSum), pairing) =>
val opponent = playerMap.get(pairing opponentOf p.userId)
val opponentPoints = opponent.so(_.points.value)
val result = pairing.resultFor(p.userId)
val newTieBreak = tieBreak + result.fold(opponentPoints / 2) { _ so opponentPoints }
val newPerf = perfSum + opponent.so(_.rating.value) + result.so: win =>
if win then 500 else -500
newTieBreak -> newPerf
p.copy(
tieBreak = Swiss.TieBreak(tieBreak),
performance = playerPairings.nonEmpty option Swiss.Performance(perfSum / playerPairings.size)
).recomputeScore
players = SwissScoring.computePlayers(withPoints, pairingMap)
_ <- SwissPlayer.fields: f =>
prevPlayers
.zip(players)
Expand Down Expand Up @@ -97,3 +80,22 @@ private object SwissScoring:
playerMap: SwissPlayer.PlayerMap,
pairings: SwissPairing.PairingMap
)

def computePlayers(withPoints: List[SwissPlayer], pairingMap: SwissPairing.PairingMap) =
val playerMap = withPoints.mapBy(_.userId)
withPoints.map: p =>
val playerPairings = (~pairingMap.get(p.userId)).values
val (tieBreak, perfSum) = playerPairings.foldLeft(0f -> 0f):
case ((tieBreak, perfSum), pairing) =>
val opponent = playerMap.get(pairing opponentOf p.userId)
val opponentPoints = opponent.so(_.points.value)
val result = pairing.resultFor(p.userId)
val newTieBreak = tieBreak + result.fold(opponentPoints / 2)(_ so opponentPoints)
if p.userId.value == "supertactic91" then println(s"$pairing $result $newTieBreak")
val newPerf = perfSum + opponent.so(_.rating.value) + result.so:
if _ then 500 else -500
newTieBreak -> newPerf
p.copy(
tieBreak = Swiss.TieBreak(tieBreak),
performance = playerPairings.nonEmpty option Swiss.Performance(perfSum / playerPairings.size)
).recomputeScore
26 changes: 19 additions & 7 deletions modules/swiss/src/main/SwissSheet.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import lila.db.dsl.{ *, given }
private case class SwissSheet(outcomes: List[SwissSheet.Outcome]):
import SwissSheet.*

def points = SwissPoints fromDouble {
outcomes.foldLeft(0) { case (acc, out) => acc + pointsFor(out).doubled }
}
def points = SwissPoints.fromDoubled:
outcomes.foldLeft(0): (acc, out) =>
acc + pointsFor(out).doubled

private object SwissSheet:

Expand All @@ -29,28 +29,40 @@ private object SwissSheet:

import Outcome.*

def pointsFor(outcome: Outcome) = SwissPoints fromDouble {
def pointsFor(outcome: Outcome) = SwissPoints.fromDoubled:
outcome match
case Win | Bye | ForfeitWin => 2
case Late | Draw => 1
case _ => 0
}

def many(
swiss: Swiss,
players: List[SwissPlayer],
pairingMap: SwissPairing.PairingMap
): List[SwissSheet] =
many(swiss.allRounds, players, pairingMap)

def many(
rounds: List[SwissRoundNumber],
players: List[SwissPlayer],
pairingMap: SwissPairing.PairingMap
): List[SwissSheet] =
players.map: player =>
one(swiss, ~pairingMap.get(player.userId), player)
one(rounds, ~pairingMap.get(player.userId), player)

def one(
swiss: Swiss,
pairingMap: Map[SwissRoundNumber, SwissPairing],
player: SwissPlayer
): SwissSheet = one(swiss.allRounds, pairingMap, player)

def one(
rounds: List[SwissRoundNumber],
pairingMap: Map[SwissRoundNumber, SwissPairing],
player: SwissPlayer
): SwissSheet =
SwissSheet:
swiss.allRounds.map: round =>
rounds.map: round =>
pairingMap get round match
case Some(pairing) =>
pairing.status match
Expand Down
2 changes: 1 addition & 1 deletion modules/swiss/src/main/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ private type IdPlayers = Map[Int, UserId]

opaque type SwissPoints = Int
object SwissPoints:
def fromDouble(d: Int): SwissPoints = d
def fromDoubled(d: Int): SwissPoints = d
extension (p: SwissPoints)
def doubled: Int = p
def value: Float = p / 2f
Expand Down
55 changes: 55 additions & 0 deletions modules/swiss/src/test/SwissScoringTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package lila.swiss

class SwissScoringTest extends munit.FunSuite:

import SwissScoring.*

def compute(nbRounds: Int, players: List[SwissPlayer], pairings: List[SwissPairing]) =
val rounds = SwissRoundNumber from (1 to nbRounds).toList
val pairingMap = SwissPairing.toMap(pairings)
val sheets = SwissSheet.many(rounds, players, pairingMap)
val withPoints = (players zip sheets).map: (player, sheet) =>
player.copy(points = sheet.points)
computePlayers(withPoints, pairingMap)

test("empty"):
assertEquals(compute(1, Nil, Nil), Nil)

test("one round"):
val players = List(
makePlayer('a'),
makePlayer('b')
)
val pairings = List(
makePairing(1, 'a', 'b', "1-0")
)
compute(1, players, pairings) match
case List(pa, pb) =>
assertEquals(pa.points.value, 1f)
assertEquals(pb.points.value, 0f)
case _ => fail("expected 2 players")

def makeUserId(name: Char) = UserId(s"user-$name")
def makeSwissId = SwissId("swissId")
def makePlayer(name: Char) = SwissPlayer(
id = SwissPlayer.Id(s"swissId:${makeUserId(name)}"),
swissId = makeSwissId,
userId = makeUserId(name),
rating = IntRating(1500),
provisional = RatingProvisional.No,
points = SwissPoints.fromDoubled(0),
tieBreak = Swiss.TieBreak(0),
performance = None,
score = Swiss.Score(0),
absent = false,
byes = Set.empty
)
def makePairing(round: Int, white: Char, black: Char, outcome: String) = SwissPairing(
id = GameId(s"game-$round$white$black"),
swissId = makeSwissId,
round = SwissRoundNumber(round),
white = makeUserId(white),
black = makeUserId(black),
status = Right(chess.Outcome.fromResult(outcome).flatMap(_.winner).pp),
isForfeit = false
)

0 comments on commit c9eec5d

Please sign in to comment.