Merge remote-tracking branch 'origin/ScalaEvaluator'
* origin/ScalaEvaluator: Allow games with 40 turns UI and determinations Weigh positive matches more heavily UI, Mark/Report Conds. and DateTime entry Update scalachess Improve display of report card
This commit is contained in:
commit
cfccb2f455
|
@ -12,6 +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 views._
|
||||
|
||||
object User extends LilaController {
|
||||
|
@ -147,7 +148,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 {
|
||||
case ((eval, spy), gameResults) => html.user.mod(user, spy, eval, gameResults)
|
||||
case ((eval, spy), gameResults) => html.user.mod(user, spy, eval, PlayerAggregateAssessment(gameResults))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
@(game: Game, results: GameResults)(implicit ctx: Context)
|
||||
<br />
|
||||
@if(isGranted(_.MarkEngine) && game.analysable && !game.isCorrespondence && game.turns > 40) {
|
||||
@if(isGranted(_.MarkEngine) && game.analysable && !game.isCorrespondence && game.turns >= 40) {
|
||||
@game.players.map { p =>
|
||||
<table class="modAssessment">
|
||||
<tr>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
@(u: User, spy: lila.security.UserSpy, eval: Option[lila.evaluation.Evaluation], gameResults: List[lila.evaluation.GameGroupResult])(implicit ctx: Context)
|
||||
@(u: User, spy: lila.security.UserSpy, eval: Option[lila.evaluation.Evaluation], aggregateAssessment: lila.evaluation.PlayerAggregateAssessment)(implicit ctx: Context)
|
||||
|
||||
@import lila.evaluation.Display
|
||||
|
||||
<div class="actions clearfix">
|
||||
@if(isGranted(_.UserEvaluate)) {
|
||||
|
@ -75,9 +77,52 @@
|
|||
}
|
||||
</div>
|
||||
}
|
||||
@if(gameResults.nonEmpty) {
|
||||
<div class="evaluation">
|
||||
<table class="slist hint--top">
|
||||
@if(aggregateAssessment.gameGroupResults.nonEmpty) {
|
||||
<div class="evaluation results">
|
||||
<table class="reportCard">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="@(if(aggregateAssessment.markPri) "mark" else "noMark")">Cheating</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="@(if(aggregateAssessment.markSec) "mark" else "noMark")">Cheating</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="@(if(aggregateAssessment.reportPri) "report" else "noReport")">Report</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="@(if(aggregateAssessment.reportSec) "report" else "noReport")">Report</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<span class="sig_5">@aggregateAssessment.cheatingSum</span> / 2
|
||||
</td>
|
||||
<td>
|
||||
<span class="sig_5">@aggregateAssessment.cheatingSum</span> +
|
||||
<span class="sig_4">@aggregateAssessment.likelyCheatingSum</span> / 4
|
||||
</td>
|
||||
<td>
|
||||
<span class="sig_5">@aggregateAssessment.cheatingSum</span> +
|
||||
<span class="sig_4">@aggregateAssessment.likelyCheatingSum</span> / 2
|
||||
</td>
|
||||
<td>
|
||||
<span class="sig_5">@aggregateAssessment.cheatingSum</span> +
|
||||
<span class="sig_4">@aggregateAssessment.likelyCheatingSum</span> +
|
||||
<span class="sig_3">@aggregateAssessment.unclearSum</span> / 4
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="legend">
|
||||
@Range(1, 6).map { i =>
|
||||
<span class="sig_@i">@Display.assessmentString(i)</span>
|
||||
}
|
||||
</p>
|
||||
<table class="slist">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Assessed game</th>
|
||||
|
@ -91,15 +136,16 @@
|
|||
</th>
|
||||
<th>Blurs</th>
|
||||
<th>Bot</th>
|
||||
<th><span data-hint="Primary match">1</span></th>
|
||||
<th><span data-hint="Secondary match">2</span></th>
|
||||
<th><span data-hint="Tertiary match">3</span></th>
|
||||
<th><span data-hint="Quaternary match">4</span></th>
|
||||
<th><span data-hint="Quinary match">5</span></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>
|
||||
@gameResults.map { result =>
|
||||
@aggregateAssessment.gameGroupResults.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>
|
||||
|
@ -107,18 +153,23 @@
|
|||
<td>@(result.blur)%</td>
|
||||
<td>@if(result.hold){Yes} else {No}</td>
|
||||
<td>
|
||||
<a href="@routes.Round.watcher(result.bestMatch.gameId, result.bestMatch.color.name)" class="peerLink hint--top" data-hint="@result.bestMatch.explanation">
|
||||
<span class="assessment sig_@(result.bestMatch.assessment)">@result.bestMatch.assessmentString</span>
|
||||
<br /><span class="@if(result.bestMatch.positiveMatch) {match} else {partial}">@(result.bestMatch.matchPercentage)%</span>
|
||||
<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>
|
||||
</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" data-hint="@peer.explanation">
|
||||
<span class="assessment sig_@(peer.assessment)">@peer.assessmentString</span>
|
||||
<br /><span class="@if(peer.positiveMatch) {match} else {partial}">@(peer.matchPercentage)%</span>
|
||||
<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>
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package lila.evaluation
|
||||
|
||||
import Math.{pow, E, PI, log, sqrt, abs, exp}
|
||||
import Math.{pow, E, PI, log, sqrt, abs, exp, round}
|
||||
import org.joda.time.DateTime
|
||||
import scalaz.NonEmptyList
|
||||
import chess.{ Color }
|
||||
|
@ -26,16 +26,6 @@ case class PeerGame(
|
|||
) {
|
||||
val color = Color(white)
|
||||
|
||||
val assessmentString: String =
|
||||
assessment match {
|
||||
case 1 => "NC" // Not cheating
|
||||
case 2 => "UC" // Unlikely Cheating
|
||||
case 3 => "NA" // Unclear
|
||||
case 4 => "LC" // Likely Cheating
|
||||
case 5 => "CH" // Cheating
|
||||
case _ => "Undef"
|
||||
}
|
||||
|
||||
val explanation: String =
|
||||
(if (positiveMatch) "Matches " else "Partially matches ") + "with " +
|
||||
(assessment match {
|
||||
|
@ -48,6 +38,43 @@ case class PeerGame(
|
|||
}) + " 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 + likelyCheatingSum >= 2
|
||||
val reportSec: Boolean = cheatingSum + likelyCheatingSum + unclearSum >= 4
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -55,6 +82,7 @@ case class GameGroupResult(
|
|||
white: Boolean, // The side of the game being talked about
|
||||
bestMatch: PeerGame,
|
||||
secondaryMatches: List[PeerGame],
|
||||
date: DateTime,
|
||||
// Meta infos
|
||||
sfAvg: Int,
|
||||
sfSd: Int,
|
||||
|
@ -63,9 +91,48 @@ case class GameGroupResult(
|
|||
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]
|
||||
|
@ -100,17 +167,18 @@ case class GameGroup(analysed: Analysed, color: Color, assessment: Option[Int] =
|
|||
def groupedDiffList(game: Game, color: Color, analysis: Analysis, size: Int = 5): List[List[Int]] =
|
||||
Accuracy.diffsList(Pov(game, color), analysis).grouped(size).toList
|
||||
// Insist that is greater than this (so this can be compared in full saturation)
|
||||
if (that.analysed.game.turns < this.analysed.game.turns) return (Similarity(0), Similarity(0))
|
||||
val thisPlayerDiffs = groupedDiffList(this.analysed.game, this.color, this.analysed.analysis)
|
||||
val thatPlayerDiffs = groupedDiffList(that.analysed.game, that.color, that.analysed.analysis)
|
||||
if (thisPlayerDiffs.size != thatPlayerDiffs.size) return (Similarity(0), Similarity(0))
|
||||
else {
|
||||
(
|
||||
(groupedDiffList(this.analysed.game, this.color, this.analysed.analysis) zip
|
||||
groupedDiffList(that.analysed.game, that.color, that.analysed.analysis)) map {
|
||||
thisPlayerDiffs zip thatPlayerDiffs map {
|
||||
a => listToListSimilarity(a._1, a._2, 0.8)
|
||||
}
|
||||
,
|
||||
(groupedDiffList(this.analysed.game, !this.color, this.analysed.analysis) zip
|
||||
groupedDiffList(that.analysed.game, !that.color, that.analysed.analysis)) map {
|
||||
a => listToListSimilarity(a._1, a._2, 0.8)
|
||||
a => listToListSimilarity(a._1, a._2, 0.6)
|
||||
}
|
||||
) match {
|
||||
case (Nil, Nil) => (Similarity(0), Similarity(0)) // Both empty
|
||||
|
@ -266,6 +334,12 @@ object Statistics {
|
|||
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)
|
||||
|
|
|
@ -6,6 +6,7 @@ import lila.db.BSON.BSONJodaDateTimeHandler
|
|||
import lila.evaluation.{ PlayerAssessment, GameGroupResult, GameResults, GameGroup, Analysed, PeerGame, MatchAndSig }
|
||||
import lila.game.Game
|
||||
import lila.game.{ Game, GameRepo }
|
||||
import org.joda.time.DateTime
|
||||
import reactivemongo.bson._
|
||||
import scala.concurrent._
|
||||
|
||||
|
@ -55,7 +56,7 @@ final class AssessApi(collRef: Coll, collRes: Coll, logApi: ModlogApi) {
|
|||
|
||||
def onAnalysisReady(game: Game, analysis: Analysis): Funit = {
|
||||
def playerAssessmentGameGroups: Fu[List[GameGroup]] =
|
||||
getPlayerAssessments(200) flatMap { assessments =>
|
||||
getPlayerAssessments(400) flatMap { assessments =>
|
||||
GameRepo.gameOptions(assessments.map(_.gameId)) flatMap { games =>
|
||||
AnalysisRepo.doneByIds(assessments.map(_.gameId)) map { analyses =>
|
||||
assessments zip games zip analyses flatMap {
|
||||
|
@ -86,6 +87,7 @@ final class AssessApi(collRef: Coll, collRes: Coll, logApi: ModlogApi) {
|
|||
white = source.color.white,
|
||||
bestMatch = a,
|
||||
secondaryMatches = b,
|
||||
date = DateTime.now,
|
||||
sfAvg = source.sfAvg,
|
||||
sfSd = source.sfSd,
|
||||
mtAvg = source.mtAvg,
|
||||
|
@ -96,7 +98,7 @@ final class AssessApi(collRef: Coll, collRes: Coll, logApi: ModlogApi) {
|
|||
case _ => None
|
||||
}
|
||||
|
||||
if (!game.isCorrespondence && game.turns > 40 && game.mode.rated) {
|
||||
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)
|
||||
|
||||
|
|
|
@ -228,32 +228,37 @@ div.user_show .evaluation strong {
|
|||
display: inline;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
div.user_show .evaluation .assessment {
|
||||
text-align: right;
|
||||
div.user_show .evaluation .percentage {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
div.user_show .evaluation .legend {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
div.user_show .evaluation .legend span {
|
||||
padding: 10px;
|
||||
}
|
||||
div.user_show .evaluation .sig_1 {
|
||||
color: #99d531;
|
||||
color: #388922; /* green */
|
||||
}
|
||||
div.user_show .evaluation .sig_2 {
|
||||
color: #b8d531;
|
||||
color: #83ab23; /* appricot */
|
||||
}
|
||||
div.user_show .evaluation .sig_3 {
|
||||
color: #d3d531;
|
||||
color: #B8AA1A; /* yellow */
|
||||
}
|
||||
div.user_show .evaluation .sig_4 {
|
||||
font-weight: bold;
|
||||
color: #d59931;
|
||||
color: #C36A2A; /* orange */
|
||||
}
|
||||
div.user_show .evaluation .sig_5 {
|
||||
font-weight: bold;
|
||||
color: #de6039;
|
||||
color: #dc322f; /* red */
|
||||
}
|
||||
div.user_show .evaluation .match {
|
||||
font-weight: bold;
|
||||
color: #759900;
|
||||
}
|
||||
div.user_show .evaluation .partial {
|
||||
color: #d59120;
|
||||
}
|
||||
div.user_show div.content_box_inter.tabs {
|
||||
margin-left: -8px;
|
||||
|
@ -265,5 +270,42 @@ div.user_show #crosstable {
|
|||
margin: 25px auto 15px auto;
|
||||
}
|
||||
div.user_show .evaluation .peerLink {
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
div.user_show .results table.slist th {
|
||||
text-align: center;
|
||||
}
|
||||
div.user_show .results table.slist td {
|
||||
text-align: center;
|
||||
}
|
||||
div.user_show .reportCard {
|
||||
margin: 0 auto 10px auto;
|
||||
}
|
||||
div.user_show .reportCard th {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 2em;
|
||||
padding: 0 20px 5px 20px;
|
||||
}
|
||||
div.user_show .reportCard td {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
div.user_show .results .aggregate {
|
||||
font-size: 2.2em;
|
||||
margin-top: -5px;
|
||||
}
|
||||
div.user_show .reportCard .mark {
|
||||
color: #dc322f; /* red */
|
||||
}
|
||||
div.user_show .reportCard .noMark {
|
||||
/*color: #dc322f;*/ /* red */
|
||||
}
|
||||
div.user_show .reportCard .report {
|
||||
color: #B8AA1A; /* red */
|
||||
}
|
||||
div.user_show .reportCard .noReport {
|
||||
/*color: #dc322f;*/ /* red */
|
||||
}
|
Loading…
Reference in a new issue