alternative sandbag detection and auto-message

pull/3596/head
Thibault Duplessis 2017-09-11 23:48:58 -05:00
parent c86519b9e6
commit 15b8005ade
5 changed files with 93 additions and 1 deletions

View File

@ -296,7 +296,7 @@ lazy val practice = module("practice", Seq(common, db, memo, user, study)).setti
libraryDependencies ++= provided(play.api, reactivemongo.driver)
)
lazy val playban = module("playban", Seq(common, db, game)).settings(
lazy val playban = module("playban", Seq(common, db, game, message)).settings(
libraryDependencies ++= provided(play.api, reactivemongo.driver)
)

View File

@ -162,6 +162,16 @@
</rollingPolicy>
</appender>
</logger>
<logger name="sandbag" level="DEBUG">
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/lichess/sandbag.log</file>
<encoder><pattern>%date %-5level %logger{30} %message%n%xException</pattern></encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/lichess/sandbag-log-%d{yyyy-MM-dd}.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
</appender>
</logger>
<!-- Set a specific actor to DEBUG -->
<!-- <logger name="actors.MyActor" level="DEBUG" /> -->

View File

@ -565,6 +565,8 @@ case class Game(
def pov(c: Color) = Pov(this, c)
def whitePov = pov(White)
def blackPov = pov(Black)
def playerPov(p: Player) = pov(p.color)
def loserPov = loser map playerPov
}
object Game {

View File

@ -106,6 +106,14 @@ Our cheating detection algorithms have marked your account for using computer as
none
}
lazy val sandbagAuto = ModPreset(
subject = "Warning: possible sandbagging",
text = """You have lost a couple games after a few moves. Please note that you MUST try to win every rated game.
Losing rated games on purpose is called "sandbagging", and is not allowed on lichess.
Thank you for your understanding."""
)
lazy val asJson = play.api.libs.json.Json.toJson {
all.map { p =>
List(p.subject, p.text)

View File

@ -0,0 +1,72 @@
package lila.playban
import com.github.blemale.scaffeine.{ Cache, Scaffeine }
import scala.concurrent.duration._
import lila.game.{ Game, Pov }
import lila.message.{ MessageApi, ModPreset }
import lila.user.{ User, UserRepo }
private final class SandbagWatch(messenger: MessageApi) {
import SandbagWatch._
def apply(game: Game): Funit = (game.rated && game.finished) ?? {
game.userIds.map { userId =>
(records getIfPresent userId, isSandbag(game, userId)) match {
case (None, false) => funit
case (Some(record), false) => updateRecord(userId, record + Good)
case (record, true) => updateRecord(userId, (record | newRecord) + Sandbag)
}
}.sequenceFu.void
}
private def sendMessage(userId: User.ID): Funit = for {
mod <- UserRepo.lichess
user <- UserRepo byId userId
} yield (mod zip user).headOption.?? {
case (m, u) =>
lila.log("sandbag").info(s"https://lichess.org/@/${u.username}")
messenger.sendPreset(m, u, ModPreset.sandbagAuto).void
}
private def updateRecord(userId: User.ID, record: Record) =
if (record.immaculate) fuccess(records invalidate userId)
else {
records.put(userId, record)
if (record.latestIsSandbag && record.outcomes.size > 1) sendMessage(userId)
else funit
}
private val records: Cache[User.ID, Record] = Scaffeine()
.expireAfterWrite(6 hours)
.build[User.ID, Record]
private def isSandbag(game: Game, userId: User.ID): Boolean =
game.playerByUserId(userId).exists { player =>
game.opponent(player).wins && game.turns <= {
if (game.variant == chess.variant.Atomic) 3
else 5
}
}
}
private object SandbagWatch {
sealed trait Outcome
case object Good extends Outcome
case object Sandbag extends Outcome
val maxOutcomes = 5
case class Record(outcomes: List[Outcome]) {
def +(outcome: Outcome) = copy(outcomes = outcome :: outcomes.take(maxOutcomes - 1))
def latestIsSandbag = outcomes.headOption.exists(_ == Sandbag)
def immaculate = outcomes.size == maxOutcomes && outcomes.forall(Good.==)
}
val newRecord = Record(Nil)
}