lila/modules/playban/src/main/model.scala

143 lines
4.0 KiB
Scala

package lila.playban
import org.joda.time.DateTime
import play.api.libs.json._
import lila.common.Json.jodaWrites
case class UserRecord(
_id: String,
o: Option[Vector[Outcome]],
b: Option[Vector[TempBan]],
c: Option[RageSit]
) {
def userId = _id
def outcomes: Vector[Outcome] = ~o
def bans: Vector[TempBan] = ~b
def rageSit = c | RageSit.empty
def banInEffect = bans.lastOption.exists(_.inEffect)
def banMinutes = bans.lastOption.map(_.remainingMinutes)
def nbOutcomes = outcomes.size
def badOutcomeScore: Float =
outcomes.collect {
case Outcome.NoPlay | Outcome.Abort => .7f
case o if o != Outcome.Good => 1
} sum
def badOutcomeRatio: Float = if (bans.sizeIs < 3) 0.4f else 0.3f
def minBadOutcomes: Int =
bans.size match {
case 0 | 1 => 4
case 2 | 3 => 3
case _ => 2
}
def badOutcomesStreakSize: Int =
bans.size match {
case 0 => 6
case 1 | 2 => 5
case _ => 4
}
def bannable(accountCreationDate: DateTime): Option[TempBan] = {
rageSitRecidive || {
outcomes.lastOption.exists(_ != Outcome.Good) && {
// too many bad overall
badOutcomeScore >= (badOutcomeRatio * nbOutcomes atLeast minBadOutcomes.toFloat) || {
// bad result streak
outcomes.sizeIs >= badOutcomesStreakSize &&
outcomes.takeRight(badOutcomesStreakSize).forall(Outcome.Good !=)
}
}
}
} option TempBan.make(bans, accountCreationDate)
def rageSitRecidive =
outcomes.lastOption.exists(Outcome.rageSitLike.contains) && {
rageSit.isTerrible || {
rageSit.isVeryBad && outcomes.count(Outcome.rageSitLike.contains) > 1
} || {
rageSit.isBad && outcomes.count(Outcome.rageSitLike.contains) > 2
}
}
}
case class TempBan(
date: DateTime,
mins: Int
) {
def endsAt = date plusMinutes mins
def remainingSeconds: Int = (endsAt.getSeconds - nowSeconds).toInt atLeast 0
def remainingMinutes: Int = (remainingSeconds / 60) atLeast 1
def inEffect = endsAt.isAfterNow
}
object TempBan {
implicit val tempbanWrites = Json.writes[TempBan]
private def make(minutes: Int) =
TempBan(
DateTime.now,
minutes atMost 3 * 24 * 60
)
private val baseMinutes = 10
/** Create a playban. First offense: 10 min.
* Multiplier of repeat offense after X days:
* - 0 days: 3x
* - 0 - 3 days: linear scale from 3x to 1x
* - >3 days quick drop off
* Account less than 3 days old --> 2x the usual time
*/
def make(bans: Vector[TempBan], accountCreationDate: DateTime): TempBan =
make {
(bans.lastOption ?? { prev =>
prev.endsAt.toNow.getStandardHours.toSaturatedInt match {
case h if h < 72 => prev.mins * (132 - h) / 60
case h => (55.6 * prev.mins / (Math.pow(5.56 * prev.mins - 54.6, h / 720) + 54.6)).toInt
}
} atLeast baseMinutes) * (if (accountCreationDate.plusDays(3).isAfterNow) 2 else 1)
}
}
sealed abstract class Outcome(
val id: Int,
val name: String
) {
val key = s"${toString.head.toLower}${toString.tail}"
}
object Outcome {
case object Good extends Outcome(0, "Nothing unusual")
case object Abort extends Outcome(1, "Aborts the game")
case object NoPlay extends Outcome(2, "Won't play a move")
case object RageQuit extends Outcome(3, "Quits without resigning")
case object Sitting extends Outcome(4, "Lets time run out")
case object SitMoving extends Outcome(5, "Waits then moves at last moment")
case object Sandbag extends Outcome(6, "Deliberately lost the game")
case object SitResign extends Outcome(7, "Waits then resigns at last moment")
val rageSitLike: Set[Outcome] = Set(RageQuit, Sitting, SitMoving, SitResign)
val all = List(Good, Abort, NoPlay, RageQuit, Sitting, SitMoving, SitResign, Sandbag)
val byId = all map { v =>
(v.id, v)
} toMap
def apply(id: Int): Option[Outcome] = byId get id
}