store tournament player color history in heap

pull/7079/head
Thibault Duplessis 2020-08-04 17:10:44 +02:00
parent 8ddc252400
commit 95776f920b
10 changed files with 82 additions and 83 deletions

View File

@ -136,8 +136,7 @@ object BSONHandlers {
score = r intD "s",
fire = r boolD "f",
performance = r intD "e",
team = r strO "t",
colorHistory = ColorHistory(r intO "c")
team = r strO "t"
)
def writes(w: BSON.Writer, o: Player) =
$doc(
@ -151,8 +150,7 @@ object BSONHandlers {
"m" -> o.magicScore,
"f" -> w.boolO(o.fire),
"e" -> o.performance,
"t" -> o.team,
"c" -> o.colorHistory.toInt
"t" -> o.team
)
}

View File

@ -1,15 +1,14 @@
package lila.tournament
import chess.Color
import scala.concurrent.duration._
import lila.memo.CacheApi
//positive strike -> user played straight strike games by white pieces
//negative strike -> black pieces
class ColorHistory private (val strike: Int, val balance: Int) extends Ordered[ColorHistory] {
import ColorHistory.{ hi, lo }
def toInt = ((strike - lo) << 16) | (balance - lo)
override def hashCode = toInt
override def equals(other: Any) =
other match {
case that: ColorHistory => strike == that.strike && balance == that.balance
case _ => false
}
case class ColorHistory(strike: Int, balance: Int) extends Ordered[ColorHistory] {
override def compare(that: ColorHistory): Int = {
if (strike < that.strike) -1
else if (strike > that.strike) 1
@ -17,42 +16,40 @@ class ColorHistory private (val strike: Int, val balance: Int) extends Ordered[C
else if (balance > that.balance) 1
else 0
}
def firstGetsWhite(that: ColorHistory)(firstGetsWhite: () => Boolean) = {
def firstGetsWhite(that: ColorHistory)(fallback: () => Boolean) = {
val c = compare(that)
c < 0 || (c == 0 && firstGetsWhite())
}
//value > 0 -> user plays by white pieces
//value < 0 -> user plays by black pieces
//returns packed value after updating color history
def incColor(value: Int): ColorHistory = {
if (value > 0) {
new ColorHistory((strike + 1).max(1).min(hi), (balance + 1).min(hi))
} else {
new ColorHistory((strike - 1).min(-1).max(lo), (balance - 1).max(lo))
}
c < 0 || (c == 0 && fallback())
}
def inc(color: Color): ColorHistory =
copy(
strike = color.fold((strike + 1) atLeast 1, (strike - 1) atMost -1),
balance = balance + color.fold(1, -1)
)
//couldn't play if both players played maxStrike blacks games before
//or both player maxStrike games before
def couldPlay(that: ColorHistory, maxStrike: Int): Boolean = {
def couldPlay(that: ColorHistory, maxStrike: Int): Boolean =
(strike > -maxStrike || that.strike > -maxStrike) &&
(strike < maxStrike || that.strike < maxStrike)
}
(strike < maxStrike || that.strike < maxStrike)
//add some penalty for pairs when both players have played last game with same color
//heuristics: after such pairing one streak will be always incremented
def sameColors(that: ColorHistory): Boolean = strike.sign * that.strike.sign > 0
}
object ColorHistory {
private val lo = -0x8000
private val hi = 0x7fff
private val mask = 0xffff
val empty = new ColorHistory(0, 0)
def apply(o: Option[Int]): ColorHistory = {
o match {
case Some(v) => new ColorHistory((v >>> 16) + lo, (v & mask) + lo)
case None => empty
}
}
def minValue: ColorHistory = apply(Some(0))
def maxValue: ColorHistory = apply(Some(-1))
case class PlayerWithColorHistory(player: Player, colorHistory: ColorHistory)
final class ColorHistoryApi(cacheApi: CacheApi) {
private val cache = cacheApi.scaffeine
.expireAfterAccess(1 hour)
.build[Player.ID, ColorHistory]()
def default = ColorHistory(0, 0)
def get(playerId: Player.ID) = cache.getIfPresent(playerId) | default
def inc(playerId: Player.ID, color: Color) = cache.put(playerId, get(playerId) inc color)
}

View File

@ -86,6 +86,8 @@ final class Env(
indexLeaderboard = leaderboardIndexer.indexOne _
)
private lazy val colorHistoryApi = wire[ColorHistoryApi]
lazy val api: TournamentApi = wire[TournamentApi]
lazy val crudApi = wire[crud.CrudApi]

View File

@ -84,8 +84,9 @@ private[tournament] object Pairing {
if (firstGetsWhite) make(gameId, tourId, user1, user2)
else make(gameId, tourId, user2, user1)
}
def prepWithColor(tour: Tournament, p1: Player, p2: Player) =
def prepWithColor(tour: Tournament, p1: RankedPlayerWithColorHistory, p2: RankedPlayerWithColorHistory) =
if (p1.colorHistory.firstGetsWhite(p2.colorHistory)(() => scala.util.Random.nextBoolean()))
Prep(tour.id, p1.userId, p2.userId)
else Prep(tour.id, p2.userId, p1.userId)
Prep(tour.id, p1.player.userId, p2.player.userId)
else Prep(tour.id, p2.player.userId, p1.player.userId)
}

View File

@ -17,8 +17,7 @@ private[tournament] case class Player(
score: Int = 0,
fire: Boolean = false,
performance: Int = 0,
team: Option[TeamID] = None,
colorHistory: ColorHistory = ColorHistory.empty
team: Option[TeamID] = None
) {
def id = _id

View File

@ -34,6 +34,7 @@ final class TournamentApi(
tellRound: lila.round.TellRound,
roundSocket: lila.round.RoundSocket,
trophyApi: lila.user.TrophyApi,
colorHistoryApi: ColorHistoryApi,
verify: Condition.Verify,
duelStore: DuelStore,
pause: Pause,
@ -438,7 +439,7 @@ final class TournamentApi(
)(userId: User.ID): Funit =
(tour.perfType.ifTrue(tour.mode.rated) ?? { userRepo.perfOf(userId, _) }) flatMap { perf =>
playerRepo.update(tour.id, userId) { player =>
cached.sheet.update(tour, userId) map { sheet =>
cached.sheet.update(tour, userId).map { sheet =>
player.copy(
score = sheet.total,
fire = tour.streakable && sheet.onFire,
@ -453,17 +454,10 @@ final class TournamentApi(
} yield Math.round {
player.performance * (nbGames - 1) / nbGames + performance / nbGames
} toInt
} | player.performance,
colorHistory = {
for {
g <- finishing
whiteUserId <- g.whitePlayer.userId
if g.blackPlayer.userId.nonEmpty
} yield {
player.colorHistory.incColor(if (player.is(whiteUserId)) 1 else -1)
}
} | player.colorHistory
} | player.performance
)
} >>- finishing.flatMap(_.whitePlayer.userId).foreach { whiteUserId =>
colorHistoryApi.inc(player.id, chess.Color(player is whiteUserId))
}
}
}

View File

@ -6,9 +6,12 @@ import lila.user.User
import PairingSystem.Data
private object AntmaPairing {
private[this] val maxStrike = 3
def apply(data: Data, players: RankedPlayers): List[Pairing.Prep] =
private type RPlayer = RankedPlayerWithColorHistory
def apply(data: Data, players: List[RPlayer]): List[Pairing.Prep] =
players.nonEmpty ?? {
import data._
@ -18,10 +21,10 @@ private object AntmaPairing {
lastOpponents.hash.get(u1).contains(u2) ||
lastOpponents.hash.get(u2).contains(u1)
def pairScore(a: RankedPlayer, b: RankedPlayer): Option[Int] =
def pairScore(a: RPlayer, b: RPlayer): Option[Int] =
if (
justPlayedTogether(a.player.userId, b.player.userId) ||
!a.player.colorHistory.couldPlay(b.player.colorHistory, maxStrike)
!a.colorHistory.couldPlay(b.colorHistory, maxStrike)
) None
else
Some {
@ -29,10 +32,10 @@ private object AntmaPairing {
Math.abs(a.player.rating - b.player.rating)
}
def battleScore(a: RankedPlayer, b: RankedPlayer): Option[Int] =
def battleScore(a: RPlayer, b: RPlayer): Option[Int] =
(a.player.team != b.player.team) ?? pairScore(a, b)
def duelScore: (RankedPlayer, RankedPlayer) => Option[Int] = (_, _) => Some(1)
def duelScore: (RPlayer, RPlayer) => Option[Int] = (_, _) => Some(1)
Chronometer.syncMon(_.tournament.pairing.wmmatching) {
WMMatching(
@ -46,7 +49,7 @@ private object AntmaPairing {
Nil
},
_ map {
case (a, b) => Pairing.prepWithColor(tour, a.player, b.player)
case (a, b) => Pairing.prepWithColor(tour, a, b)
}
)
}

View File

@ -6,7 +6,8 @@ import lila.user.{ User, UserRepo }
final private[tournament] class PairingSystem(
pairingRepo: PairingRepo,
playerRepo: PlayerRepo,
userRepo: UserRepo
userRepo: UserRepo,
colorHistoryApi: ColorHistoryApi
)(implicit
ec: scala.concurrent.ExecutionContext,
idGenerator: lila.game.IdGenerator
@ -74,13 +75,15 @@ final private[tournament] class PairingSystem(
}
}
private def proximityPairings(tour: Tournament, players: RankedPlayers): List[Pairing.Prep] =
players grouped 2 collect {
case List(p1, p2) => Pairing.prepWithColor(tour, p1.player, p2.player)
private def proximityPairings(tour: Tournament, players: List[RankedPlayer]): List[Pairing.Prep] =
addColorHistory(players) grouped 2 collect {
case List(p1, p2) => Pairing.prepWithColor(tour, p1, p2)
} toList
private def bestPairings(data: Data, players: RankedPlayers): List[Pairing.Prep] =
(players.size > 1) ?? AntmaPairing(data, players)
(players.size > 1) ?? AntmaPairing(data, addColorHistory(players))
private def addColorHistory(players: RankedPlayers) = players.map(_ withColorHistory colorHistoryApi.get)
}
private object PairingSystem {
@ -103,7 +106,9 @@ private object PairingSystem {
* top rank factor = 2000
* bottom rank factor = 300
*/
def rankFactorFor(players: RankedPlayers): (RankedPlayer, RankedPlayer) => Int = {
def rankFactorFor(
players: List[RankedPlayerWithColorHistory]
): (RankedPlayerWithColorHistory, RankedPlayerWithColorHistory) => Int = {
val maxRank = players.maxBy(_.rank).rank
(a, b) => {
val rank = Math.min(a.rank, b.rank)

View File

@ -77,6 +77,9 @@ case class RankedPlayer(rank: Int, player: Player) {
def is(other: RankedPlayer) = player is other.player
def withColorHistory(getHistory: Player.ID => ColorHistory) =
RankedPlayerWithColorHistory(rank, player, getHistory(player.id))
override def toString = s"$rank. ${player.userId}[${player.rating}]"
}
@ -88,6 +91,13 @@ object RankedPlayer {
}
}
case class RankedPlayerWithColorHistory(rank: Int, player: Player, colorHistory: ColorHistory) {
def is(other: RankedPlayer) = player is other.player
override def toString = s"$rank. ${player.userId}[${player.rating}]"
}
case class FeaturedGame(
game: lila.game.Game,
white: RankedPlayer,

View File

@ -3,10 +3,10 @@ import org.specs2.mutable.Specification
object ColorHistoryTest {
def apply(s: String): ColorHistory = {
s.foldLeft(ColorHistory(None)) { (acc, c) =>
s.foldLeft(ColorHistory(0, 0)) { (acc, c) =>
c match {
case 'W' => acc.incColor(1)
case 'B' => acc.incColor(-1)
case 'W' => acc.inc(chess.White)
case 'B' => acc.inc(chess.Black)
}
}
}
@ -21,7 +21,7 @@ object ColorHistoryTest {
}
class ColorHistoryTest extends Specification {
import ColorHistoryTest.{ apply, couldPlay, firstGetsWhite, sameColors, toTuple2, unpack }
import ColorHistoryTest.{ apply, couldPlay, firstGetsWhite, sameColors, unpack }
"arena tournament color history" should {
"hand tests" in {
unpack("WWW") must be equalTo ((3, 3))
@ -47,16 +47,6 @@ class ColorHistoryTest extends Specification {
firstGetsWhite("WW", "BWW") must beFalse
firstGetsWhite("BB", "WBB") must beTrue
}
"serialization" in {
toTuple2(ColorHistory(Some(-1))) must be equalTo ((0x7fff, 0x7fff))
toTuple2(ColorHistory(Some(0))) must be equalTo ((-0x8000, -0x8000))
}
"min/(max)Value incColor" in {
val minh = ColorHistory.minValue
toTuple2(minh.incColor(-1)) must be equalTo toTuple2(minh)
val maxh = ColorHistory.maxValue
toTuple2(maxh.incColor(1)) must be equalTo toTuple2(maxh)
}
"equals" in {
apply("") must be equalTo apply("")
apply("WBW") must be equalTo apply("W")