more work on user perf stats

pull/1352/head
Thibault Duplessis 2015-12-27 01:14:19 +07:00
parent cfa894dbfe
commit 5b40f990f3
9 changed files with 85 additions and 65 deletions

View File

@ -23,7 +23,6 @@ evenMoreJs = moreJs,
evenMoreCss = moreCss) {
<div class="content_box no_padding" id="perfStat">
<div class="content_box_top">
<span class="righty"><strong>@u.perfs(perfType).nb.localize</strong> @trans.games()</span>
<h1 class="lichess_title">
<a href="@routes.User.show(u.username)" data-icon="I" class="text">
@u.username <span>@perfType.name stats</span>

View File

@ -99,6 +99,8 @@ case class Game(
def updatedAtOrCreatedAt = updatedAt | createdAt
def durationSeconds = (updatedAtOrCreatedAt.getSeconds - createdAt.getSeconds).toInt
def lastMoveTimeInSeconds: Option[Int] = lastMoveTime.map(x => (x / 10).toInt)
// in tenths of seconds

View File

@ -42,7 +42,7 @@ object PerfStat {
lowest = none,
bestWins = Results(Nil),
worstLosses = Results(Nil),
count = Count(all = 0, rated = 0, win = 0, loss = 0, draw = 0, tour = 0, berserk = 0, opAvg = Avg(0, 0)),
count = Count.init,
resultStreak = ResultStreak(win = Streaks.init, loss = Streaks.init),
playStreak = PlayStreak(nb = Streaks.init, time = Streaks.init, lastDate = none)
)
@ -56,8 +56,8 @@ case class ResultStreak(win: Streaks, loss: Streaks) {
case class PlayStreak(nb: Streaks, time: Streaks, lastDate: Option[DateTime]) {
def agg(pov: Pov) = {
val cont = isContinued(pov.game.createdAt)
val seconds = (pov.game.updatedAtOrCreatedAt.getSeconds - pov.game.createdAt.getSeconds).toInt
val seconds = pov.game.durationSeconds
val cont = seconds < 3 * 60 * 60 && isContinued(pov.game.createdAt)
copy(
nb = nb(cont, pov)(1),
time = time(cont, pov)(seconds),
@ -105,7 +105,9 @@ case class Count(
draw: Int,
tour: Int,
berserk: Int,
opAvg: Avg) {
opAvg: Avg,
seconds: Int,
disconnects: Int) {
def apply(pov: Pov) = copy(
all = all + 1,
rated = rated + pov.game.rated.fold(1, 0),
@ -114,7 +116,17 @@ case class Count(
draw = draw + pov.win.isEmpty.fold(1, 0),
tour = tour + pov.game.isTournament.fold(1, 0),
berserk = berserk + pov.player.berserk.fold(1, 0),
opAvg = pov.opponent.stableRating.fold(opAvg)(opAvg.agg))
opAvg = pov.opponent.stableRating.fold(opAvg)(opAvg.agg),
seconds = seconds + (pov.game.durationSeconds match {
case s if s > 3 * 60 * 60 => 0
case s => s
}),
disconnects = disconnects + {
~pov.loss && pov.game.status == chess.Status.Timeout
}.fold(1, 0))
}
object Count {
val init = Count(all = 0, rated = 0, win = 0, loss = 0, draw = 0, tour = 0, berserk = 0, opAvg = Avg(0, 0), seconds = 0, disconnects = 0)
}
case class Avg(avg: Double, pop: Int) {

View File

@ -14,12 +14,6 @@
text-transform: uppercase;
margin-left: 10px;
}
#perfStat .content_box_top .righty {
float: right;
margin-right: 20px;
line-height: 30px;
text-transform: lowercase;
}
#perfStat h2 {
font-size: 1.5em;
text-transform: uppercase;
@ -37,6 +31,12 @@
#perfStat time {
display: inline;
}
#perfStat green {
color: #759900;
}
#perfStat red {
color: #dc322f;
}
#perfStat .content_box_content a {
color: #3893E8;
}
@ -98,7 +98,7 @@
}
#perfStat .result th,
#perfStat .result td {
padding: 12px 0;
padding: 10px 0;
}
#perfStat .result td:first-child {
text-indent: 20px;

View File

@ -1,14 +1,15 @@
var m = require('mithril');
var util = require('./util');
function percent(x, y) {
if (y == 0) return 'N/A';
return Math.round(x * 100 / y) + '%';
if (y === 0) return 0;
return Math.round(x * 100 / y);
}
module.exports = function(d) {
var c = d.stat.count;
var per = function(x, y) {
return percent(x, y || c.all);
return percent(x, y || c.all) + '%';
};
return [
m('div.half', m('table', m('tbody', [
@ -31,7 +32,11 @@ module.exports = function(d) {
m('th', 'Berserked games'),
m('td', c.berserk),
m('td', per(c.berserk, c.tour)),
])
]),
c.seconds ? m('tr.full', [
m('th', 'Time spent playing'),
m('td[colspan=2]', util.formatSeconds(c.seconds, 'short'))
]) : null
]))),
m('div.half', m('table', m('tbody', [
m('tr', [
@ -41,8 +46,8 @@ module.exports = function(d) {
]),
m('tr.full', [
m('th', 'Victories'),
m('td', c.win),
m('td', per(c.win))
m('td', util.green(c.win)),
m('td', util.green(per(c.win)))
]),
m('tr.full', [
m('th', 'Draws'),
@ -51,9 +56,16 @@ module.exports = function(d) {
]),
m('tr.full', [
m('th', 'Defeats'),
m('td', c.loss),
m('td', per(c.loss))
])
m('td', util.red(c.loss)),
m('td', util.red(per(c.loss)))
]),
m('tr.full', (function(color) {
return [
m('th', 'Disconnections'),
m('td', color(c.disconnects)),
m('td', color(per(c.disconnects, c.loss)))
];
})(percent(c.disconnects, c.loss) >= 15 ? util.red : util.identity))
])))
];
};

View File

@ -1,10 +1,10 @@
var m = require('mithril');
var util = require('./util');
function ratingAt(title, opt) {
function ratingAt(title, opt, color) {
return util.fMap(opt, function(r) {
return [
m('h2', [title + ': ', m('strong', r.int)]),
m('h2', [title + ': ', m('strong', color(r.int))]),
util.gameLink(r.gameId, ['reached ', util.date(r.at)])
];
}, [
@ -15,7 +15,7 @@ function ratingAt(title, opt) {
module.exports = function(d) {
return [
m('div.half', ratingAt('Highest rating', d.stat.highest)),
m('div.half', ratingAt('Lowest rating', d.stat.lowest))
m('div.half', ratingAt('Highest rating', d.stat.highest, util.green)),
m('div.half', ratingAt('Lowest rating', d.stat.lowest, util.red))
];
};

View File

@ -18,12 +18,6 @@ function streaks(s, display) {
]);
}
function formatSeconds(s) {
var d = moment.duration(s, 'seconds');
var hours = d.days() * 24 + d.hours();
return hours + ' hours, ' + d.minutes() + ' minutes';
}
var lessThan = 'Less than one hour between games.';
module.exports = {
@ -34,10 +28,10 @@ module.exports = {
title: lessThan
}, 'Games played in a row')),
streaks(s, function(v) {
return [
return v ? [
m('strong', v),
' game' + (v > 1 ? 's' : '')
];
] : 'none';
})
];
});
@ -48,26 +42,8 @@ module.exports = {
m('h2', m('span', {
title: 'less than one hour between games'
}, 'Max time spent playing')),
streaks(s, formatSeconds)
streaks(s, util.formatSeconds)
];
});
}
};
// fMap(d.stat.playStreak.nb, function(s) {
// return m('div', [
// m('h2', 'Games played in a row (less than one hour between games)'),
// streaks(s)
// ]);
// }),
// fMap(d.stat.playStreak.time, function(s) {
// return m('div', [
// m('h2', 'Max time spent playing (less than one hour between games)'),
// streaks(s, function(s) {
// return [
// m('strong', formatSeconds(s.v)),
// fromTo(s)
// ];
// })
// ]);
// })

View File

@ -1,33 +1,37 @@
var m = require('mithril');
var util = require('./util');
function streak(s, title) {
function streak(s, title, color) {
return m('div.streak', [
m('h3', [
title + ': ',
m('strong', s.v),
' game' + (s.v > 1 ? 's' : '')
s.v > 0 ? color([
m('strong', s.v),
' game' + (s.v > 1 ? 's' : '')
]) : 'none'
]),
util.fromTo(s)
]);
}
function streaks(s) {
return [
streak(s.max, 'Longest'),
streak(s.cur, 'Current')
];
function streaks(color) {
return function(s) {
return [
streak(s.max, 'Longest', color),
streak(s.cur, 'Current', color)
];
};
}
module.exports = function(d) {
return [
m('div.half', [
m('h2', 'Winning streaks'),
util.fMap(d.stat.resultStreak.win, streaks, util.noData)
m('h2', 'Winning streak'),
util.fMap(d.stat.resultStreak.win, streaks(util.green), util.noData)
]),
m('div.half', [
m('h2', 'Losing streaks'),
util.fMap(d.stat.resultStreak.loss, streaks, util.noData)
m('h2', 'Losing streak'),
util.fMap(d.stat.resultStreak.loss, streaks(util.red), util.noData)
])
];
};

View File

@ -53,5 +53,20 @@ module.exports = {
m('h3', 'Current streak'), (f || streak)(s.cur)
])
];
},
green: function(v) {
return m('green', v);
},
red: function(v) {
return m('red', v);
},
identity: function(v) {
return v;
},
formatSeconds: function(s, format) {
var d = moment.duration(s, 'seconds');
var hours = d.days() * 24 + d.hours();
if (format === 'short') return hours + 'h, ' + d.minutes() + 'm';
return hours + ' hours, ' + d.minutes() + ' minutes';
}
};