store and show total result in crosstables
parent
1f70ca62ab
commit
7fa13eaa81
|
@ -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 & 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>
|
||||
}
|
||||
|
|
|
@ -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> </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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue