playban WIP
parent
c723fbd2ca
commit
cdf23799cd
|
@ -6,8 +6,10 @@ import reactivemongo.bson.Macros
|
|||
import reactivemongo.core.commands._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import chess.Color
|
||||
import lila.db.BSON._
|
||||
import lila.db.Types.Coll
|
||||
import lila.game.{ Pov, Game, Player, Source }
|
||||
|
||||
final class PlaybanApi(coll: Coll) {
|
||||
|
||||
|
@ -17,23 +19,72 @@ final class PlaybanApi(coll: Coll) {
|
|||
def read(bsonInt: BSONInteger): Outcome = Outcome(bsonInt.value) err s"No such playban outcome: ${bsonInt.value}"
|
||||
def write(x: Outcome) = BSONInteger(x.id)
|
||||
}
|
||||
private implicit val banBSONHandler = Macros.handler[TempBan]
|
||||
private implicit val UserRecordBSONHandler = Macros.handler[UserRecord]
|
||||
|
||||
def record(userId: String, outcome: Outcome): Funit = coll.db.command {
|
||||
private case class Blame(player: Player, outcome: Outcome)
|
||||
|
||||
private def blameable(game: Game) = game.source == Source.Lobby && game.hasClock
|
||||
|
||||
def abort(pov: Pov): Funit = blameable(pov.game) ?? {
|
||||
|
||||
val blame =
|
||||
if (pov.game olderThan 45) pov.game.playerWhoDidNotMove map { Blame(_, Outcome.NoPlay) }
|
||||
else if (pov.game olderThan 15) none
|
||||
else pov.player.some map { Blame(_, Outcome.Abort) }
|
||||
|
||||
blame match {
|
||||
case None => pov.game.userIds.map(save(Outcome.Good)).sequenceFu.void
|
||||
case Some(Blame(player, outcome)) =>
|
||||
player.userId.??(save(outcome)) >>
|
||||
pov.game.opponent(player).userId.??(save(Outcome.Good))
|
||||
}
|
||||
}
|
||||
|
||||
def currentBan(userId: String): Fu[Option[TempBan]] = coll.find(
|
||||
BSONDocument("_id" -> userId, "b.0" -> BSONDocument("$exists" -> true)),
|
||||
BSONDocument("_id" -> false, "b" -> BSONDocument("$slice" -> -1))
|
||||
).one[BSONDocument].map {
|
||||
_.flatMap(_.getAs[List[TempBan]]("b")).??(_.find(_.inEffect))
|
||||
}
|
||||
|
||||
def bans(userId: String): Fu[List[TempBan]] = coll.find(
|
||||
BSONDocument("_id" -> userId, "b.0" -> BSONDocument("$exists" -> true)),
|
||||
BSONDocument("_id" -> false, "b" -> true)
|
||||
).one[BSONDocument].map {
|
||||
~_.flatMap(_.getAs[List[TempBan]]("b"))
|
||||
}
|
||||
|
||||
private def save(outcome: Outcome): String => Funit = userId => coll.db.command {
|
||||
FindAndModify(
|
||||
collection = coll.name,
|
||||
query = BSONDocument("_id" -> userId),
|
||||
modify = Update(
|
||||
update = BSONDocument("$push" -> BSONDocument(
|
||||
"h" -> BSONDocument(
|
||||
"o" -> BSONDocument(
|
||||
"$each" -> List(outcome),
|
||||
"$slice" -> -30)
|
||||
"$slice" -> -20)
|
||||
)),
|
||||
fetchNewObject = true),
|
||||
upsert = true
|
||||
)
|
||||
} map2 UserRecordBSONHandler.read flatMap {
|
||||
case None => fufail(s"can't find record for user $userId")
|
||||
case Some(userRecord) => funit
|
||||
case None => fufail(s"can't find record for user $userId")
|
||||
case Some(record) => legiferate(record)
|
||||
} logFailure "PlaybanApi"
|
||||
|
||||
private def legiferate(record: UserRecord): Funit = record.newBan ?? { ban =>
|
||||
loginfo(s"[playban] ban ${record.userId} for {$ban.mins} minutes")
|
||||
coll.update(
|
||||
BSONDocument("_id" -> record.userId),
|
||||
BSONDocument(
|
||||
"$unset" -> BSONDocument("o" -> true),
|
||||
"$push" -> BSONDocument(
|
||||
"b" -> BSONDocument(
|
||||
"$each" -> List(ban),
|
||||
"$slice" -> -30)
|
||||
)
|
||||
)
|
||||
).void
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,52 @@
|
|||
package lila.playban
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
case class UserRecord(
|
||||
_id: String,
|
||||
h: List[Outcome]) {
|
||||
o: List[Outcome],
|
||||
b: List[TempBan]) {
|
||||
|
||||
def userId = _id
|
||||
def history = h
|
||||
def outcomes = o
|
||||
def bans = b
|
||||
|
||||
def banInEffect = bans.lastOption.??(_.inEffect)
|
||||
|
||||
lazy val nbOutcomes = outcomes.size
|
||||
|
||||
lazy val nbBadOutcomes = outcomes.count(_ != Outcome.Good)
|
||||
|
||||
def badOutcomeRatio: Double =
|
||||
if (nbOutcomes == 0) 0
|
||||
else nbBadOutcomes.toDouble / nbOutcomes
|
||||
|
||||
def nbBadOutcomesBeforeBan = if (bans.isEmpty) 5 else 3
|
||||
|
||||
def newBan: Option[TempBan] = {
|
||||
!banInEffect &&
|
||||
nbBadOutcomes >= nbBadOutcomesBeforeBan &&
|
||||
badOutcomeRatio > 1d / 3
|
||||
} option bans.lastOption.filterNot(_.isOld).fold(TempBan(5)) { prev =>
|
||||
TempBan(prev.mins * 2)
|
||||
}
|
||||
}
|
||||
|
||||
case class TempBan(
|
||||
date: DateTime,
|
||||
mins: Int) {
|
||||
|
||||
lazy val endsAt = date plusMinutes mins
|
||||
|
||||
def remainingSeconds: Int = (endsAt.getSeconds - nowSeconds).toInt max 0
|
||||
|
||||
def inEffect = endsAt isBefore DateTime.now
|
||||
|
||||
def isOld = date isBefore DateTime.now.minusDays(1)
|
||||
}
|
||||
|
||||
object TempBan {
|
||||
def apply(minutes: Int): TempBan = TempBan(DateTime.now, minutes min 120)
|
||||
}
|
||||
|
||||
sealed abstract class Outcome(
|
||||
|
|
|
@ -19,6 +19,7 @@ final class Env(
|
|||
ai: lila.ai.Client,
|
||||
aiPerfApi: lila.ai.AiPerfApi,
|
||||
crosstableApi: lila.game.CrosstableApi,
|
||||
playban: lila.playban.PlaybanApi,
|
||||
lightUser: String => Option[lila.common.LightUser],
|
||||
userJsonView: lila.user.JsonView,
|
||||
uciMemo: lila.game.UciMemo,
|
||||
|
@ -113,6 +114,7 @@ final class Env(
|
|||
perfsUpdater = perfsUpdater,
|
||||
aiPerfApi = aiPerfApi,
|
||||
crosstableApi = crosstableApi,
|
||||
playban = playban,
|
||||
bus = system.lilaBus,
|
||||
timeline = hub.actor.timeline,
|
||||
casualOnly = CasualOnly)
|
||||
|
@ -187,6 +189,7 @@ object Env {
|
|||
ai = lila.ai.Env.current.client,
|
||||
aiPerfApi = lila.ai.Env.current.aiPerfApi,
|
||||
crosstableApi = lila.game.Env.current.crosstableApi,
|
||||
playban = lila.playban.Env.current.api,
|
||||
lightUser = lila.user.Env.current.lightUser,
|
||||
userJsonView = lila.user.Env.current.jsonView,
|
||||
uciMemo = lila.game.Env.current.uciMemo,
|
||||
|
|
|
@ -9,12 +9,14 @@ import lila.game.actorApi.{ FinishGame, AbortedBy }
|
|||
import lila.game.tube.gameTube
|
||||
import lila.game.{ GameRepo, Game, Pov, Event }
|
||||
import lila.i18n.I18nKey.{ Select => SelectI18nKey }
|
||||
import lila.playban.{ PlaybanApi, Outcome }
|
||||
import lila.user.tube.userTube
|
||||
import lila.user.{ User, UserRepo, Perfs }
|
||||
|
||||
private[round] final class Finisher(
|
||||
messenger: Messenger,
|
||||
perfsUpdater: PerfsUpdater,
|
||||
playban: PlaybanApi,
|
||||
aiPerfApi: lila.ai.AiPerfApi,
|
||||
crosstableApi: lila.game.CrosstableApi,
|
||||
bus: lila.common.Bus,
|
||||
|
@ -22,6 +24,7 @@ private[round] final class Finisher(
|
|||
casualOnly: Boolean) {
|
||||
|
||||
def abort(pov: Pov): Fu[Events] = apply(pov.game, _.Aborted) addEffect { _ =>
|
||||
playban.abort(pov)
|
||||
bus.publish(AbortedBy(pov), 'abortGame)
|
||||
}
|
||||
|
||||
|
|
|
@ -203,7 +203,7 @@ object ApplicationBuild extends Build {
|
|||
libraryDependencies ++= provided(play.api, RM, PRM)
|
||||
)
|
||||
|
||||
lazy val playban = project("playban", Seq(common, db)).settings(
|
||||
lazy val playban = project("playban", Seq(common, db, game)).settings(
|
||||
libraryDependencies ++= provided(play.api, RM, PRM)
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue