opening coach WIP, using opening families and ECO
parent
a0d5293fbb
commit
534b5353e4
|
@ -22,7 +22,7 @@ object Coach extends LilaController {
|
|||
lila.user.UserRepo named username flatMap {
|
||||
case None => fuccess(NotFound(Json.obj("error" -> s"User $username not found")))
|
||||
case Some(u) => env.share.grant(u, ctx.me) flatMap {
|
||||
case true => env.statApi.fetchOrCompute(u.id) flatMap env.jsonView.apply map { Ok(_) }
|
||||
case true => env.statApi.fetchOrCompute(u.id) flatMap env.jsonView.raw map { Ok(_) }
|
||||
case false => fuccess(Forbidden(Json.obj("error" -> s"User $username data is protected")))
|
||||
}
|
||||
} map (_ as JSON)
|
||||
|
@ -30,8 +30,12 @@ object Coach extends LilaController {
|
|||
|
||||
def opening(username: String) = Open { implicit ctx =>
|
||||
Accessible(username) { user =>
|
||||
env.statApi.fetch(user.id) map { stat =>
|
||||
Ok(html.coach.opening(user, stat))
|
||||
env.statApi.fetch(user.id) flatMap { stat =>
|
||||
stat ?? { s =>
|
||||
env.jsonView.opening(s).map(json => (s, json).some)
|
||||
} map { data =>
|
||||
Ok(html.coach.opening(user, data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
@(title: String, side: Option[Html] = None, robots: Boolean = true, evenMoreJs: Html = Html(""), evenMoreCss: Html = Html(""), openGraph: Option[lila.app.ui.OpenGraph] = None)(body: Html)(implicit ctx: Context)
|
||||
@(title: String, side: Option[Html] = None, robots: Boolean = true, moreJs: Html = Html(""), evenMoreCss: Html = Html(""), openGraph: Option[lila.app.ui.OpenGraph] = None, chessground: Boolean = true)(body: Html)(implicit ctx: Context)
|
||||
|
||||
@moreCss = {
|
||||
@cssTag("coach.css")
|
||||
@evenMoreCss
|
||||
}
|
||||
|
||||
@moreJs = {
|
||||
@evenMoreJs
|
||||
}
|
||||
|
||||
@base.layout(
|
||||
title = title,
|
||||
side = side,
|
||||
moreCss = moreCss,
|
||||
moreJs = moreJs,
|
||||
robots = robots,
|
||||
chessground = chessground,
|
||||
openGraph = openGraph)(body)
|
||||
|
|
|
@ -1,5 +1,28 @@
|
|||
@(u: User, stat: Option[lila.coach.UserStat])(implicit ctx: Context)
|
||||
|
||||
@coach.layout(title = s"${u.username} openingss") {
|
||||
@(u: User, statOption: Option[(lila.coach.UserStat, play.api.libs.json.JsObject)])(implicit ctx: Context)
|
||||
|
||||
@moreJs = {
|
||||
@highchartsTag
|
||||
@statOption.map {
|
||||
case (stat, data) => {
|
||||
@jsAt(s"compiled/lichess.coach.opening${isProd??(".min")}.js")
|
||||
@embedJs {
|
||||
LichessCoachOpening(document.getElementById('coach_opening'), {
|
||||
data: @Html(J.stringify(data))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@coach.layout(
|
||||
title = s"${u.username} openings",
|
||||
moreJs = moreJs,
|
||||
chessground = false) {
|
||||
|
||||
<div class="content_box">
|
||||
|
||||
<h1 class="lichess_title">@userLink(u) openings</h1>
|
||||
|
||||
<div id="coach_opening"></div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package lila.analyse
|
||||
|
||||
import chess.format.pgn.{ Pgn, Tag, Turn, Move }
|
||||
import chess.OpeningExplorer.Opening
|
||||
import chess.Opening
|
||||
import chess.{ Status, Color, Clock }
|
||||
|
||||
private[analyse] final class Annotator(netDomain: String) {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
package lila.coach
|
||||
|
||||
import play.api.libs.json._
|
||||
|
||||
private[coach] object JSONWriters {
|
||||
|
||||
implicit val SectionWriter = OWrites[GameSections.Section] { s =>
|
||||
Json.obj(
|
||||
"nb" -> s.nb,
|
||||
"nbAnalysed" -> s.nbAnalysed,
|
||||
"moveAvg" -> s.moveAvg,
|
||||
"acplAvg" -> s.acplAvg)
|
||||
}
|
||||
implicit val GameSectionsWriter = Json.writes[GameSections]
|
||||
implicit val BestWinWriter = Json.writes[Results.BestWin]
|
||||
implicit val ResultsWriter = Json.writes[Results]
|
||||
implicit val ColorResultsWriter = Json.writes[ColorResults]
|
||||
implicit val OpeningsMapWriter = Writes[Openings.OpeningsMap] { o => Json.toJson(o.m) }
|
||||
implicit val OpeningsWriter = Json.writes[Openings]
|
||||
implicit val PerfResultsBestRatingWriter = Json.writes[PerfResults.BestRating]
|
||||
implicit val PerfResultsStatusMapWriter = OWrites[Map[chess.Status, Int]] { m =>
|
||||
JsObject(m.map { case (status, i) => status.name -> JsNumber(i) })
|
||||
}
|
||||
implicit val PerfResultsStreakWriter = Json.writes[PerfResults.Streak]
|
||||
implicit val PerfResultsStatusScoresWriter = Json.writes[PerfResults.StatusScores]
|
||||
implicit val PerfResultsOutcomeStatusesWriter = Json.writes[PerfResults.OutcomeStatuses]
|
||||
implicit val PerfResultsWriter = Json.writes[PerfResults]
|
||||
implicit val PerfResultsPerfMapWriter = OWrites[Map[lila.rating.PerfType, PerfResults]] { m =>
|
||||
JsObject(m.map { case (pt, res) => pt.key -> PerfResultsWriter.writes(res) })
|
||||
}
|
||||
implicit val PerfResultsMapWriter = Json.writes[PerfResults.PerfResultsMap]
|
||||
implicit val UserStatWriter = Json.writes[UserStat]
|
||||
|
||||
implicit val OpeningWriter: OWrites[chess.Opening] = OWrites { o =>
|
||||
Json.obj(
|
||||
"code" -> o.code,
|
||||
"name" -> o.name,
|
||||
"moves" -> o.moves
|
||||
)
|
||||
}
|
||||
|
||||
implicit val OpeningFamilyWriter = Json.writes[OpeningFamily]
|
||||
implicit val OpeningFamiliesWriter = Json.writes[OpeningFamilies]
|
||||
implicit val OpeningApiDataWriter = Json.writes[OpeningApiData]
|
||||
}
|
|
@ -4,34 +4,30 @@ import play.api.libs.json._
|
|||
|
||||
final class JsonView {
|
||||
|
||||
def apply(userStat: UserStat): Fu[JsObject] = fuccess {
|
||||
UserStatWriter writes userStat
|
||||
import JSONWriters._
|
||||
|
||||
def raw(stat: UserStat): Fu[JsObject] = fuccess {
|
||||
UserStatWriter writes stat
|
||||
}
|
||||
|
||||
private implicit val SectionWriter = OWrites[GameSections.Section] { s =>
|
||||
Json.obj(
|
||||
"nb" -> s.nb,
|
||||
"nbAnalysed" -> s.nbAnalysed,
|
||||
"moveAvg" -> s.moveAvg,
|
||||
"acplAvg" -> s.acplAvg)
|
||||
def opening(stat: UserStat): Fu[JsObject] = fuccess {
|
||||
OpeningApiDataWriter writes OpeningApiData(
|
||||
results = stat.results.base,
|
||||
colorResults = stat.colorResults,
|
||||
openings = stat.openings,
|
||||
families = OpeningFamilies(
|
||||
white = familiesOf(stat.openings.white),
|
||||
black = familiesOf(stat.openings.black)
|
||||
)
|
||||
)
|
||||
}
|
||||
private implicit val GameSectionsWriter = Json.writes[GameSections]
|
||||
private implicit val BestWinWriter = Json.writes[Results.BestWin]
|
||||
private implicit val ResultsWriter = Json.writes[Results]
|
||||
private implicit val ColorResultsWriter = Json.writes[ColorResults]
|
||||
private implicit val OpeningsMapWriter = Json.writes[Openings.OpeningsMap]
|
||||
private implicit val OpeningsWriter = Json.writes[Openings]
|
||||
private implicit val PerfResultsBestRatingWriter = Json.writes[PerfResults.BestRating]
|
||||
private implicit val PerfResultsStatusMapWriter = OWrites[Map[chess.Status, Int]] { m =>
|
||||
JsObject(m.map { case (status, i) => status.name -> JsNumber(i) })
|
||||
}
|
||||
private implicit val PerfResultsStreakWriter = Json.writes[PerfResults.Streak]
|
||||
private implicit val PerfResultsStatusScoresWriter = Json.writes[PerfResults.StatusScores]
|
||||
private implicit val PerfResultsOutcomeStatusesWriter = Json.writes[PerfResults.OutcomeStatuses]
|
||||
private implicit val PerfResultsWriter = Json.writes[PerfResults]
|
||||
private implicit val PerfResultsPerfMapWriter = OWrites[Map[lila.rating.PerfType, PerfResults]] { m =>
|
||||
JsObject(m.map { case (pt, res) => pt.key -> PerfResultsWriter.writes(res) })
|
||||
}
|
||||
private implicit val PerfResultsMapWriter = Json.writes[PerfResults.PerfResultsMap]
|
||||
private implicit val UserStatWriter = Json.writes[UserStat]
|
||||
|
||||
private def familiesOf(opMap: Openings.OpeningsMap) =
|
||||
opMap.m.foldLeft(Map[String, List[String]]()) {
|
||||
case (acc, (code, _)) => chess.Openings.codeFamily.get(code).fold(acc) { family =>
|
||||
acc + (family -> (code :: acc.getOrElse(family, Nil)))
|
||||
}
|
||||
}.map {
|
||||
case (family, codes) => OpeningFamily(family, codes)
|
||||
}.toList
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ case class Openings(
|
|||
black: Openings.OpeningsMap) {
|
||||
|
||||
def apply(c: Color) = c.fold(white, black)
|
||||
|
||||
def all = white ++ black
|
||||
}
|
||||
|
||||
object Openings {
|
||||
|
@ -16,6 +18,7 @@ object Openings {
|
|||
case class OpeningsMap(m: Map[String, Results]) {
|
||||
def best(max: Int): List[(String, Results)] = m.toList.sortBy(-_._2.nbGames) take max
|
||||
def trim(max: Int) = copy(m = best(max).toMap)
|
||||
def ++(other: OpeningsMap) = OpeningsMap(m ++ other.m)
|
||||
}
|
||||
val emptyOpeningsMap = OpeningsMap(Map.empty)
|
||||
|
||||
|
@ -36,8 +39,8 @@ object Openings {
|
|||
ops + (code -> ops.get(code).|(Results.emptyComputation).aggregate(p))
|
||||
|
||||
def run = Openings(
|
||||
white = OpeningsMap(white.mapValues(_.run)) trim 10,
|
||||
black = OpeningsMap(black.mapValues(_.run)) trim 10)
|
||||
white = OpeningsMap(white.mapValues(_.run)) trim 15,
|
||||
black = OpeningsMap(black.mapValues(_.run)) trim 15)
|
||||
}
|
||||
val emptyComputation = Computation(Map.empty, Map.empty)
|
||||
}
|
||||
|
|
|
@ -6,3 +6,12 @@ case class RichPov(
|
|||
analysis: Option[lila.analyse.Analysis],
|
||||
division: chess.Division,
|
||||
accuracy: Option[lila.analyse.Accuracy.DividedAccuracy])
|
||||
|
||||
case class OpeningFamily(name: String, codes: List[String])
|
||||
case class OpeningFamilies(white: List[OpeningFamily], black: List[OpeningFamily])
|
||||
|
||||
case class OpeningApiData(
|
||||
results: Results,
|
||||
colorResults: ColorResults,
|
||||
openings: Openings,
|
||||
families: OpeningFamilies)
|
||||
|
|
|
@ -444,7 +444,7 @@ case class Game(
|
|||
|
||||
def resetTurns = copy(turns = 0, startedAtTurn = 0)
|
||||
|
||||
lazy val opening: Option[chess.OpeningExplorer.Opening] =
|
||||
lazy val opening: Option[chess.Opening] =
|
||||
if (playable || fromPosition || !Game.openingSensiblevariants(variant)) none
|
||||
else chess.OpeningExplorer openingOf pgnMoves
|
||||
|
||||
|
|
|
@ -20,13 +20,13 @@ case object Identified {
|
|||
|
||||
private lazy val movesPerName: Map[Name, Moves] =
|
||||
chess.Openings.db.foldLeft(Map[Name, Moves]()) {
|
||||
case (outerAcc, (_, fullName, moves)) => List(1, 2).foldLeft(outerAcc) {
|
||||
case (outerAcc, opening) => List(1, 2).foldLeft(outerAcc) {
|
||||
case (acc, length) =>
|
||||
val name = fullName.split(',').take(length).mkString(",")
|
||||
val name = opening.fullName.split(',').take(length).mkString(",")
|
||||
acc get name match {
|
||||
case None => acc + (name -> moves)
|
||||
case Some(ms) if moves.size < ms.size => acc + (name -> moves)
|
||||
case _ => acc
|
||||
case None => acc + (name -> opening.moves)
|
||||
case Some(ms) if opening.size < ms.size => acc + (name -> opening.moves)
|
||||
case _ => acc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -340,7 +340,7 @@ object JsonView {
|
|||
"emerg" -> c.emerg)
|
||||
}
|
||||
|
||||
implicit val openingWriter: OWrites[chess.OpeningExplorer.Opening] = OWrites { o =>
|
||||
implicit val openingWriter: OWrites[chess.Opening] = OWrites { o =>
|
||||
Json.obj(
|
||||
"code" -> o.code,
|
||||
"name" -> o.name,
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
var source = require('vinyl-source-stream');
|
||||
var gulp = require('gulp');
|
||||
var gutil = require('gulp-util');
|
||||
var watchify = require('watchify');
|
||||
var browserify = require('browserify');
|
||||
var uglify = require('gulp-uglify');
|
||||
var streamify = require('gulp-streamify');
|
||||
|
||||
var sources = ['./src/main.js'];
|
||||
var destination = '../../public/compiled/';
|
||||
var onError = function(error) {
|
||||
gutil.log(gutil.colors.red(error.message));
|
||||
};
|
||||
var standalone = 'LichessCoachOpening';
|
||||
|
||||
gulp.task('prod', function() {
|
||||
return browserify('./src/main.js', {
|
||||
standalone: standalone
|
||||
}).bundle()
|
||||
.on('error', onError)
|
||||
.pipe(source('lichess.coach.opening.min.js'))
|
||||
.pipe(streamify(uglify()))
|
||||
.pipe(gulp.dest(destination));
|
||||
});
|
||||
|
||||
gulp.task('dev', function() {
|
||||
return browserify('./src/main.js', {
|
||||
standalone: standalone
|
||||
}).bundle()
|
||||
.on('error', onError)
|
||||
.pipe(source('lichess.coach.opening.js'))
|
||||
.pipe(gulp.dest(destination));
|
||||
});
|
||||
|
||||
gulp.task('watch', function() {
|
||||
var opts = watchify.args;
|
||||
opts.debug = true;
|
||||
opts.standalone = standalone;
|
||||
|
||||
var bundleStream = watchify(browserify(sources, opts))
|
||||
.on('update', rebundle)
|
||||
.on('log', gutil.log);
|
||||
|
||||
function rebundle() {
|
||||
return bundleStream.bundle()
|
||||
.on('error', onError)
|
||||
.pipe(source('lichess.coach.opening.js'))
|
||||
.pipe(gulp.dest(destination));
|
||||
}
|
||||
|
||||
return rebundle();
|
||||
});
|
||||
|
||||
gulp.task('default', ['watch']);
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "tournamentSchedule",
|
||||
"version": "1.0.0",
|
||||
"description": "lichess.org opening coach",
|
||||
"main": "src/main.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ornicar/lila"
|
||||
},
|
||||
"keywords": [
|
||||
"chess",
|
||||
"lichess",
|
||||
"coach",
|
||||
"opening"
|
||||
],
|
||||
"author": "ornicar",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ornicar/lila/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ornicar/lila",
|
||||
"devDependencies": {
|
||||
"browserify": "~9.0.8",
|
||||
"gulp": "~3.9.0",
|
||||
"gulp-streamify": "~0.0.5",
|
||||
"gulp-uglify": "~1.2.0",
|
||||
"gulp-util": "~3.0.4",
|
||||
"vinyl-source-stream": "~1.1.0",
|
||||
"watchify": "~3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"chessground": "2.9.1",
|
||||
"mithril": "0.2.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
var m = require('mithril');
|
||||
|
||||
module.exports = function(opts) {
|
||||
|
||||
this.data = opts.data;
|
||||
window.openingData = opts.data;
|
||||
|
||||
this.trans = function(key) {
|
||||
var str = env.i18n[key] || key;
|
||||
Array.prototype.slice.call(arguments, 1).forEach(function(arg) {
|
||||
str = str.replace('%s', arg);
|
||||
});
|
||||
return str;
|
||||
};
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
module.exports = function(el, data, color) {
|
||||
var totalGames = data.colorResults[color].nbGames;
|
||||
var percent = function(nb) {
|
||||
return nb * 100 / totalGames;
|
||||
};
|
||||
var colors = Highcharts.getOptions().colors,
|
||||
categories = data.families[color].map(function(f) {
|
||||
return f.name;
|
||||
}),
|
||||
data = data.families[color].map(function(family, index) {
|
||||
var graphColor = colors[index];
|
||||
return {
|
||||
y: percent(family.codes.reduce(function(acc, code) {
|
||||
var op = data.openings[color][code];
|
||||
return acc + (op ? op.nbGames : 0);
|
||||
}, 0)),
|
||||
color: graphColor,
|
||||
drilldown: {
|
||||
name: family.name,
|
||||
categories: family.codes,
|
||||
data: family.codes.map(function(c) {
|
||||
return percent(data.openings[color][c].nbGames);
|
||||
}),
|
||||
color: graphColor
|
||||
}
|
||||
};
|
||||
}),
|
||||
familyData = [],
|
||||
codeData = [],
|
||||
i,
|
||||
j,
|
||||
dataLen = data.length,
|
||||
drillDataLen,
|
||||
brightness;
|
||||
|
||||
// Build the data arrays
|
||||
for (i = 0; i < dataLen; i += 1) {
|
||||
|
||||
// add browser data
|
||||
familyData.push({
|
||||
name: categories[i],
|
||||
y: data[i].y,
|
||||
color: data[i].color
|
||||
});
|
||||
|
||||
// add version data
|
||||
drillDataLen = data[i].drilldown.data.length;
|
||||
for (j = 0; j < drillDataLen; j += 1) {
|
||||
brightness = 0.2 - (j / drillDataLen) / 5;
|
||||
codeData.push({
|
||||
name: data[i].drilldown.categories[j],
|
||||
y: data[i].drilldown.data[j],
|
||||
color: Highcharts.Color(data[i].color).brighten(brightness).get()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Create the chart
|
||||
$(el).highcharts({
|
||||
chart: {
|
||||
type: 'pie'
|
||||
},
|
||||
title: {
|
||||
text: 'Openings played as ' + color
|
||||
},
|
||||
yAxis: {},
|
||||
plotOptions: {
|
||||
pie: {
|
||||
shadow: false,
|
||||
center: ['50%', '50%']
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
valueSuffix: '%'
|
||||
},
|
||||
series: [{
|
||||
name: 'Opening families',
|
||||
data: familyData,
|
||||
size: '60%',
|
||||
dataLabels: {
|
||||
formatter: function() {
|
||||
return this.y > 5 ? this.point.name : null;
|
||||
},
|
||||
color: 'white',
|
||||
distance: -30
|
||||
}
|
||||
}, {
|
||||
name: 'Opening codes',
|
||||
data: codeData,
|
||||
size: '80%',
|
||||
innerSize: '60%',
|
||||
dataLabels: {
|
||||
formatter: function() {
|
||||
// display only if larger than 1
|
||||
return this.y > 1 ? '<b>' + this.point.name + ':</b> ' + this.y + '%' : null;
|
||||
}
|
||||
}
|
||||
}]
|
||||
});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
var m = require('mithril');
|
||||
|
||||
var ctrl = require('./ctrl');
|
||||
var view = require('./view');
|
||||
|
||||
module.exports = function(element, opts) {
|
||||
|
||||
var controller = new ctrl(opts);
|
||||
|
||||
m.module(element, {
|
||||
controller: function() {
|
||||
return controller;
|
||||
},
|
||||
view: view
|
||||
});
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
var m = require('mithril');
|
||||
|
||||
var highchart = require('./highchart');
|
||||
|
||||
function colorChart(data, color) {
|
||||
return m('div.' + color, {
|
||||
config: function(el, isUpdate) {
|
||||
if (isUpdate) return;
|
||||
highchart(el, data, color);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = function(ctrl) {
|
||||
return m('div.charts ', ['white'].map(function(color) {
|
||||
return colorChart(ctrl.data, color);
|
||||
}));
|
||||
};
|
Loading…
Reference in New Issue