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:
Thibault Duplessis 2015-02-04 09:00:02 +01:00
commit cfccb2f455
6 changed files with 215 additions and 45 deletions

View file

@ -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))
}
}
}

View file

@ -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>

View file

@ -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">&Sigma;</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)">&nbsp;</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>
}

View file

@ -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)

View file

@ -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)

View file

@ -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 */
}