commit
4f1fbabca1
|
@ -11,7 +11,7 @@ import lila.analyse.{ Analysis, TimeChart, AdvantageChart, Accuracy }
|
|||
import lila.api.Context
|
||||
import lila.app._
|
||||
import lila.common.HTTPRequest
|
||||
import lila.evaluation.GameResults
|
||||
import lila.evaluation.GameAssessments
|
||||
import lila.game.{ Pov, Game => GameModel, GameRepo, PgnDump }
|
||||
import lila.hub.actorApi.map.Tell
|
||||
import lila.round.actorApi.AnalysisAvailable
|
||||
|
@ -66,8 +66,8 @@ object Analyse extends LilaController {
|
|||
if (HTTPRequest.isBot(ctx.req)) divider.empty
|
||||
else divider(pov.game, initialFen)
|
||||
val pgn = Env.game.pgnDump(pov.game, initialFen)
|
||||
val assessResults = if (isGranted(_.MarkEngine)) Env.mod.assessApi.getResultsByGameId(pov.game.id)
|
||||
else fuccess(GameResults(None, None))
|
||||
val assessResults = if (isGranted(_.MarkEngine)) Env.mod.assessApi.getGameResultsById(pov.game.id)
|
||||
else fuccess(GameAssessments(None, None))
|
||||
assessResults flatMap {
|
||||
results =>
|
||||
Env.api.roundApi.watcher(pov, lila.api.Mobile.Api.currentVersion,
|
||||
|
|
|
@ -86,28 +86,6 @@ object Mod extends LilaController {
|
|||
|
||||
def redirect(username: String, mod: Boolean = true) = Redirect(routes.User.show(username).url + mod.??("?mod"))
|
||||
|
||||
def assessGame(id: String, side: String) = SecureBody(_.MarkEngine) { implicit ctx =>
|
||||
me =>
|
||||
import play.api.data.Forms._
|
||||
import play.api.data._
|
||||
implicit def req = ctx.body
|
||||
|
||||
Form(single("assessment" -> number(min = 1, max = 5))).bindFromRequest.fold(
|
||||
err => fuccess(BadRequest),
|
||||
assessment => {
|
||||
val color: Color = Color(side == "white")
|
||||
|
||||
assessApi.createPlayerAssessment(PlayerAssessment(
|
||||
_id = id + "/" + color.name,
|
||||
gameId = id,
|
||||
white = (color == Color.White),
|
||||
assessment = assessment,
|
||||
by = me.id,
|
||||
date = DateTime.now)).void
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def refreshAssess = SecureBody(_.MarkEngine) { implicit ctx =>
|
||||
me =>
|
||||
import play.api.data.Forms._
|
||||
|
|
|
@ -12,7 +12,7 @@ import lila.rating.PerfType
|
|||
import lila.security.Permission
|
||||
import lila.user.tube.userTube
|
||||
import lila.user.{ User => UserModel, UserRepo }
|
||||
import lila.evaluation.PlayerAggregateAssessment
|
||||
import lila.evaluation.{PlayerAggregateAssessment}
|
||||
import views._
|
||||
|
||||
object User extends LilaController {
|
||||
|
@ -147,7 +147,7 @@ object User extends LilaController {
|
|||
|
||||
def mod(username: String) = Secure(_.UserSpy) { implicit ctx =>
|
||||
me => OptionFuOk(UserRepo named username) { user =>
|
||||
(Env.evaluation.evaluator find user) zip (Env.security userSpy user.id) zip (Env.mod.assessApi.getResultsByUserId(user.id, 25)) map {
|
||||
(Env.evaluation.evaluator find user) zip (Env.security userSpy user.id) zip (Env.mod.assessApi.getPlayerAssessmentsByUserId(user.id, 25)) map {
|
||||
case ((eval, spy), gameResults) => html.user.mod(user, spy, eval, PlayerAggregateAssessment(gameResults))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@import lila.evaluation.GameResults
|
||||
@import lila.evaluation.GameAssessments
|
||||
|
||||
@(pov: Pov, data: play.api.libs.json.JsObject, pgn: String, analysis: Option[lila.analyse.Analysis], advantageChart: Option[String], tour: Option[lila.tournament.Tournament], timeChart: lila.analyse.TimeChart, cross: Option[lila.game.Crosstable], userTv: Option[User], division: chess.Division, results: GameResults)(implicit ctx: Context)
|
||||
@(pov: Pov, data: play.api.libs.json.JsObject, pgn: String, analysis: Option[lila.analyse.Analysis], advantageChart: Option[String], tour: Option[lila.tournament.Tournament], timeChart: lila.analyse.TimeChart, cross: Option[lila.game.Crosstable], userTv: Option[User], division: chess.Division, results: GameAssessments)(implicit ctx: Context)
|
||||
|
||||
@import pov._
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@import lila.evaluation.GameResults
|
||||
@import lila.evaluation.{ GameAssessments, Display }
|
||||
|
||||
@(game: Game, results: GameResults)(implicit ctx: Context)
|
||||
@(game: Game, results: GameAssessments)(implicit ctx: Context)
|
||||
<br />
|
||||
@if(isGranted(_.MarkEngine) && game.analysable && !game.isCorrespondence && game.turns >= 40) {
|
||||
@game.players.map { p =>
|
||||
|
@ -9,30 +9,17 @@
|
|||
@playerLink(p, cssClass = s"is color-icon ${p.color.name}".some, withOnline = false, mod = true)
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<select id="@(p.color.name)Assessment" data-val="@(results.color(p.color).fold(0){ _.bestMatch.assessment })">
|
||||
<option value="0"></option>
|
||||
<option value="1">Not cheating</option>
|
||||
<option value="2">Unlikely cheating</option>
|
||||
<option value="3">Unclear</option>
|
||||
<option value="4">Likely cheating</option>
|
||||
<option value="5">Cheating</option>
|
||||
</select>
|
||||
</td>
|
||||
@results.color(p.color).fold{
|
||||
<td class="noMatch">None</td>
|
||||
}{ result =>
|
||||
<td class=@(if(result.bestMatch.positiveMatch) "match" else "partial")>
|
||||
<a href="@routes.Round.watcher(result.bestMatch.gameId, result.bestMatch.color.name)">
|
||||
@(result.bestMatch.matchPercentage)%
|
||||
</a>
|
||||
<td class="match">
|
||||
@Display.assessmentString(result.assessment)
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
</table>
|
||||
}
|
||||
<div class="assessmentButtons">
|
||||
<a class="button" id="confirmAssessment">Confirm</a>
|
||||
<a class="button" id="refreshAssessment">Refresh</a>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
}
|
||||
</div>
|
||||
}
|
||||
@if(aggregateAssessment.gameGroupResults.nonEmpty) {
|
||||
@if(aggregateAssessment.playerAssessments.nonEmpty) {
|
||||
<div class="evaluation results">
|
||||
<table class="reportCard">
|
||||
<thead>
|
||||
|
@ -140,47 +140,21 @@
|
|||
<th>Blurs</th>
|
||||
<th>Bot</th>
|
||||
<th><span class="hint--top-left" data-hint="Aggregate match">Σ</span></th>
|
||||
<th><span class="hint--top-left" data-hint="Primary match">1</span></th>
|
||||
<th><span class="hint--top-left" data-hint="Secondary match">2</span></th>
|
||||
<th><span class="hint--top-left" data-hint="Tertiary match">3</span></th>
|
||||
<th><span class="hint--top-left" data-hint="Quaternary match">4</span></th>
|
||||
<th><span class="hint--top-left" data-hint="Quinary match">5</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@aggregateAssessment.gameGroupResults.map { result =>
|
||||
@aggregateAssessment.playerAssessments.map { result =>
|
||||
<tr>
|
||||
<td><a href="@routes.Round.watcher(result.gameId, result.color.name)">@routes.Round.watcher(result.gameId, result.color.name)</a></td>
|
||||
<td>@result.sfAvg ± @result.sfSd</td>
|
||||
<td>@(result.mtAvg/10) ± @(result.mtSd/10)</td>
|
||||
<td>@(result.blur)%</td>
|
||||
<td>@(result.blurs)%</td>
|
||||
<td>@if(result.hold){Yes} else {No}</td>
|
||||
<td>
|
||||
<span class="aggregate hint--top-left" data-hint="@result.aggregate.explanation">
|
||||
<span class="sig_@(result.aggregate.assessment)" data-icon="@Display.dataIcon(result.aggregate)"></span>
|
||||
<span class="aggregate hint--top-left">
|
||||
<span class="sig_@(result.assessment)" data-icon="J"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a href="@routes.Round.watcher(result.bestMatch.gameId, result.bestMatch.color.name)" class="peerLink hint--top-left" data-hint="@result.bestMatch.explanation">
|
||||
<span class="sig_@(result.bestMatch.assessment)" data-icon="@Display.dataIcon(result.bestMatch)"> </span>
|
||||
<br /><span class="percentage">@(result.bestMatch.matchPercentage)%</span>
|
||||
</a>
|
||||
</td>
|
||||
@Range(0, 4).map { i =>
|
||||
@result.secondaryMatches.lift(i) match {
|
||||
case Some(peer) => {
|
||||
<td>
|
||||
<a href="@routes.Round.watcher(peer.gameId, peer.color.name)" class="peerLink hint--top-left" data-hint="@peer.explanation">
|
||||
<span class="sig_@(peer.assessment)" data-icon="@Display.dataIcon(peer)"></span>
|
||||
<br /><span class="percentage">@(peer.matchPercentage)%</span>
|
||||
</a>
|
||||
</td>
|
||||
}
|
||||
case _ => {
|
||||
<td></td>
|
||||
}
|
||||
}
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
|
|
@ -210,7 +210,6 @@ POST /mod/:username/title controllers.Mod.setTitle(username: String
|
|||
GET /mod/:username/communication controllers.Mod.communication(username: String)
|
||||
POST /mod/:ip/ipban controllers.Mod.ipban(ip: String)
|
||||
GET /mod/log controllers.Mod.log
|
||||
POST /mod/$gameId<\w{8}>/$color<white|black>/assess controllers.Mod.assessGame(gameId: String, color: String)
|
||||
POST /mod/refreshAssess controllers.Mod.refreshAssess
|
||||
|
||||
# Wiki
|
||||
|
|
|
@ -0,0 +1,240 @@
|
|||
package lila.evaluation
|
||||
|
||||
import chess.{ Color }
|
||||
import lila.game.{ Pov, Game }
|
||||
import lila.analyse.{ Accuracy, Analysis }
|
||||
import Math.{ pow, abs, sqrt, E, exp }
|
||||
import org.joda.time.DateTime
|
||||
import scalaz.NonEmptyList
|
||||
|
||||
case class PlayerAssessment(
|
||||
_id: String,
|
||||
gameId: String,
|
||||
userId: String,
|
||||
white: Boolean,
|
||||
assessment: Int, // 1 = Not Cheating, 2 = Unlikely Cheating, 3 = Unknown, 4 = Likely Cheating, 5 = Cheating
|
||||
date: DateTime,
|
||||
// meta
|
||||
sfAvg: Int,
|
||||
sfSd: Int,
|
||||
mtAvg: Int,
|
||||
mtSd: Int,
|
||||
blurs: Int,
|
||||
hold: Boolean
|
||||
) {
|
||||
val color = Color.apply(white)
|
||||
}
|
||||
|
||||
case class PlayerAggregateAssessment(playerAssessments: List[PlayerAssessment]) {
|
||||
import Statistics._
|
||||
|
||||
def countAssessmentValue(assessment: Int) = listSum(playerAssessments map {
|
||||
case a if (a.assessment == assessment) => 1
|
||||
case _ => 0
|
||||
})
|
||||
|
||||
val cheatingSum = countAssessmentValue(5)
|
||||
val likelyCheatingSum = countAssessmentValue(4)
|
||||
|
||||
val markPri = countAssessmentValue(5) >= 2
|
||||
val markSec = countAssessmentValue(5) + countAssessmentValue(4) >= 4
|
||||
|
||||
val reportPri = countAssessmentValue(5) >= 1
|
||||
val reportSec = countAssessmentValue(5) + countAssessmentValue(4) >= 2
|
||||
|
||||
}
|
||||
|
||||
case class GameAssessments(
|
||||
white: Option[PlayerAssessment],
|
||||
black: Option[PlayerAssessment]) {
|
||||
def color(c: Color) = c match {
|
||||
case Color.White => white
|
||||
case _ => black
|
||||
}
|
||||
}
|
||||
|
||||
case class Analysed(game: Game, analysis: Analysis)
|
||||
|
||||
case class Assessible(analysed: Analysed) {
|
||||
import Statistics._
|
||||
|
||||
def moveTimes(color: Color): List[Int] =
|
||||
skip(this.analysed.game.moveTimes.toList, {if (color == Color.White) 0 else 1})
|
||||
|
||||
def suspiciousErrorRate(color: Color): Boolean =
|
||||
listAverage(Accuracy.diffsList(Pov(this.analysed.game, color), this.analysed.analysis)) < 15
|
||||
|
||||
def alwaysHasAdvantage(color: Color): Boolean = {
|
||||
def chartFromDifs(difs: List[(Int, Int)], sum: Int = 0): List[Int] =
|
||||
difs match {
|
||||
case Nil => Nil
|
||||
case a :: Nil => List((sum + a._1), (sum + a._1 + a._2))
|
||||
case a :: b => List((sum + a._1), (sum + a._1 + a._2)) ::: chartFromDifs(b, sum + a._1 + a._2)
|
||||
}
|
||||
|
||||
!chartFromDifs(Accuracy.diffsList(Pov(this.analysed.game, color), this.analysed.analysis) zip
|
||||
Accuracy.diffsList(Pov(this.analysed.game, !color), this.analysed.analysis)).exists(_ < -50)
|
||||
}
|
||||
|
||||
def highBlurRate(color: Color): Boolean =
|
||||
this.analysed.game.playerBlurPercent(color) > 90
|
||||
|
||||
def moderateBlurRate(color: Color): Boolean =
|
||||
this.analysed.game.playerBlurPercent(color) > 70
|
||||
|
||||
def consistentMoveTimes(color: Color): Boolean =
|
||||
moveTimes(color).toNel.map(coefVariation).fold(false)(_ < 0.8)
|
||||
|
||||
def noFastMoves(color: Color): Boolean = !moveTimes(color).exists(_ < 10)
|
||||
|
||||
def suspiciousHoldAlert(color: Color): Boolean =
|
||||
this.analysed.game.player(color).hasSuspiciousHoldAlert
|
||||
|
||||
def rankCheating(color: Color): Int =
|
||||
((
|
||||
suspiciousErrorRate(color),
|
||||
alwaysHasAdvantage(color),
|
||||
highBlurRate(color),
|
||||
moderateBlurRate(color),
|
||||
consistentMoveTimes(color),
|
||||
noFastMoves(color),
|
||||
suspiciousHoldAlert(color)
|
||||
) match {
|
||||
// SF1 SF2 BLR1 BLR2 MTs1 MTs2 Holds
|
||||
case (true, true, true, true, true, true, true) => 5 // all true, obvious cheat
|
||||
case (true, _, _, _, _, true, true) => 5 // high accuracy, no fast moves, hold alerts
|
||||
case (true, _, true, _, _, true, _) => 5 // high accuracy, high blurs, no fast moves
|
||||
case (true, _, _, _, true, true, _) => 5 // high accuracy, consistent move times, no fast moves
|
||||
case (_, true, _, _, _, _, true) => 4 // always has advantage, hold alerts
|
||||
case (_, true, true, _, _, _, _) => 4 // always has advantage, high blurs
|
||||
case (true, _, _, false, false, true, _) => 3 // high accuracy, no fast moves, but doesn't blur or flat line
|
||||
case (true, _, _, _, _, false, _) => 2 // high accuracy, but has fast moves
|
||||
case (false, false, _, _, _, _, _) => 1 // low accuracy, doesn't hold advantage
|
||||
case (false, false, false, false, false, false, false) => 1 // all false, obviously not cheating
|
||||
case _ => 1
|
||||
}).min(this.analysed.game.wonBy(color) match {
|
||||
case Some(c) if (c) => 5
|
||||
case _ => 3
|
||||
})
|
||||
|
||||
def sfAvg(color: Color): Int = listAverage(Accuracy.diffsList(Pov(this.analysed.game, color), this.analysed.analysis)).toInt
|
||||
def sfSd(color: Color): Int = listDeviation(Accuracy.diffsList(Pov(this.analysed.game, color), this.analysed.analysis)).toInt
|
||||
def mtAvg(color: Color): Int = listAverage(skip(this.analysed.game.moveTimes.toList, {if (color == Color.White) 0 else 1})).toInt
|
||||
def mtSd(color: Color): Int = listDeviation(skip(this.analysed.game.moveTimes.toList, {if (color == Color.White) 0 else 1})).toInt
|
||||
def blurs(color: Color): Int = this.analysed.game.playerBlurPercent(color)
|
||||
def hold(color: Color): Boolean = this.analysed.game.player(color).hasSuspiciousHoldAlert
|
||||
|
||||
def playerAssessment(color: Color): PlayerAssessment = PlayerAssessment(
|
||||
_id = this.analysed.game.id + "/" + color.name,
|
||||
gameId = this.analysed.game.id,
|
||||
userId = this.analysed.game.player(color).userId.getOrElse(""),
|
||||
white = (color == Color.White),
|
||||
assessment = rankCheating(color),
|
||||
date = DateTime.now,
|
||||
// meta
|
||||
sfAvg = sfAvg(color),
|
||||
sfSd = sfSd(color),
|
||||
mtAvg = mtAvg(color),
|
||||
mtSd = mtSd(color),
|
||||
blurs = blurs(color),
|
||||
hold = hold(color)
|
||||
)
|
||||
|
||||
val assessments: GameAssessments = GameAssessments(
|
||||
white = Some(playerAssessment(Color.White)),
|
||||
black = Some(playerAssessment(Color.Black)))
|
||||
}
|
||||
|
||||
object Display {
|
||||
def assessmentString(x: Int): String =
|
||||
x match {
|
||||
case 1 => "Not cheating"
|
||||
case 2 => "Unlikely cheating"
|
||||
case 3 => "Unclear"
|
||||
case 4 => "Likely cheating"
|
||||
case 5 => "Cheating"
|
||||
case 6 => "Undef"
|
||||
}
|
||||
}
|
||||
|
||||
object Statistics {
|
||||
import Erf._
|
||||
import scala.annotation._
|
||||
|
||||
def variance[T](a: NonEmptyList[T], optionalAvg: Option[T] = None)(implicit n: Numeric[T]): Double = {
|
||||
val avg: Double = optionalAvg.fold(average(a)){n.toDouble}
|
||||
|
||||
a.map( i => pow(n.toDouble(i) - avg, 2)).list.sum / a.length
|
||||
}
|
||||
|
||||
def deviation[T](a: NonEmptyList[T], optionalAvg: Option[T] = None)(implicit n: Numeric[T]): Double = sqrt(variance(a, optionalAvg))
|
||||
|
||||
def average[T](a: NonEmptyList[T])(implicit n: Numeric[T]): Double = {
|
||||
@tailrec def average(a: List[T], sum: T = n.zero, depth: Int = 0): Double = {
|
||||
a match {
|
||||
case List() => n.toDouble(sum) / depth
|
||||
case x :: xs => average(xs, n.plus(sum, x), depth + 1)
|
||||
}
|
||||
}
|
||||
average(a.list)
|
||||
}
|
||||
|
||||
// Coefficient of Variance
|
||||
def coefVariation(a: NonEmptyList[Int]): Double = sqrt(variance(a)) / average(a)
|
||||
|
||||
def intervalToVariance4(interval: Double): Double = pow(interval / 3, 8) // roughly speaking
|
||||
|
||||
// Accumulative probability function for normal distributions
|
||||
def cdf[T](x: T, avg: T, sd: T)(implicit n: Numeric[T]): Double =
|
||||
0.5 * (1 + erf(n.toDouble(n.minus(x, avg)) / (n.toDouble(sd)*sqrt(2))))
|
||||
|
||||
// The probability that you are outside of abs(x-n) from the mean on both sides
|
||||
def confInterval[T](x: T, avg: T, sd: T)(implicit n: Numeric[T]): Double =
|
||||
1 - cdf(n.abs(x), avg, sd) + cdf(n.times(n.fromInt(-1), n.abs(x)), avg, sd)
|
||||
|
||||
def skip[A](l: List[A], n: Int) =
|
||||
l.zipWithIndex.collect {case (e,i) if ((i+n) % 2) == 0 => e} // (i+1) because zipWithIndex is 0-based
|
||||
|
||||
def listSum(xs: List[Int]): Int = xs match {
|
||||
case Nil => 0
|
||||
case x :: tail => x + listSum(tail)
|
||||
}
|
||||
|
||||
def listSum(xs: List[Double]): Double = xs match {
|
||||
case Nil => 0
|
||||
case x :: tail => x + listSum(tail)
|
||||
}
|
||||
|
||||
def listAverage[T](x: List[T])(implicit n: Numeric[T]): Double = x match {
|
||||
case Nil => 0
|
||||
case a :: Nil => n.toDouble(a)
|
||||
case a :: b => average(NonEmptyList.nel(a, b))
|
||||
}
|
||||
|
||||
def listDeviation[T](x: List[T])(implicit n: Numeric[T]): Double = x match {
|
||||
case Nil => 0
|
||||
case _ :: Nil => 0
|
||||
case a :: b => deviation(NonEmptyList.nel(a, b))
|
||||
}
|
||||
}
|
||||
|
||||
object Erf {
|
||||
// constants
|
||||
val a1: Double = 0.254829592
|
||||
val a2: Double = -0.284496736
|
||||
val a3: Double = 1.421413741
|
||||
val a4: Double = -1.453152027
|
||||
val a5: Double = 1.061405429
|
||||
val p: Double = 0.3275911
|
||||
|
||||
def erf(x: Double): Double = {
|
||||
// Save the sign of x
|
||||
val sign = if (x < 0) -1 else 1
|
||||
val absx = abs(x)
|
||||
|
||||
// A&S formula 7.1.26, rational approximation of error function
|
||||
val t = 1.0/(1.0 + p*absx);
|
||||
val y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*exp(-x*x);
|
||||
sign*y
|
||||
}
|
||||
}
|
|
@ -1,356 +0,0 @@
|
|||
package lila.evaluation
|
||||
|
||||
import Math.{pow, E, PI, log, sqrt, abs, exp, round}
|
||||
import org.joda.time.DateTime
|
||||
import scalaz.NonEmptyList
|
||||
import chess.{ Color }
|
||||
import lila.game.{ Pov, Game }
|
||||
import lila.analyse.{ Accuracy, Analysis }
|
||||
|
||||
case class PlayerAssessment(
|
||||
_id: String,
|
||||
gameId: String,
|
||||
white: Boolean, // Side of the game being analysed
|
||||
assessment: Int, // 1 = Not Cheating, 2 = Unlikely Cheating, 3 = Unknown, 4 = Likely Cheating, 5 = Cheating
|
||||
by: String, // moderator ID
|
||||
date: DateTime) {
|
||||
val color = Color(white)
|
||||
}
|
||||
|
||||
case class PeerGame(
|
||||
gameId: String,
|
||||
white: Boolean,
|
||||
positiveMatch: Boolean,
|
||||
matchPercentage: Int,
|
||||
assessment: Int
|
||||
) {
|
||||
val color = Color(white)
|
||||
|
||||
val explanation: String =
|
||||
(if (positiveMatch) "Matches " else "Partially matches ") + "with " +
|
||||
(assessment match {
|
||||
case 1 => "a non cheating"
|
||||
case 2 => "an unlikely cheating"
|
||||
case 3 => "an unclear"
|
||||
case 4 => "a likely cheating"
|
||||
case 5 => "a cheating"
|
||||
case _ => "Undefined"
|
||||
}) + " game at " + matchPercentage + "% confidence"
|
||||
}
|
||||
|
||||
case class PlayerAggregateAssessment(
|
||||
gameGroupResults: List[GameGroupResult]
|
||||
) {
|
||||
import Statistics.listSum
|
||||
def sumAssessment(x: Int): Int =
|
||||
listSum(gameGroupResults.map { result =>
|
||||
if (result.aggregate.assessment == x && (result.aggregate.positiveMatch || result.aggregate.confidence >= 80)) 1
|
||||
else 0
|
||||
})
|
||||
|
||||
val cheatingSum: Int = sumAssessment(5)
|
||||
val likelyCheatingSum: Int = sumAssessment(4)
|
||||
val unclearSum: Int = sumAssessment(3)
|
||||
|
||||
val markPri: Boolean = cheatingSum >= 2
|
||||
val markSec: Boolean = cheatingSum + likelyCheatingSum >= 4
|
||||
val reportPri: Boolean = cheatingSum >= 1
|
||||
val reportSec: Boolean = cheatingSum + likelyCheatingSum >= 2
|
||||
}
|
||||
|
||||
case class AggregateAssessment(
|
||||
assessment: Int,
|
||||
confidence: Int,
|
||||
positiveMatch: Boolean
|
||||
) {
|
||||
val explanation: String =
|
||||
(if (positiveMatch) "Matches " else "Partially matches ") +
|
||||
(assessment match {
|
||||
case 1 => "Not Cheating"
|
||||
case 2 => "Unlikely Cheating"
|
||||
case 3 => "Unclear"
|
||||
case 4 => "Likely Cheating"
|
||||
case 5 => "Cheating"
|
||||
case _ => "Undefined"
|
||||
}) + " at " + confidence + "% confidence"
|
||||
}
|
||||
|
||||
case class GameGroupResult(
|
||||
_id: String, // sourceGameId + "/" + sourceGameColor
|
||||
userId: String, // The userId of the player being evaluated
|
||||
gameId: String, // The game being talked about
|
||||
white: Boolean, // The side of the game being talked about
|
||||
bestMatch: PeerGame,
|
||||
secondaryMatches: List[PeerGame],
|
||||
date: DateTime,
|
||||
// Meta infos
|
||||
sfAvg: Int,
|
||||
sfSd: Int,
|
||||
mtAvg: Int,
|
||||
mtSd: Int,
|
||||
blur: Int,
|
||||
hold: Boolean
|
||||
) {
|
||||
import Statistics.{listSum, listAverage}
|
||||
val color = Color(white)
|
||||
val aggregate: AggregateAssessment = {
|
||||
val peers = bestMatch :: secondaryMatches
|
||||
AggregateAssessment(
|
||||
round(listSum(peers.map {
|
||||
case a if (a.positiveMatch) => 4 * a.matchPercentage * a.assessment
|
||||
case a => a.matchPercentage * a.assessment
|
||||
}).toDouble / listSum(peers.map {
|
||||
case a if (a.positiveMatch) => 4 * a.matchPercentage
|
||||
case a => a.matchPercentage
|
||||
})).toInt,
|
||||
bestMatch.matchPercentage,
|
||||
peers.exists(_.positiveMatch)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Display {
|
||||
def assessmentString(x: Int): String =
|
||||
x match {
|
||||
case 1 => "Not Cheating" // Not cheating
|
||||
case 2 => "Unlikely Cheating" // Unlikely Cheating
|
||||
case 3 => "Unclear" // Unclear
|
||||
case 4 => "Likely Cheating" // Likely Cheating
|
||||
case 5 => "Cheating" // Cheating
|
||||
case _ => "Undef"
|
||||
}
|
||||
|
||||
def dataIcon(positiveMatch: Boolean, confidence: Int): String = {
|
||||
(positiveMatch, confidence) match {
|
||||
case (true, _) => "J"
|
||||
case (_, a) if (a >= 80) => "l"
|
||||
case _ => "K"
|
||||
}
|
||||
|
||||
}
|
||||
def dataIcon(x: AggregateAssessment): String = dataIcon(x.positiveMatch, x.confidence)
|
||||
def dataIcon(x: PeerGame): String = dataIcon(x.positiveMatch, x.matchPercentage)
|
||||
}
|
||||
|
||||
|
||||
case class GameResults(
|
||||
white: Option[GameGroupResult],
|
||||
black: Option[GameGroupResult]
|
||||
) {
|
||||
|
||||
def color(c: Color): Option[GameGroupResult] = c.fold(white, black)
|
||||
}
|
||||
|
||||
case class Rating(perf: Int, interval: Int)
|
||||
|
||||
case class Similarity(a: Double, threshold: Double = 0.9) {
|
||||
def apply: Double = a.min(1).max(0)
|
||||
|
||||
val matches: Boolean = this.apply >= threshold
|
||||
}
|
||||
|
||||
case class Analysed(game: Game, analysis: Analysis)
|
||||
|
||||
case class MatchAndSig(matches: Boolean, significance: Double)
|
||||
|
||||
case class GameGroup(analysed: Analysed, color: Color, assessment: Option[Int] = None) {
|
||||
import Statistics._
|
||||
|
||||
def compareMoveTimes (that: GameGroup): Similarity = {
|
||||
val thisMt: List[Int] = skip(this.analysed.game.moveTimes.toList, {if (this.color == Color.White) 0 else 1})
|
||||
val thatMt: List[Int] = skip(that.analysed.game.moveTimes.toList, {if (that.color == Color.White) 0 else 1})
|
||||
|
||||
listToListSimilarity(thisMt, thatMt, 0.8)
|
||||
}
|
||||
|
||||
def compareSfAccuracies (that: GameGroup): Similarity = {
|
||||
def chartDifs(difs: List[(Int, Int)], sum: Int = 0): List[Int] = {
|
||||
difs match {
|
||||
case Nil => Nil
|
||||
case a :: Nil => List((sum + a._1), (sum + a._1 + a._2))
|
||||
case a :: b => List((sum + a._1), (sum + a._1 + a._2)) ::: chartDifs(b, sum + a._1 + a._2)
|
||||
}
|
||||
}
|
||||
|
||||
def averageDif(chart: List[(Int, Int)]): Double = listAverage(chart.map{a => abs(a._1 - a._2)})
|
||||
|
||||
if (abs(this.analysed.game.playedTurns - that.analysed.game.playedTurns) <= 5) {
|
||||
val avgDif = averageDif(
|
||||
(
|
||||
chartDifs(Accuracy.diffsList(Pov(this.analysed.game, this.color), this.analysed.analysis) zip
|
||||
Accuracy.diffsList(Pov(this.analysed.game, !this.color), this.analysed.analysis))
|
||||
) zip (
|
||||
chartDifs(Accuracy.diffsList(Pov(that.analysed.game, that.color), that.analysed.analysis) zip
|
||||
Accuracy.diffsList(Pov(that.analysed.game, !that.color), that.analysed.analysis))
|
||||
))
|
||||
Similarity(((300d - avgDif) / 300d).max(0d), 0.67)
|
||||
} else Similarity(0)
|
||||
}
|
||||
|
||||
def compareBlurRates (that: GameGroup): Similarity = pointToPointSimilarity(
|
||||
(200 * this.analysed.game.player(this.color).blurs / this.analysed.game.turns).toInt,
|
||||
(200 * that.analysed.game.player(that.color).blurs / that.analysed.game.turns).toInt,
|
||||
5d
|
||||
)
|
||||
|
||||
def compareHoldAlerts (that: GameGroup): Similarity = Similarity(
|
||||
if (this.analysed.game.player(this.color).hasSuspiciousHoldAlert == that.analysed.game.player(that.color).hasSuspiciousHoldAlert) 1 else 0,
|
||||
0.9
|
||||
)
|
||||
|
||||
val sfAvg: Int = listAverage(Accuracy.diffsList(Pov(this.analysed.game, this.color), this.analysed.analysis)).toInt
|
||||
val sfSd: Int = listDeviation(Accuracy.diffsList(Pov(this.analysed.game, this.color), this.analysed.analysis)).toInt
|
||||
val mtAvg: Int = listAverage(skip(this.analysed.game.moveTimes.toList, {if (this.color == Color.White) 0 else 1})).toInt
|
||||
val mtSd: Int = listDeviation(skip(this.analysed.game.moveTimes.toList, {if (this.color == Color.White) 0 else 1})).toInt
|
||||
val blurs: Int = (200 * this.analysed.game.player(this.color).blurs / this.analysed.game.turns).toInt;
|
||||
val hold: Boolean = this.analysed.game.player(this.color).hasSuspiciousHoldAlert
|
||||
|
||||
def similarityTo (that: GameGroup): MatchAndSig = {
|
||||
// Calls compare functions to determine how similar `this` and `that` are to each other
|
||||
val sfComparison = compareSfAccuracies(that)
|
||||
|
||||
val similarities = NonEmptyList(
|
||||
compareMoveTimes(that),
|
||||
compareSfAccuracies(that),
|
||||
compareBlurRates(that),
|
||||
compareHoldAlerts(that)
|
||||
)
|
||||
|
||||
MatchAndSig(
|
||||
allSimilar(similarities), // Are they all similar?
|
||||
ssd(similarities) // How significant is the similarity?
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Statistics {
|
||||
import Erf._
|
||||
import scala.annotation._
|
||||
|
||||
def variance[T](a: NonEmptyList[T], optionalAvg: Option[T] = None)(implicit n: Numeric[T]): Double = {
|
||||
val avg: Double = optionalAvg.fold(average(a)){n.toDouble}
|
||||
|
||||
a.map( i => pow(n.toDouble(i) - avg, 2)).list.sum / a.length
|
||||
}
|
||||
|
||||
def deviation[T](a: NonEmptyList[T], optionalAvg: Option[T] = None)(implicit n: Numeric[T]): Double = sqrt(variance(a, optionalAvg))
|
||||
|
||||
def average[T](a: NonEmptyList[T])(implicit n: Numeric[T]): Double = {
|
||||
@tailrec def average(a: List[T], sum: T = n.zero, depth: Int = 0): Double = {
|
||||
a match {
|
||||
case List() => n.toDouble(sum) / depth
|
||||
case x :: xs => average(xs, n.plus(sum, x), depth + 1)
|
||||
}
|
||||
}
|
||||
average(a.list)
|
||||
}
|
||||
|
||||
def setToSetSimilarity(avgA: Double, avgB: Double, varA: Double, varB: Double, threshold: Double): Similarity = {
|
||||
val sim = Similarity(
|
||||
pow(E, (-0.25) * ( log( 0.25 * ((varA / varB) + (varB / varA) + 2) ) + pow(avgA - avgB, 2) / ( varA + varB ) )),
|
||||
threshold)
|
||||
|
||||
if (sim.a.isNaN || sim.a.isInfinity) Similarity(1, threshold)
|
||||
else sim
|
||||
}
|
||||
|
||||
// Bhattacharyya Coefficient
|
||||
def setToSetSimilarity[T](a: NonEmptyList[T], b: NonEmptyList[T], threshold: Double = 0.9)(implicit n: Numeric[T]): Similarity = {
|
||||
val aDouble: NonEmptyList[Double] = a.map(n.toDouble)
|
||||
val bDouble: NonEmptyList[Double] = b.map(n.toDouble)
|
||||
|
||||
val avgA = average(a)
|
||||
val avgB = average(b)
|
||||
|
||||
val varA = pow(variance(aDouble, Some(avgA)), 2)
|
||||
val varB = pow(variance(bDouble, Some(avgB)), 2)
|
||||
|
||||
setToSetSimilarity(avgA, avgB, varA, varB, threshold)
|
||||
}
|
||||
|
||||
def listToListSimilarity[T](x: List[T], y: List[T], threshold: Double = 0.9)(implicit n: Numeric[T]): Similarity =
|
||||
(x, y) match {
|
||||
case (Nil, Nil) => Similarity(1) // Both empty
|
||||
case (Nil, _ :: _) => Similarity(0) // One empty, The other with some
|
||||
case (_ :: _, Nil) => Similarity(0)
|
||||
case (a :: Nil, b :: Nil) => pointToPointSimilarity(a, b, 5d) // Both have one
|
||||
case (a :: Nil, b :: c) => pointToSetSimilarity(a, NonEmptyList.nel(b, c)) // One with one element, the other with many
|
||||
case (a :: b, c :: Nil) => pointToSetSimilarity(c, NonEmptyList.nel(a, b))
|
||||
case (a :: b, c :: d) => setToSetSimilarity(NonEmptyList.nel(a, b), NonEmptyList.nel(c, d), threshold) // Both have many
|
||||
}
|
||||
|
||||
def pointToSetSimilarity[T](x: T, set: NonEmptyList[T])(implicit n: Numeric[T]): Similarity = Similarity(
|
||||
confInterval(n.toDouble(x), average(set), sqrt(variance(set))),
|
||||
0.9
|
||||
)
|
||||
|
||||
def pointToPointSimilarity[T](a: T, b: T, variance: Double)(implicit n: Numeric[T]): Similarity = Similarity(
|
||||
(a, b) match {
|
||||
case (a, b) if (a == b || n.toDouble(n.abs(n.minus(a, b))) < variance) => 1
|
||||
case _ => 0
|
||||
}
|
||||
)
|
||||
|
||||
// Coefficient of Variance
|
||||
def coefVariation(a: NonEmptyList[Int]): Double = sqrt(variance(a)) / average(a)
|
||||
|
||||
def intervalToVariance4(interval: Double): Double = pow(interval / 3, 8) // roughly speaking
|
||||
|
||||
// Accumulative probability function for normal distributions
|
||||
def cdf[T](x: T, avg: T, sd: T)(implicit n: Numeric[T]): Double =
|
||||
0.5 * (1 + erf(n.toDouble(n.minus(x, avg)) / (n.toDouble(sd)*sqrt(2))))
|
||||
|
||||
// The probability that you are outside of abs(x-n) from the mean on both sides
|
||||
def confInterval[T](x: T, avg: T, sd: T)(implicit n: Numeric[T]): Double =
|
||||
1 - cdf(n.abs(x), avg, sd) + cdf(n.times(n.fromInt(-1), n.abs(x)), avg, sd)
|
||||
|
||||
// all Similarities in the non empty list are similar
|
||||
def allSimilar(a: NonEmptyList[Similarity]): Boolean = a.list.forall( _.matches )
|
||||
def allSimilar(a: Similarity, b: List[Similarity]): Boolean = allSimilar(NonEmptyList.nel(a, b))
|
||||
|
||||
// Square Sum Distance
|
||||
def ssd(a: NonEmptyList[Similarity]): Double = sqrt(a.map(x => pow(x.apply, 2)).list.sum / a.size)
|
||||
def ssd(a: Similarity, b: List[Similarity]): Double = ssd(NonEmptyList.nel(a, b))
|
||||
|
||||
def skip[A](l: List[A], n: Int) =
|
||||
l.zipWithIndex.collect {case (e,i) if ((i+n) % 2) == 0 => e} // (i+1) because zipWithIndex is 0-based
|
||||
|
||||
|
||||
def listSum(xs: List[Int]): Int = xs match {
|
||||
case Nil => 0
|
||||
case x :: tail => x + listSum(tail)
|
||||
}
|
||||
|
||||
def listAverage[T](x: List[T])(implicit n: Numeric[T]): Double = x match {
|
||||
case Nil => 0
|
||||
case a :: Nil => n.toDouble(a)
|
||||
case a :: b => average(NonEmptyList.nel(a, b))
|
||||
}
|
||||
|
||||
def listDeviation[T](x: List[T])(implicit n: Numeric[T]): Double = x match {
|
||||
case Nil => 0
|
||||
case _ :: Nil => 0
|
||||
case a :: b => deviation(NonEmptyList.nel(a, b))
|
||||
}
|
||||
}
|
||||
|
||||
object Erf {
|
||||
// constants
|
||||
val a1: Double = 0.254829592
|
||||
val a2: Double = -0.284496736
|
||||
val a3: Double = 1.421413741
|
||||
val a4: Double = -1.453152027
|
||||
val a5: Double = 1.061405429
|
||||
val p: Double = 0.3275911
|
||||
|
||||
def erf(x: Double): Double = {
|
||||
// Save the sign of x
|
||||
val sign = if (x < 0) -1 else 1
|
||||
val absx = abs(x)
|
||||
|
||||
// A&S formula 7.1.26, rational approximation of error function
|
||||
val t = 1.0/(1.0 + p*absx);
|
||||
val y = 1.0 - (((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*exp(-x*x);
|
||||
sign*y
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ package lila.mod
|
|||
import lila.analyse.{ Analysis, AnalysisRepo }
|
||||
import lila.db.Types.Coll
|
||||
import lila.db.BSON.BSONJodaDateTimeHandler
|
||||
import lila.evaluation.{ PlayerAssessment, GameGroupResult, GameResults, GameGroup, Analysed, PeerGame, MatchAndSig }
|
||||
import lila.evaluation.{ Analysed, PlayerAssessment, GameAssessments, Assessible }
|
||||
import lila.game.Game
|
||||
import lila.game.{ Game, GameRepo }
|
||||
import org.joda.time.DateTime
|
||||
|
@ -13,104 +13,45 @@ import scala.concurrent._
|
|||
import chess.Color
|
||||
|
||||
|
||||
final class AssessApi(collRef: Coll, collRes: Coll, logApi: ModlogApi) {
|
||||
final class AssessApi(collAssessments: Coll, logApi: ModlogApi) {
|
||||
|
||||
private implicit val bestMatchBSONhandler = Macros.handler[PeerGame]
|
||||
private implicit val playerAssessmentBSONhandler = Macros.handler[PlayerAssessment]
|
||||
private implicit val gameGroupResultBSONhandler = Macros.handler[GameGroupResult]
|
||||
|
||||
def createPlayerAssessment(assessed: PlayerAssessment) =
|
||||
collRef.update(BSONDocument("_id" -> assessed._id), assessed, upsert = true) >>
|
||||
logApi.assessGame(assessed.by, assessed.gameId, assessed.color.name, assessed.assessment) >>
|
||||
refreshAssess(assessed.gameId)
|
||||
collAssessments.update(BSONDocument("_id" -> assessed._id), assessed, upsert = true).void
|
||||
|
||||
def createResult(result: GameGroupResult) =
|
||||
collRes.update(BSONDocument("_id" -> result._id), result, upsert = true).void
|
||||
|
||||
def getPlayerAssessments(max: Int): Fu[List[PlayerAssessment]] = collRef.find(BSONDocument())
|
||||
.cursor[PlayerAssessment]
|
||||
.collect[List](max)
|
||||
|
||||
def getPlayerAssessmentById(id: String) = collRef.find(BSONDocument("_id" -> id))
|
||||
def getPlayerAssessmentById(id: String) =
|
||||
collAssessments.find(BSONDocument("_id" -> id))
|
||||
.one[PlayerAssessment]
|
||||
|
||||
def getResultsByUserId(userId: String, nb: Int = 100) = collRes.find(BSONDocument("userId" -> userId))
|
||||
.sort(BSONDocument("bestMatch.assessment" -> -1, "bestMatch.positiveMatch" -> -1, "bestMatch.matchPercentage" -> -1))
|
||||
.cursor[GameGroupResult]
|
||||
def getPlayerAssessmentsByUserId(userId: String, nb: Int = 100) =
|
||||
collAssessments.find(BSONDocument("userId" -> userId))
|
||||
.sort(BSONDocument("assessment" -> -1))
|
||||
.cursor[PlayerAssessment]
|
||||
.collect[List](nb)
|
||||
|
||||
def getResultsByGameIdAndColor(gameId: String, color: Color) = collRes.find(BSONDocument("_id" -> (gameId + "/" + color.name)))
|
||||
.one[GameGroupResult]
|
||||
def getResultsByGameIdAndColor(gameId: String, color: Color) =
|
||||
collAssessments.find(BSONDocument("_id" -> (gameId + "/" + color.name)))
|
||||
.one[PlayerAssessment]
|
||||
|
||||
def getResultsByGameId(gameId: String): Fu[GameResults] =
|
||||
def getGameResultsById(gameId: String) =
|
||||
getResultsByGameIdAndColor(gameId, Color.White) zip
|
||||
getResultsByGameIdAndColor(gameId, Color.Black) map
|
||||
GameResults.tupled
|
||||
getResultsByGameIdAndColor(gameId, Color.Black) map {
|
||||
a => GameAssessments(a._1, a._2)
|
||||
}
|
||||
|
||||
def refreshAssess(gameId: String): Funit =
|
||||
GameRepo.game(gameId) zip
|
||||
AnalysisRepo.doneById(gameId) map {
|
||||
case (Some(g), Some(a)) => onAnalysisReady(g, a)
|
||||
case _ => funit
|
||||
case (Some(g), Some(a)) => onAnalysisReady(g, a)
|
||||
case _ => funit
|
||||
}
|
||||
|
||||
def onAnalysisReady(game: Game, analysis: Analysis): Funit = {
|
||||
def playerAssessmentGameGroups: Fu[List[GameGroup]] =
|
||||
getPlayerAssessments(400) flatMap { assessments =>
|
||||
GameRepo.gameOptions(assessments.map(_.gameId)) flatMap { games =>
|
||||
AnalysisRepo.doneByIds(assessments.map(_.gameId)) map { analyses =>
|
||||
assessments zip games zip analyses flatMap {
|
||||
case ((assessment, Some(game)), Some(analysis)) =>
|
||||
Some(GameGroup(Analysed(game, analysis), assessment.color, Some(assessment.assessment)))
|
||||
case _ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def buildGameGroupResult(source: GameGroup, assessments: List[GameGroup], nb: Int = 5): Option[GameGroupResult] =
|
||||
(assessments.map(source.similarityTo) zip assessments).sortBy(-_._1.significance).take(nb).map {
|
||||
case (matchAndSig: MatchAndSig, gameGroup: GameGroup) =>
|
||||
PeerGame(
|
||||
gameId = gameGroup.analysed.game.id,
|
||||
white = gameGroup.color.white,
|
||||
positiveMatch = matchAndSig.matches,
|
||||
matchPercentage = (100 * matchAndSig.significance).toInt,
|
||||
assessment = gameGroup.assessment.getOrElse(1).min(source.analysed.game.wonBy(source.color) match {
|
||||
case Some(color) if (color) => 5
|
||||
case _ => 3
|
||||
})
|
||||
)
|
||||
} match {
|
||||
case a :: b =>
|
||||
Some(GameGroupResult(
|
||||
_id = source.analysed.game.id + "/" + source.color.name,
|
||||
userId = source.analysed.game.player(source.color).userId.getOrElse(""),
|
||||
gameId = source.analysed.game.id,
|
||||
white = source.color.white,
|
||||
bestMatch = a,
|
||||
secondaryMatches = b,
|
||||
date = DateTime.now,
|
||||
sfAvg = source.sfAvg,
|
||||
sfSd = source.sfSd,
|
||||
mtAvg = source.mtAvg,
|
||||
mtSd = source.mtSd,
|
||||
blur = source.blurs,
|
||||
hold = source.hold
|
||||
))
|
||||
case _ => None
|
||||
}
|
||||
|
||||
if (!game.isCorrespondence && game.turns >= 40 && game.mode.rated) {
|
||||
val whiteGameGroup = GameGroup(Analysed(game, analysis), Color.White)
|
||||
val blackGameGroup = GameGroup(Analysed(game, analysis), Color.Black)
|
||||
|
||||
playerAssessmentGameGroups flatMap {
|
||||
a => {
|
||||
buildGameGroupResult(whiteGameGroup, a).fold(funit){createResult} >>
|
||||
buildGameGroupResult(blackGameGroup, a).fold(funit){createResult}
|
||||
}
|
||||
}
|
||||
val gameAssessments: GameAssessments = Assessible(Analysed(game, analysis)).assessments
|
||||
gameAssessments.white.fold(funit){createPlayerAssessment} >>
|
||||
gameAssessments.black.fold(funit){createPlayerAssessment}
|
||||
} else funit
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,7 @@ final class Env(
|
|||
firewall: Firewall,
|
||||
userSpy: String => Fu[UserSpy]) {
|
||||
|
||||
private val CollectionPlayerAssessment = config getString "collection.crossref"
|
||||
private val CollectionResult = config getString "collection.result"
|
||||
private val CollectionPlayerAssessment= config getString "collection.player_assessment"
|
||||
private val CollectionBoosting = config getString "collection.boosting"
|
||||
private val CollectionModlog = config getString "collection.modlog"
|
||||
private val ActorName = config getString "actor.name"
|
||||
|
@ -24,7 +23,7 @@ final class Env(
|
|||
|
||||
lazy val logApi = new ModlogApi
|
||||
|
||||
lazy val assessApi = new AssessApi(db(CollectionPlayerAssessment), db(CollectionResult), logApi)
|
||||
lazy val assessApi = new AssessApi(db(CollectionPlayerAssessment), logApi)
|
||||
|
||||
lazy val api = new ModApi(
|
||||
logApi = logApi,
|
||||
|
|
|
@ -2032,38 +2032,12 @@ lichess.storage = {
|
|||
});
|
||||
|
||||
if ($('#refreshAssessment').length) {
|
||||
$('#whiteAssessment').val($('#whiteAssessment').data('val'));
|
||||
$('#blackAssessment').val($('#blackAssessment').data('val'));
|
||||
|
||||
var sendAssessment = function(side, e) {
|
||||
if ($(e.target).val() != "0") {
|
||||
$.post('/mod/' + data.game.id + '/' + side + '/assess', {
|
||||
assessment: $(e.target).val()
|
||||
});
|
||||
}
|
||||
}
|
||||
$('#whiteAssessment').change(sendAssessment.bind(null, 'white'));
|
||||
$('#blackAssessment').change(sendAssessment.bind(null, 'black'));
|
||||
|
||||
$('#refreshAssessment').click(function() {
|
||||
$.post('/mod/refreshAssess', {
|
||||
assess: data.game.id,
|
||||
success: function(){ setTimeout(function(){ location.reload(); }, 3000) }
|
||||
});
|
||||
});
|
||||
|
||||
$('#confirmAssessment').click(function() {
|
||||
if ($('#whiteAssessment').val() != "0") {
|
||||
$.post('/mod/' + data.game.id + '/white/assess', {
|
||||
assessment: $('#whiteAssessment').val()
|
||||
});
|
||||
}
|
||||
if ($('#blackAssessment').val() != "0") {
|
||||
$.post('/mod/' + data.game.id + '/black/assess', {
|
||||
assessment: $('#blackAssessment').val()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ body {
|
|||
font: 12px 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, Sans-Serif;
|
||||
color: #909090;
|
||||
background: #05121b; /* url("bluegrid.jpg") center repeat; */
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
|
Loading…
Reference in New Issue