store and show total result in crosstables

pull/87/head
Thibault Duplessis 2014-05-21 22:57:10 +02:00
parent 1f70ca62ab
commit 7fa13eaa81
9 changed files with 165 additions and 78 deletions

View File

@ -103,7 +103,7 @@ moreJs = moreJs) {
</div>
<textarea id="pgnText" readonly="readonly">@Html(pgn)</textarea>
<div class="analysis_panels">
<div class="panel computer_analysis active">
<div class="panel computer_analysis">
@analysis.map { a =>
@if(a.old && ctx.isAuth) {
<form class="request_analysis future_game_analysis" action="@routes.Analyse.requestAnalysis(gameId)" method="post">
@ -146,17 +146,20 @@ moreJs = moreJs) {
data-series="@timeChart.series"
data-max="@timeChart.maxTime"></div>
</div>
@cross.map { c =>
<div class="panel crosstable active">
@views.html.game.crosstable(c)
</div>
}
</div>
<div class="analysis_menu">
<a data-panel="computer_analysis" class="active">@trans.computerAnalysis()</a>
<a data-panel="fen_pgn">FEN &amp; PGN</a>
@if(game.pgnImport.isEmpty) {
<a data-panel="move_times">@trans.moveTimes()</a>
@if(cross.isDefined) {
<a data-panel="crosstable">Crosstable</a>
}
</div>
<div id="playing_crosstable">
@cross.map { c =>
@views.html.game.crosstable(c)
}
</div>
}

View File

@ -3,27 +3,34 @@
<div id="crosstable">
<table>
<tbody>
@crosstable.userIds.map { uid =>
@crosstable.users.map { u =>
<tr>
<th>@userIdLink(uid.some)</th>
@crosstable.results.map { r =>
<td>
@crosstable.fill.map { index =>
<td @if(index == 20) { class="last" }>
<a>&nbsp;</a>
</td>
}
@crosstable.results.zipWithIndex.map {
case (r, index) => {
<td @if(index == crosstable.size - 1) { class="last" }>
<a href="@routes.Round.watcher(r.gameId, "white")">
@r.winnerId match {
case Some(w) if w == uid => { <span class="win">1</span> }
case Some(w) if w == u.id => { <span class="win">1</span> }
case None => { ½ }
case _ => { <span class="loss">0</span> }
}
</a>
</td>
}
<td class="score @crosstable.winnerId match {
case Some(w) if w == uid => { win }
}
<th class="score @crosstable.winnerId match {
case Some(w) if w == u.id => { win }
case Some(_) => { loss }
case _ => {}
}">
@crosstable.showScore(crosstable score uid)
</td>
@crosstable.showScore(u.id)
</th>
<th class="user">@userIdLink(u.id.some, withOnline = false)</th>
</tr>
}
</tbody>

View File

@ -31,9 +31,9 @@ POST /@/:username/note controllers.User.writeNote(username: Stri
GET /@/:username/mini controllers.User.showMini(username: String)
GET /@/:username/:filterName controllers.User.showFilter(username: String, filterName: String, page: Int ?= 1)
GET /@/:username controllers.User.show(username: String)
GET /people controllers.User.list(page: Int ?= 1)
GET /people/online controllers.User.online
GET /people/autocomplete controllers.User.autocomplete
GET /player controllers.User.list(page: Int ?= 1)
GET /player/online controllers.User.online
GET /player/autocomplete controllers.User.autocomplete
GET /leaderboard controllers.User.leaderboard
# Account

View File

@ -1,8 +1,8 @@
package lila.game
case class Crosstable(
user1: String,
user2: String,
user1: Crosstable.User,
user2: Crosstable.User,
results: List[Crosstable.Result],
nbGames: Int) {
@ -10,34 +10,46 @@ case class Crosstable(
def nonEmpty = results.nonEmpty option this
def userIds = List(user2, user1)
def score(u: String) = if (u == user1) score1 else score2
private lazy val score1 = computeScore(user1)
private lazy val score2 = computeScore(user2)
// multiplied by ten
private def computeScore(userId: String): Int = results.foldLeft(0) {
case (s, Result(_, Some(w))) if w == userId => s + 10
case (s, Result(_, None)) => s + 5
case (s, _) => s
}
def users = List(user2, user1)
def winnerId =
if (score1 > score2) Some(user1)
else if (score1 < score2) Some(user2)
if (user1.score > user2.score) Some(user1.id)
else if (user1.score < user2.score) Some(user2.id)
else None
def showScore(byTen: Int) = s"${byTen / 10}${(byTen % 10 != 0).??("½")}"
def user(id: String) = users find (_.id == id)
def showScore(userId: String) = {
val byTen = user(userId) ?? (_.score)
s"${byTen / 10}${(byTen % 10 != 0).??("½")}"
}
def addWins(userId: Option[String], wins: Int) = copy(
user1 = user1.copy(
score = user1.score + (userId match {
case None => wins * 5
case Some(u) if user1.id == u => wins * 10
case _ => 0
})),
user2 = user2.copy(
score = user2.score + (userId match {
case None => wins * 5
case Some(u) if user2.id == u => wins * 10
case _ => 0
})))
def fromPov(user: String) =
if (user == user2) copy(user1 = user2, user2 = user1)
else this
lazy val size = results.size
def fill = (1 to 20 - size)
}
object Crosstable {
case class User(id: String, score: Int) // score is x10
case class Result(gameId: String, winnerId: Option[String])
private[game] def makeKey(u1: String, u2: String): String = List(u1, u2).sorted mkString "/"
@ -46,8 +58,9 @@ object Crosstable {
import lila.db.BSON
object BSONFields {
val id = "_id"
val score1 = "s1"
val score2 = "s2"
val results = "r"
val nbGames = "n"
}
@ -57,28 +70,29 @@ object Crosstable {
import BSONFields._
def reads(r: BSON.Reader): Crosstable = r str id split '/' match {
case Array(u1, u2) => Crosstable(
user1 = u1,
user2 = u2,
results = r.get[List[String]](results).map {
_ split '/' match {
case Array(gameId, res) => Result(gameId, Some(if (res == "1") u1 else u2))
case Array(gameId) => Result(gameId, none)
case x => sys error s"Invalid result string $x"
case Array(u1Id, u2Id) => Crosstable(
user1 = User(u1Id, r intD "s1"),
user2 = User(u2Id, r intD "s2"),
results = r.get[List[String]](results).map { r =>
r drop 8 match {
case "" => Result(r take 8, none)
case "+" => Result(r take 8, Some(u1Id))
case "-" => Result(r take 8, Some(u2Id))
case _ => sys error s"Invalid result string $r"
}
},
nbGames = r int nbGames)
case x => sys error s"Invalid crosstable id $x"
}
def writeResult(result: Result, u1: String): String = {
val res = result.winnerId ?? { w => s"/${if (w == u1) 1 else 0}" }
s"${result.gameId}$res"
}
def writeResult(result: Result, u1: String): String =
result.gameId + (result.winnerId ?? { w => if (w == u1) "+" else "-" })
def writes(w: BSON.Writer, o: Crosstable) = BSONDocument(
id -> makeKey(o.user1, o.user2),
results -> o.results.map { writeResult(_, o.user1) },
id -> makeKey(o.user1.id, o.user2.id),
score1 -> o.user1.score,
score2 -> o.user2.score,
results -> o.results.map { writeResult(_, o.user1.id) },
nbGames -> w.int(o.nbGames))
}
}

View File

@ -1,12 +1,14 @@
package lila.game
import play.api.libs.json.JsObject
import play.modules.reactivemongo.json.BSONFormats.toJSON
import reactivemongo.bson.{ BSONDocument, BSONInteger }
import reactivemongo.core.commands._
import lila.common.PimpedJson._
import lila.db.Types._
import lila.user.UserRepo
import reactivemongo.core.commands.Count
import reactivemongo.bson.{ BSONDocument, BSONInteger }
final class CrosstableApi(coll: Coll) {
import Crosstable.Result
@ -28,15 +30,24 @@ final class CrosstableApi(coll: Coll) {
val result = Result(game.id, game.winnerUserId)
val bsonResult = Crosstable.crosstableBSONHandler.writeResult(result, u1)
val bson = BSONDocument(
"$inc" -> BSONDocument(Crosstable.BSONFields.nbGames -> BSONInteger(1))
) ++ {
if (game.rated) BSONDocument("$push" -> BSONDocument(
Crosstable.BSONFields.results -> BSONDocument(
"$each" -> List(bsonResult),
"$slice" -> -maxGames
)))
else BSONDocument()
}
"$inc" -> BSONDocument(
Crosstable.BSONFields.nbGames -> BSONInteger(1),
"s1" -> BSONInteger(game.winnerUserId match {
case Some(u) if u == u1 => 10
case None => 5
case _ => 0
}),
"s2" -> BSONInteger(game.winnerUserId match {
case Some(u) if u == u2 => 10
case None => 5
case _ => 0
})
)
) ++ BSONDocument("$push" -> BSONDocument(
Crosstable.BSONFields.results -> BSONDocument(
"$each" -> List(bsonResult),
"$slice" -> -maxGames
)))
coll.update(select(u1, u2), bson).void
case _ => funit
}
@ -46,26 +57,51 @@ final class CrosstableApi(coll: Coll) {
private def create(x1: String, x2: String): Fu[Option[Crosstable]] =
UserRepo.orderByGameCount(x1, x2) map (_ -> List(x1, x2).sorted) flatMap {
case (Some((u1, u2)), List(su1, su2)) => {
case (Some((u1, u2)), List(su1, su2)) =>
val selector = BSONDocument(
Game.BSONFields.playerUids -> BSONDocument("$all" -> List(u1, u2)),
Game.BSONFields.status -> BSONDocument("$gte" -> chess.Status.Mate.id))
tube.gameTube.coll.find(
selector,
BSONDocument(Game.BSONFields.winnerId -> true)
).sort(BSONDocument(Game.BSONFields.createdAt -> -1))
.cursor[BSONDocument].collect[List](maxGames).map {
_.map { doc =>
doc.getAs[String](Game.BSONFields.id).map { id =>
Result(id, doc.getAs[String](Game.BSONFields.winnerId))
for {
localResults <- tube.gameTube.coll.find(
selector,
BSONDocument(Game.BSONFields.winnerId -> true)
).sort(BSONDocument(Game.BSONFields.createdAt -> -1))
.cursor[BSONDocument].collect[List](maxGames).map {
_.map { doc =>
doc.getAs[String](Game.BSONFields.id).map { id =>
Result(id, doc.getAs[String](Game.BSONFields.winnerId))
}
}.flatten.reverse
}
nbGames <- tube.gameTube.coll.db command Count(tube.gameTube.coll.name, selector.some)
ctDraft = Crosstable(Crosstable.User(su1, 0), Crosstable.User(su2, 0), localResults, nbGames)
crosstable <- {
val command = Aggregate(tube.gameTube.coll.name, Seq(
Match(selector),
GroupField(Game.BSONFields.winnerId)("nb" -> SumValue(1))
))
tube.gameTube.coll.db.command(command) map { stream =>
stream.toList.foldLeft(ctDraft) {
case (ct, obj) => toJSON(obj).asOpt[JsObject] flatMap { o =>
o int "nb" map { nb =>
ct.addWins(o str "_id", nb)
}
} getOrElse ct
}
}.flatten.reverse
} zip (tube.gameTube.coll.db command Count(tube.gameTube.coll.name, selector.some)) map {
case (results, nbGames) => Crosstable(su1, su2, results, nbGames)
}
}
} flatMap { crosstable =>
coll insert crosstable inject crosstable.some
}
_ <- coll insert crosstable
} yield crosstable.some
case _ => fuccess(none)
}

View File

@ -58,6 +58,9 @@ div.analysis_panels > div {
div.analysis_panels > div.active {
display: block;
}
div.analysis_panels #crosstable {
margin-top: 60px;
}
div.game_analysis {
padding: 10px;
width: 492px;

View File

@ -1181,6 +1181,7 @@ div.join_warning {
}
/* soft inactive gradient */
#crosstable th,
#friend_box .title,
#chat div.top,
div.undertable_top,
@ -2054,6 +2055,7 @@ div.lichess_overboard.joining .mini_board {
}
#crosstable td a {
line-height: 2em;
font-size: 1.1em;
display: block;
width: 100%;
background: #fff;
@ -2066,8 +2068,20 @@ div.lichess_overboard.joining .mini_board {
#crosstable .loss {
color: #ac524f;
}
#crosstable td.last {
border-right: 1px solid #ccc;
}
#crosstable th {
border: 1px solid #ccc;
padding: 3px;
}
#crosstable .score {
font-size: 1.3em;
text-align: right;
padding-right: 7px;
}
#crosstable .user {
padding-left: 7px;
}
#now_playing {
margin: 2em 0 1em -30px;

View File

@ -108,11 +108,15 @@ body.dark div.content_box.prefs form li,
body.dark div.content_box.prefs fieldset,
body.dark #claim_draw_zone,
body.dark #themepicker div.color_demo,
body.dark #crosstable th,
body.dark div.force_resign_zone,
body.dark div.proposed_takeback,
body.dark div.offered_draw {
border-color: #3d3d3d;
}
body.dark #crosstable td.last {
border-right-color: #3d3d3d;
}
body.dark #timeline,
body.dark #timeline > .entry {
border-color: #2b2b2b;
@ -312,6 +316,7 @@ body.dark div.content_box_top {
}
/* soft inactive gradient */
body.dark #crosstable th,
body.dark #chat div.top,
body.dark #friend_box .title,
body.dark div.undertable_top,

5
todo
View File

@ -1 +1,6 @@
the todo list has moved to https://etherpad.mozilla.org/ep/pad/view/ro.3bIwxJwTQYW/latest
deploy
------
db.crosstable.drop()
restart nginx (/people -> /player)