alternative sandbag detection and auto-message
parent
c86519b9e6
commit
15b8005ade
|
@ -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)
|
||||
)
|
||||
|
||||
|
|
|
@ -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" /> -->
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue