store tournament player color history in heap
parent
8ddc252400
commit
95776f920b
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in New Issue