opening coach WIP, using opening families and ECO

pull/742/head
Thibault Duplessis 2015-07-20 17:18:28 +02:00
parent a0d5293fbb
commit 534b5353e4
18 changed files with 364 additions and 48 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
todo 100644
View File

@ -0,0 +1 @@
make coach data reload async with creation/update flag in DB

View File

@ -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']);

View File

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

View File

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

View File

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

View File

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

View File

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