more work on opening trainer

This commit is contained in:
Thibault Duplessis 2015-01-05 18:27:27 +01:00
parent 22a8b0b0f7
commit df491b2c48
11 changed files with 441 additions and 29 deletions

View file

@ -30,7 +30,7 @@ active = siteMenu.puzzle.some) {
<h1>@trans.training()</h1>
<div class="tabs buttonset">
<a href="@routes.Puzzle.home()" class="button">Puzzle</a>
<a href="@routes.Coordinate.home()" class="button active">Coordinate</a>
<a href="@routes.Coordinate.home()" class="button active">Coord</a>
</div>
@if(ctx.isAuth) {
@scoreOption.map { score =>

View file

@ -19,13 +19,19 @@ object JsData extends lila.Steroids {
"attempts" -> opening.attempts,
"fen" -> opening.fen,
"color" -> opening.color.name,
"moves" -> JsArray(opening.scoredMoves.map {
case ScoredMove(move, score) => Json.obj(
"moves" -> JsArray(opening.qualityMoves.map {
case QualityMove(move, quality) => Json.obj(
"first" -> move.first,
"cp" -> move.cp,
"line" -> move.line.mkString(" "),
"score" -> score.name)
"quality" -> quality.name)
}),
"url" -> s"$netBaseUrl${routes.Opening.show(opening.id)}"
))))
),
"user" -> userInfos.map { i =>
Json.obj(
"score" -> i.score,
"history" -> i.history.nonEmpty.option(Json.toJson(i.chart))
)
})))
}

View file

@ -9,7 +9,10 @@ document.querySelector('#lichess .round'),
@JsData(opening, userInfos),
openingRoutes.controllers,
@Html(J.stringify(i18nJsObject(
trans.training
trans.training,
trans.toTrackYourProgress,
trans.signUp,
trans.trainingSignupExplanation
)))
);
}

View file

@ -17,29 +17,40 @@ case class Opening(
attempts: Int,
score: Double) {
def scoredMoves = moves.map { move =>
ScoredMove(move, Score.Good)
def qualityMoves: List[QualityMove] = {
val bestCp = moves.foldLeft(Int.MaxValue) {
case (cp, move) => if (move.cp < cp) move.cp else cp
}
moves.map { move =>
QualityMove(move, Quality(move.cp - bestCp))
}
}
}
sealed trait Score {
def name = toString.toLowerCase
sealed abstract class Quality(val threshold: Int) {
val name = toString.toLowerCase
}
object Score {
case object Great extends Score
case object Good extends Score
case object Dubious extends Score
case object Bad extends Score
object Quality {
case object Good extends Quality(30)
case object Dubious extends Quality(80)
case object Bad extends Quality(Int.MaxValue)
def apply(cp: Int) =
if (cp < Good.threshold) Good
else if (cp < Dubious.threshold) Dubious
else Bad
}
case class ScoredMove(
case class QualityMove(
move: Move,
score: Score)
quality: Quality)
object Opening {
type ID = Int
val defaultScore = 50
def make(
fen: String,
color: Color,

View file

@ -2,27 +2,33 @@ package lila.opening
import reactivemongo.bson._
import reactivemongo.bson.Macros
import play.api.libs.json._
import lila.db.Types.Coll
import lila.rating.Glicko
import lila.user.User
case class UserInfos(user: User, history: List[Attempt])
case class UserInfos(user: User, history: List[Attempt], chart: JsArray) {
def score = if (history.isEmpty) 50f
else history.foldLeft(0)(_ + _.score) / history.size
}
object UserInfos {
private def historySize = 20
private def chartSize = 12
import Attempt.attemptBSONHandler
def apply(attemptColl: Coll) = new {
def apply(user: User): Fu[UserInfos] = fetchAttempts(user.id) map { attempts =>
new UserInfos(user, makeHistory(attempts))
new UserInfos(user, makeHistory(attempts), makeChart(attempts))
} recover {
case e: Exception =>
play.api.Logger("Puzzle UserInfos").error(e.getMessage)
new UserInfos(user, Nil)
play.api.Logger("Opening UserInfos").error(e.getMessage)
new UserInfos(user, Nil, JsArray())
}
def apply(user: Option[User]): Fu[Option[UserInfos]] =
@ -33,8 +39,14 @@ object UserInfos {
Attempt.BSONFields.userId -> userId
)).sort(BSONDocument(
Attempt.BSONFields.date -> -1
)).cursor[Attempt].collect[List](historySize)
)).cursor[Attempt].collect[List](math.max(historySize, chartSize))
}
private def makeHistory(attempts: List[Attempt]) = attempts.take(historySize)
private def makeChart(attempts: List[Attempt]) = JsArray {
val scores = attempts.take(chartSize).reverse map (_.score)
val filled = List.fill(chartSize - scores.size)(Glicko.default.intRating) ::: scores
filled map { JsNumber(_) }
}
}

File diff suppressed because one or more lines are too long

View file

@ -30,9 +30,10 @@
"watchify": "^1.0.2"
},
"dependencies": {
"chessground": "1.8.0",
"chessground": "1.8.7",
"chessli.js": "file:../chessli",
"lodash-node": "^2.4.1",
"merge": "^1.2.0",
"mithril": "0.1.24"
"mithril": "0.1.28"
}
}

View file

@ -1,23 +1,95 @@
var m = require('mithril');
var chessground = require('chessground');
var Chess = require('chessli.js').Chess;
module.exports = function(cfg, router, i18n) {
this.data = cfg;
console.log(this.data);
this.vm = {
nbGood: this.data.opening.moves.filter(function(m) {
return m.quality === 'good';
}).length,
figuredOut: [],
messedUp: [],
flash: null
};
var chess = new Chess(this.data.opening.fen);
var init = {
dests: chess.dests(),
check: chess.in_check()
};
var onMove = function(orig, dest, meta) {
submitMove(orig + dest);
setTimeout(function() {
this.chessground.set({
fen: this.data.opening.fen,
lastMove: null,
turnColor: this.data.opening.color,
check: init.check,
premovable: {
enabled: false
},
movable: {
dests: init.dests
}
});
}.bind(this), 1000);
m.redraw();
}.bind(this);
this.chessground = new chessground.controller({
fen: this.data.opening.fen,
orientation: this.data.opening.color,
viewOnly: true,
turnColor: this.data.opening.color,
check: init.check,
movable: {
color: this.data.opening.color,
free: false,
dests: init.dests,
events: {
after: onMove
}
},
});
var submitMove = function(move) {
var found = this.data.opening.moves.filter(function(m) {
return m.first === move;
})[0];
if (found && found.quality === 'good') {
if (this.vm.figuredOut.indexOf(move) === -1) this.vm.figuredOut.push(move);
} else if (found && found.quality === 'dubious') {
flash('dubious');
} else if (!found || found.quality === 'bad') {
if (this.vm.messedUp.indexOf(move) === -1) this.vm.messedUp.push(move);
flash('bad');
}
}.bind(this);
var flash = function(f) {
this.vm.flash = f;
setTimeout(function() {
this.vm.flash = null;
m.redraw();
}.bind(this), 1000);
}.bind(this);
this.router = router;
this.trans = function() {
var str = i18n[arguments[0]]
var str = i18n[arguments[0]] || untranslated[arguments[0]] || arguments[0];
Array.prototype.slice.call(arguments, 1).forEach(function(arg) {
str = str.replace('%s', arg);
});
return str;
};
var untranslated = {
yourOpeningScoreX: 'Your opening score: %s',
findNbGoodMoves: 'Find %s good moves',
};
};

View file

@ -1,3 +1,4 @@
var m = require('mithril');
var ctrl = require('./ctrl');
var view = require('./view');
@ -7,5 +8,8 @@ module.exports = function(element, config, router, i18n) {
controller: function () { return controller; },
view: view
});
controller.initiate();
};
// lol, that's for the rest of lichess to access mithril
// without having to include it a second time
window.Chessground = require('chessground');

View file

@ -1,5 +1,9 @@
var chessground = require('chessground');
var m = require('mithril');
var chessground = require('chessground');
function strong(txt) {
return '<strong>' + txt + '</strong>';
}
function renderTable(ctrl) {
return m('div.table',
@ -7,13 +11,101 @@ function renderTable(ctrl) {
);
}
function renderUserInfos(ctrl) {
return m('div.chart_container', [
m('p', m.trust(ctrl.trans('yourOpeningScoreX', strong(ctrl.data.user.score)))),
ctrl.data.user.history ? m('div.user_chart', {
config: function(el, isUpdate, context) {
var hash = ctrl.data.user.history.join('');
if (hash == context.hash) return;
context.hash = hash;
var dark = document.body.classList.contains('dark');
jQuery(el).sparkline(ctrl.data.user.history, {
type: 'line',
width: '213px',
height: '80px',
lineColor: dark ? '#4444ff' : '#0000ff',
fillColor: dark ? '#222255' : '#ccccff'
});
}
}) : null
]);
}
function renderTrainingBox(ctrl) {
return m('div.box', [
m('h1', ctrl.trans('training')),
m('div.tabs.buttonset', [
m('a.button', {
href: '/training'
}, 'Puzzle'),
m('a.button', {
href: '/training/coordinate'
}, 'Coord'),
m('a.button.active', {
href: '/training/opening'
}, 'Opening'),
]),
ctrl.data.user ? renderUserInfos(ctrl) : m('div.register', [
m('p', ctrl.trans('toTrackYourProgress')),
m('p.signup',
m('a.button', {
href: '/signup',
}, ctrl.trans('signUp'))
),
m('p', ctrl.trans('trainingSignupExplanation'))
])
]);
}
function renderResult(ctrl) {
return [
m('div.goal', m.trust(ctrl.trans('findNbGoodMoves', strong(ctrl.vm.nbGood)))),
];
}
function renderSide(ctrl) {
return m('div.side', [
renderTrainingBox(ctrl),
renderResult(ctrl)
]);
}
module.exports = function(ctrl) {
var percent = Math.ceil(ctrl.vm.figuredOut.length * 100 / ctrl.vm.nbGood) + '%';
var color;
switch (ctrl.vm.flash) {
case 'dubious':
color = 'yellow';
break;
case 'bad':
color = 'red';
break;
default:
color = 'green';
}
return m('div#opening.training', [
renderSide(ctrl),
m('div.board_and_ground', [
m('div', chessground.view(ctrl.chessground)),
m('div.right', renderTable(ctrl))
]),
m('div.center', [
m('div.lulzbar', [
m('div.bar.' + color, {
style: {
width: percent
}
}, m('span')),
m('div.label', {
style: {
left: percent
}
}, [
m('span'),
m('div.perc', percent)
])
])
])
]);
};

View file

@ -40,7 +40,7 @@ function renderTrainingBox(ctrl) {
}, 'Puzzle'),
m('a.button', {
href: '/training/coordinate'
}, 'Coordinate')
}, 'Coord')
]),
ctrl.data.user ? renderUserInfos(ctrl) : m('div.register', [
m('p', ctrl.trans('toTrackYourProgress')),