opening trainer is working

This commit is contained in:
Thibault Duplessis 2015-01-08 16:22:26 +01:00
parent 91a2c0d488
commit 82b80bbc79
21 changed files with 237 additions and 86 deletions

View file

@ -8,9 +8,9 @@ import play.twirl.api.Html
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.opening.{ Generated, Opening => OpeningModel }
import lila.user.{ User => UserModel, UserRepo }
import lila.common.HTTPRequest
import views._
import views.html.opening.JsData
@ -20,12 +20,16 @@ object Opening extends LilaController {
private def renderShow(opening: OpeningModel)(implicit ctx: Context) =
env userInfos ctx.me map { infos =>
views.html.opening.show(opening, infos)
views.html.opening.show(opening, infos, env.AnimationDuration)
}
def home = Open { implicit ctx =>
if (HTTPRequest isXhr ctx.req) env.selector(ctx.me) zip (env userInfos ctx.me) map {
case (opening, infos) => Ok(JsData(opening, infos, true)) as JSON
case (opening, infos) => Ok(JsData(opening, infos,
play = true,
attempt = none,
win = none,
animationDuration = env.AnimationDuration)) as JSON
}
else env.selector(ctx.me) flatMap { opening =>
renderShow(opening) map { Ok(_) }
@ -45,23 +49,42 @@ object Opening extends LilaController {
implicit val req = ctx.body
OptionFuResult(env.api.opening find id) { opening =>
attemptForm.bindFromRequest.fold(
err => fuccess(BadRequest(err.errorsAsJson)), {
case (found, failed) => ctx.me match {
case Some(me) => env.finisher(opening, me, found, failed) flatMap { attempt =>
UserRepo byId me.id map (_ | me) flatMap { me2 =>
env.api.opening find id zip
(env userInfos me2.some) map {
err => fuccess(BadRequest(err.errorsAsJson)),
data => {
val (found, failed) = data
val win = found == opening.goal && failed == 0
ctx.me match {
case Some(me) => env.finisher(opening, me, win) flatMap {
case (newAttempt, None) =>
UserRepo byId me.id map (_ | me) flatMap { me2 =>
(env.api.opening find id) zip (env userInfos me2.some) map {
case (o2, infos) => Ok {
JsData(o2 | opening, infos, false)
JsData(o2 | opening, infos,
play = false,
attempt = newAttempt.some,
win = none,
animationDuration = env.AnimationDuration)
}
}
}
case (oldAttempt, Some(win)) => env userInfos me.some map { infos =>
Ok(JsData(opening, infos,
play = false,
attempt = oldAttempt.some,
win = win.some,
animationDuration = env.AnimationDuration))
}
}
case None => fuccess {
Ok(JsData(opening, none, false))
Ok(JsData(opening, none,
play = false,
attempt = none,
win = win.some,
animationDuration = env.AnimationDuration))
}
}
}) map (_ as JSON)
}
) map (_ as JSON)
}
}

View file

@ -13,7 +13,10 @@ object JsData extends lila.Steroids {
def apply(
opening: Opening,
userInfos: Option[lila.opening.UserInfos],
play: Boolean)(implicit ctx: Context) =
play: Boolean,
attempt: Option[Attempt],
win: Option[Boolean],
animationDuration: scala.concurrent.duration.Duration)(implicit ctx: Context) =
Html(Json.stringify(Json.obj(
"opening" -> Json.obj(
"id" -> opening.id,
@ -32,6 +35,15 @@ object JsData extends lila.Steroids {
}),
"url" -> s"$netBaseUrl${routes.Opening.show(opening.id)}"
),
"animation" -> Json.obj(
"duration" -> ctx.pref.animationFactor * animationDuration.toMillis
),
"attempt" -> attempt.map { a =>
Json.obj(
"userRatingDiff" -> a.userRatingDiff,
"win" -> a.win)
},
"win" -> win,
"user" -> userInfos.map { i =>
Json.obj(
"rating" -> i.user.perfs.opening.intRating,

View file

@ -1,4 +1,4 @@
@(opening: lila.opening.Opening, userInfos: Option[lila.opening.UserInfos])(implicit ctx: Context)
@(opening: lila.opening.Opening, userInfos: Option[lila.opening.UserInfos], animationDuration: scala.concurrent.duration.Duration)(implicit ctx: Context)
@evenMoreJs = {
@helper.javascriptRouter("openingRoutes")(
@ -6,7 +6,7 @@
@embedJs {
LichessOpening(
document.querySelector('#lichess .round'),
@JsData(opening, userInfos, true),
@JsData(opening, userInfos, play = true, attempt = none, win = none, animationDuration = animationDuration),
openingRoutes.controllers,
@Html(J.stringify(i18nJsObject(
trans.training,
@ -20,7 +20,16 @@ trans.openingId,
trans.ratingX,
trans.playedXTimes,
trans.yourOpeningRatingX,
trans.findNbStrongMoves
trans.findNbStrongMoves,
trans.openingFailed,
trans.openingSolved,
trans.butYouCanKeepTrying,
trans.goodMove,
trans.butYouCanDoBetter,
trans.bestMove,
trans.keepGoing,
trans.victory,
trans.thisMoveGivesYourOpponentTheAdvantage
)))
);
}

View file

@ -312,3 +312,6 @@ autoSwitch=Auto switch
openingId=Opening %s
yourOpeningRatingX=Your opening rating: %s
findNbStrongMoves=Find %s strong moves
thisMoveGivesYourOpponentTheAdvantage=This move gives your opponent the advantage
openingFailed=Opening failed
openingSolved=Opening solved

@ -1 +1 @@
Subproject commit e0989842735e58803c2ac20b2548c4d8f8c03ac2
Subproject commit 8e52881a9b77d371a1144a5814f594458ce52efe

File diff suppressed because one or more lines are too long

View file

@ -16,6 +16,8 @@ final class Env(
}
import settings._
val AnimationDuration = config duration "animation.duration"
lazy val api = new OpeningApi(
openingColl = openingColl,
attemptColl = attemptColl,
@ -23,7 +25,10 @@ final class Env(
lazy val selector = new Selector(
openingColl = openingColl,
api = api)
api = api,
toleranceStep = config getInt "selector.tolerance.step",
toleranceMax = config getInt "selector.tolerance.max",
modulo = config getInt "selector.modulo")
lazy val finisher = new Finisher(
api = api,

View file

@ -12,11 +12,10 @@ private[opening] final class Finisher(
api: OpeningApi,
openingColl: Coll) {
def apply(opening: Opening, user: User, found: Int, failed: Int): Fu[Attempt] =
def apply(opening: Opening, user: User, win: Boolean): Fu[(Attempt, Option[Boolean])] = {
api.attempt.find(opening.id, user.id) flatMap {
case Some(a) => fuccess(a)
case Some(a) => fuccess(a -> win.some)
case None =>
val win = found == opening.goal && failed == 0
val userRating = user.perfs.opening.toRating
val openingRating = opening.perf.toRating
updateRatings(userRating, openingRating, win.fold(Glicko.Result.Win, Glicko.Result.Loss))
@ -44,8 +43,9 @@ private[opening] final class Finisher(
))) zip UserRepo.setPerf(user.id, "opening", userPerf)
}) recover {
case e: reactivemongo.core.commands.LastError if e.getMessage.contains("duplicate key error") => ()
} inject a
} inject (a -> none)
}
}
private val VOLATILITY = Glicko.default.volatility
private val TAU = 0.75d

View file

@ -97,6 +97,7 @@ object Opening {
val attempts = "attempts"
val wins = "wins"
val perf = "perf"
val rating = s"$perf.gl.r"
}
implicit val openingBSONHandler = new BSON[Opening] {

View file

@ -58,9 +58,10 @@ private[opening] final class OpeningApi(
private val PlayedIdsGroup = Group(BSONBoolean(true))("ids" -> Push(Attempt.BSONFields.openingId))
def playedIds(user: User): Fu[BSONArray] = {
def playedIds(user: User, max: Int): Fu[BSONArray] = {
val command = Aggregate(attemptColl.name, Seq(
Match(BSONDocument(Attempt.BSONFields.userId -> user.id)),
Limit(max),
PlayedIdsGroup
))
attemptColl.db.command(command) map {

View file

@ -4,7 +4,7 @@ import scala.concurrent.duration._
import scala.util.Random
import reactivemongo.api.QueryOpts
import reactivemongo.bson.BSONDocument
import reactivemongo.bson.{ BSONDocument, BSONInteger, BSONArray }
import reactivemongo.core.commands.Count
import lila.db.Types.Coll
@ -12,19 +12,36 @@ import lila.user.User
private[opening] final class Selector(
openingColl: Coll,
api: OpeningApi) {
api: OpeningApi,
toleranceStep: Int,
toleranceMax: Int,
modulo: Int) {
val anonSkipMax = 10000
val anonSkipMax = 2000
def apply(me: Option[User]): Fu[Opening] = me match {
case None =>
openingColl.find(BSONDocument())
.options(QueryOpts(skipN = Random nextInt anonSkipMax))
.one[Opening] flatten "Can't find a opening for anon player!"
case Some(user) => api.attempt playedIds user flatMap { ids =>
openingColl.find(BSONDocument(
Opening.BSONFields.id -> BSONDocument("$nin" -> ids)
)).one[Opening] flatten s"Can't find a opening for user $user!"
case Some(user) => api.attempt.playedIds(user, modulo) flatMap { ids =>
tryRange(user, toleranceStep, ids)
} recoverWith {
case e: Exception => apply(none)
}
}
private def tryRange(user: User, tolerance: Int, ids: BSONArray): Fu[Opening] =
openingColl.find(BSONDocument(
Opening.BSONFields.id -> BSONDocument("$nin" -> ids),
Opening.BSONFields.rating -> BSONDocument(
"$gt" -> BSONInteger(user.perfs.opening.intRating - tolerance),
"$lt" -> BSONInteger(user.perfs.opening.intRating + tolerance)
)
)).one[Opening] flatMap {
case Some(opening) => fuccess(opening)
case None => if ((tolerance + toleranceStep) <= toleranceMax)
tryRange(user, tolerance + toleranceStep, ids)
else fufail(s"Can't find a opening for user $user!")
}
}

View file

@ -10,7 +10,7 @@ object ApplicationBuild extends Build {
import Dependencies._
lazy val root = Project("lila", file(".")) enablePlugins PlayScala settings (
scalaVersion := "2.11.4",
scalaVersion := globalScalaVersion,
resolvers ++= Dependencies.Resolvers.commons,
scalacOptions := compilerOptions,
offline := true,

View file

@ -1,13 +1,15 @@
import sbt._, Keys._
import play.Play.autoImport._
import sbt._, Keys._
object BuildSettings {
import Dependencies._
val globalScalaVersion = "2.11.5"
def buildSettings = Defaults.defaultSettings ++ Seq(
organization := "org.lichess",
scalaVersion := "2.11.4",
scalaVersion := globalScalaVersion,
resolvers ++= Dependencies.Resolvers.commons,
parallelExecution in Test := false,
scalacOptions := compilerOptions,

View file

@ -364,17 +364,17 @@ body.dark .loader:before,
body.dark .loader:after {
background: #1a1a1a;
}
body.dark #puzzle > .side div.fail,
body.dark #puzzle > .side div.loss {
body.dark div.training > .side div.fail,
body.dark div.training > .side div.loss {
color: #e97472;
background: #582b33;
}
body.dark #puzzle > .side div.retry {
body.dark div.training > .side div.retry {
color: #8482c9;
background: #101034;
}
body.dark #puzzle > .side div.great,
body.dark #puzzle > .side div.win,
body.dark div.training > .side div.great,
body.dark div.training > .side div.win,
body.dark #puzzle > .right .please_vote {
background: #103410;
color: #74a962;

View file

@ -38,6 +38,7 @@
margin-top: 20px;
}
#opening .meter ul {
width: 490px;
margin-top: 13px;
padding: 0;
display: block;

View file

@ -6,44 +6,6 @@
#puzzle [data-icon]::before {
margin-right: 3px;
}
#puzzle > .side div.comment {
padding: 10px 5px;
text-align: center;
margin-bottom: 1em;
}
#puzzle > .side div.comment h3 {
font-size: inherit;
margin: 4px;
display: block;
}
#puzzle > .side div.comment .rating {
font-size: 2em;
vertical-align: -3px;
padding-left: 5px;
}
#puzzle > .side div.retry {
color: #31708f;
background: #D9EDF7;
}
#puzzle.retry > .side div.retry {
display: block;
}
#puzzle > .side div.great,
#puzzle > .side div.win {
background: #DFF0D8;
color: #3c763d;
}
#puzzle.great > .side div.great {
display: block;
}
#puzzle > .side div.loss,
#puzzle > .side div.fail {
color: #a94442;
background: #ebccd1;
}
#puzzle.fail > .side div.fail {
display: block;
}
#puzzle > .side .difficulty {
margin-bottom: 2em;
}

View file

@ -16,6 +16,44 @@ div.training > .side h1 {
font-weight: bold;
margin-bottom: 10px;
}
div.training > .side div.comment {
padding: 10px 5px;
text-align: center;
margin-bottom: 1em;
}
div.training > .side div.comment h3 {
font-size: inherit;
margin: 4px;
display: block;
}
div.training > .side div.retry {
color: #31708f;
background: #D9EDF7;
}
div.training > .side div.great,
div.training > .side div.win {
background: #DFF0D8;
color: #3c763d;
}
div.training > .side div.loss,
div.training > .side div.fail {
color: #a94442;
background: #ebccd1;
}
div.training > .side div.comment .rating {
font-size: 2em;
vertical-align: -3px;
padding-left: 5px;
}
div.training.retry > .side div.retry {
display: block;
}
div.training.great > .side div.great {
display: block;
}
div.training.fail > .side div.fail {
display: block;
}
div.training > .side .buttonset {
text-align: center;
margin-top: 2em;

View file

@ -24,6 +24,7 @@ module.exports = function(cfg, router, i18n) {
loading: false,
flash: {},
flashFound: null,
comment: null
};
}.bind(this);
initialize();
@ -80,6 +81,13 @@ module.exports = function(cfg, router, i18n) {
turnColor: this.data.opening.color,
check: init.check,
autoCastle: true,
animation: {
enabled: true,
duration: this.data.animation.duration
},
premovable: {
enabled: true
},
movable: {
color: this.data.opening.color,
free: false,
@ -104,6 +112,7 @@ module.exports = function(cfg, router, i18n) {
var known = this.data.opening.moves.filter(function(m) {
return m.uci === move.uci;
})[0];
this.comment = null;
if (known && known.quality === 'good') {
var alreadyFound = this.vm.figuredOut.filter(function(f) {
return f.uci === move.uci;
@ -112,12 +121,15 @@ module.exports = function(cfg, router, i18n) {
else {
flash(move, 'good');
this.vm.figuredOut.push(move);
this.vm.comment = 'good';
}
} else if (known && known.quality === 'dubious') {
flash(move, 'dubious');
this.vm.comment = 'dubious';
} else {
if (this.vm.messedUp.indexOf(move.uci) === -1) this.vm.messedUp.push(move);
flash(move, 'bad');
this.vm.comment = 'bad';
}
}.bind(this);

View file

@ -32,11 +32,11 @@ function renderPlayTable(ctrl) {
function renderViewTable(ctrl) {
return [
m('div.box', [
m('h2',
m('a', {
href: '/training/opening/' + ctrl.data.opening.id
}, ctrl.trans('openingId', ctrl.data.opening.id))
),
m('h2.text', {
'data-icon': ']'
}, m('a', {
href: '/training/opening/' + ctrl.data.opening.id
}, ctrl.trans('openingId', ctrl.data.opening.id))),
m('p', m.trust(ctrl.trans('ratingX', strong(ctrl.data.opening.rating)))),
m('p', m.trust(ctrl.trans('playedXTimes', strong(ctrl.data.opening.attempts)))),
m('p',
@ -74,6 +74,27 @@ function renderUserInfos(ctrl) {
]);
}
function renderCommentary(ctrl) {
switch (ctrl.vm.comment) {
case 'dubious':
return m('div.comment.retry', [
m('h3', m('strong', ctrl.trans('goodMove'))),
m('span', ctrl.trans('butYouCanDoBetter'))
]);
case 'good':
return m('div.comment.great', [
m('h3.text[data-icon=E]', m('strong', ctrl.trans('bestMove'))),
ctrl.vm.figuredOut.length < ctrl.data.goal ? m('span', ctrl.trans('keepGoing')) : null
]);
case 'bad':
return m('div.comment.fail', [
m('h3.text[data-icon=k]', m('strong', ctrl.trans('thisMoveGivesYourOpponentTheAdvantage')))
]);
default:
return ctrl.vm.comment
}
}
function renderTrainingBox(ctrl) {
return m('div.box', [
m('h1', ctrl.trans('training')),
@ -100,9 +121,50 @@ function renderTrainingBox(ctrl) {
]);
}
function renderRatingDiff(diff) {
return m('strong.rating', diff > 0 ? '+' + diff : diff);
}
function renderWin(ctrl, attempt) {
return m('div.comment.win', [
m('h3.text[data-icon=E]', [
m('strong', ctrl.trans('victory')),
attempt ? renderRatingDiff(attempt.userRatingDiff) : null
]),
attempt ? m('span', ctrl.trans('openingSolved')) : null
]);
}
function renderLoss(ctrl, attempt) {
return m('div.comment.loss',
m('h3.text[data-icon=k]', [
m('strong', ctrl.trans('openingFailed')),
attempt ? renderRatingDiff(attempt.userRatingDiff) : null
])
);
}
function renderResult(ctrl) {
switch (ctrl.data.win) {
case true:
return renderWin(ctrl, null);
case false:
return renderLoss(ctrl, null);
default:
switch (ctrl.data.attempt && ctrl.data.attempt.win) {
case true:
return renderWin(ctrl, ctrl.data.attempt);
case false:
return renderLoss(ctrl, ctrl.data.attempt);
}
}
}
function renderSide(ctrl) {
return m('div.side', [
renderTrainingBox(ctrl)
renderTrainingBox(ctrl),
ctrl.data.play ? renderCommentary(ctrl) : null,
renderResult(ctrl)
]);
}

View file

@ -17,7 +17,7 @@ module.exports = function(cfg, router, i18n) {
this.data = data(cfg);
this.userMove = function(orig, dest) {
var userMove = function(orig, dest) {
var res = puzzle.tryMove(this.data, [orig, dest]);
var newProgress = res[0];
var newLines = res[1];
@ -85,7 +85,7 @@ module.exports = function(cfg, router, i18n) {
free: false,
color: cfg.mode !== 'view' ? cfg.puzzle.color : null,
events: {
after: this.userMove
after: userMove
},
},
animation: {

View file

@ -187,7 +187,7 @@ function renderViewTable(ctrl) {
]) : null,
m('div.box', [
(ctrl.data.puzzle.enabled && ctrl.data.user) ? renderVote(ctrl) : null,
m('h2',
m('h2.text[data-icon="-"]',
m('a', {
href: ctrl.router.Puzzle.show(ctrl.data.puzzle.id).url
}, ctrl.trans('puzzleId', ctrl.data.puzzle.id))