more work on opening trainer
This commit is contained in:
parent
22a8b0b0f7
commit
df491b2c48
|
@ -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 =>
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
})))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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)
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
|
|
@ -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')),
|
||||
|
|
Loading…
Reference in a new issue