playban WIP
parent
c723fbd2ca
commit
cdf23799cd
|
@ -6,8 +6,10 @@ import reactivemongo.bson.Macros
|
||||||
import reactivemongo.core.commands._
|
import reactivemongo.core.commands._
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
|
import chess.Color
|
||||||
import lila.db.BSON._
|
import lila.db.BSON._
|
||||||
import lila.db.Types.Coll
|
import lila.db.Types.Coll
|
||||||
|
import lila.game.{ Pov, Game, Player, Source }
|
||||||
|
|
||||||
final class PlaybanApi(coll: Coll) {
|
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 read(bsonInt: BSONInteger): Outcome = Outcome(bsonInt.value) err s"No such playban outcome: ${bsonInt.value}"
|
||||||
def write(x: Outcome) = BSONInteger(x.id)
|
def write(x: Outcome) = BSONInteger(x.id)
|
||||||
}
|
}
|
||||||
|
private implicit val banBSONHandler = Macros.handler[TempBan]
|
||||||
private implicit val UserRecordBSONHandler = Macros.handler[UserRecord]
|
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(
|
FindAndModify(
|
||||||
collection = coll.name,
|
collection = coll.name,
|
||||||
query = BSONDocument("_id" -> userId),
|
query = BSONDocument("_id" -> userId),
|
||||||
modify = Update(
|
modify = Update(
|
||||||
update = BSONDocument("$push" -> BSONDocument(
|
update = BSONDocument("$push" -> BSONDocument(
|
||||||
"h" -> BSONDocument(
|
"o" -> BSONDocument(
|
||||||
"$each" -> List(outcome),
|
"$each" -> List(outcome),
|
||||||
"$slice" -> -30)
|
"$slice" -> -20)
|
||||||
)),
|
)),
|
||||||
fetchNewObject = true),
|
fetchNewObject = true),
|
||||||
upsert = true
|
upsert = true
|
||||||
)
|
)
|
||||||
} map2 UserRecordBSONHandler.read flatMap {
|
} map2 UserRecordBSONHandler.read flatMap {
|
||||||
case None => fufail(s"can't find record for user $userId")
|
case None => fufail(s"can't find record for user $userId")
|
||||||
case Some(userRecord) => funit
|
case Some(record) => legiferate(record)
|
||||||
} logFailure "PlaybanApi"
|
} 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
|
package lila.playban
|
||||||
|
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
case class UserRecord(
|
case class UserRecord(
|
||||||
_id: String,
|
_id: String,
|
||||||
h: List[Outcome]) {
|
o: List[Outcome],
|
||||||
|
b: List[TempBan]) {
|
||||||
|
|
||||||
def userId = _id
|
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(
|
sealed abstract class Outcome(
|
||||||
|
|
|
@ -19,6 +19,7 @@ final class Env(
|
||||||
ai: lila.ai.Client,
|
ai: lila.ai.Client,
|
||||||
aiPerfApi: lila.ai.AiPerfApi,
|
aiPerfApi: lila.ai.AiPerfApi,
|
||||||
crosstableApi: lila.game.CrosstableApi,
|
crosstableApi: lila.game.CrosstableApi,
|
||||||
|
playban: lila.playban.PlaybanApi,
|
||||||
lightUser: String => Option[lila.common.LightUser],
|
lightUser: String => Option[lila.common.LightUser],
|
||||||
userJsonView: lila.user.JsonView,
|
userJsonView: lila.user.JsonView,
|
||||||
uciMemo: lila.game.UciMemo,
|
uciMemo: lila.game.UciMemo,
|
||||||
|
@ -113,6 +114,7 @@ final class Env(
|
||||||
perfsUpdater = perfsUpdater,
|
perfsUpdater = perfsUpdater,
|
||||||
aiPerfApi = aiPerfApi,
|
aiPerfApi = aiPerfApi,
|
||||||
crosstableApi = crosstableApi,
|
crosstableApi = crosstableApi,
|
||||||
|
playban = playban,
|
||||||
bus = system.lilaBus,
|
bus = system.lilaBus,
|
||||||
timeline = hub.actor.timeline,
|
timeline = hub.actor.timeline,
|
||||||
casualOnly = CasualOnly)
|
casualOnly = CasualOnly)
|
||||||
|
@ -187,6 +189,7 @@ object Env {
|
||||||
ai = lila.ai.Env.current.client,
|
ai = lila.ai.Env.current.client,
|
||||||
aiPerfApi = lila.ai.Env.current.aiPerfApi,
|
aiPerfApi = lila.ai.Env.current.aiPerfApi,
|
||||||
crosstableApi = lila.game.Env.current.crosstableApi,
|
crosstableApi = lila.game.Env.current.crosstableApi,
|
||||||
|
playban = lila.playban.Env.current.api,
|
||||||
lightUser = lila.user.Env.current.lightUser,
|
lightUser = lila.user.Env.current.lightUser,
|
||||||
userJsonView = lila.user.Env.current.jsonView,
|
userJsonView = lila.user.Env.current.jsonView,
|
||||||
uciMemo = lila.game.Env.current.uciMemo,
|
uciMemo = lila.game.Env.current.uciMemo,
|
||||||
|
|
|
@ -9,12 +9,14 @@ import lila.game.actorApi.{ FinishGame, AbortedBy }
|
||||||
import lila.game.tube.gameTube
|
import lila.game.tube.gameTube
|
||||||
import lila.game.{ GameRepo, Game, Pov, Event }
|
import lila.game.{ GameRepo, Game, Pov, Event }
|
||||||
import lila.i18n.I18nKey.{ Select => SelectI18nKey }
|
import lila.i18n.I18nKey.{ Select => SelectI18nKey }
|
||||||
|
import lila.playban.{ PlaybanApi, Outcome }
|
||||||
import lila.user.tube.userTube
|
import lila.user.tube.userTube
|
||||||
import lila.user.{ User, UserRepo, Perfs }
|
import lila.user.{ User, UserRepo, Perfs }
|
||||||
|
|
||||||
private[round] final class Finisher(
|
private[round] final class Finisher(
|
||||||
messenger: Messenger,
|
messenger: Messenger,
|
||||||
perfsUpdater: PerfsUpdater,
|
perfsUpdater: PerfsUpdater,
|
||||||
|
playban: PlaybanApi,
|
||||||
aiPerfApi: lila.ai.AiPerfApi,
|
aiPerfApi: lila.ai.AiPerfApi,
|
||||||
crosstableApi: lila.game.CrosstableApi,
|
crosstableApi: lila.game.CrosstableApi,
|
||||||
bus: lila.common.Bus,
|
bus: lila.common.Bus,
|
||||||
|
@ -22,6 +24,7 @@ private[round] final class Finisher(
|
||||||
casualOnly: Boolean) {
|
casualOnly: Boolean) {
|
||||||
|
|
||||||
def abort(pov: Pov): Fu[Events] = apply(pov.game, _.Aborted) addEffect { _ =>
|
def abort(pov: Pov): Fu[Events] = apply(pov.game, _.Aborted) addEffect { _ =>
|
||||||
|
playban.abort(pov)
|
||||||
bus.publish(AbortedBy(pov), 'abortGame)
|
bus.publish(AbortedBy(pov), 'abortGame)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -203,7 +203,7 @@ object ApplicationBuild extends Build {
|
||||||
libraryDependencies ++= provided(play.api, RM, PRM)
|
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)
|
libraryDependencies ++= provided(play.api, RM, PRM)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue