complete scheduled tournaments

This commit is contained in:
Thibault Duplessis 2014-04-10 20:42:13 +02:00
parent e6d760ec7a
commit 110febb391
20 changed files with 193 additions and 120 deletions

View file

@ -21,18 +21,17 @@ private[app] final class Renderer extends Actor {
case lila.tournament.actorApi.RemindTournament(tournament) =>
sender ! V.tournament.reminder(tournament)
case lila.hub.actorApi.setup.RemindChallenge(gameId, from, _) => {
case lila.hub.actorApi.setup.RemindChallenge(gameId, from, _) =>
val replyTo = sender
(GameRepo game gameId) zip (UserRepo named from) onSuccess {
case (Some(game), Some(user)) => replyTo ! V.setup.challengeNotification(game, user)
}
}
case lila.hub.actorApi.RemindDeployPre => sender ! V.notification.deploy("pre")
case lila.hub.actorApi.RemindDeployPost => sender ! V.notification.deploy("post")
case lila.tournament.actorApi.TournamentTable(tours) =>
sender ! V.tournament.createdTable(tours)
sender ! V.tournament.enterable(tours)
case lila.puzzle.RenderDaily(puzzle, fen, lastMove) =>
sender ! V.puzzle.daily(puzzle, fen, lastMove)

View file

@ -40,7 +40,7 @@ object Tournament extends LilaController {
}
private def tournaments =
repo.created zip repo.started zip repo.finished(20)
repo.enterable zip repo.started zip repo.finished(20)
def show(id: String) = Open { implicit ctx =>
repo byId id flatMap {
@ -82,7 +82,7 @@ object Tournament extends LilaController {
tour.hasPassword.fold(
fuccess(routes.Tournament.joinPassword(id)),
env.api.join(tour, me, none).fold(
err => routes.Tournament.home(),
_ => routes.Tournament.show(tour.id),
_ => routes.Tournament.show(tour.id)
)
)

View file

@ -42,6 +42,10 @@ trait DateHelper { self: I18nHelper =>
s"""<time class="moment" datetime="${isoFormatter print date}" data-format="$format"></time>"""
}
def momentFromNow(date: DateTime) = Html {
s"""<time class="moment-from-now from-now" datetime="${isoFormatter print date}"></time>"""
}
def timeago(date: DateTime)(implicit ctx: Context): Html = Html {
s"""<time class="timeago" datetime="${isoFormatter print date}"></time>"""
}

View file

@ -89,8 +89,9 @@ trait UserHelper { self: I18nHelper with StringHelper =>
val klass = userClass(userId, cssClass, withOnline)
val href = userHref(username, params = params)
val content = truncate.fold(username)(username.take)
val dataIcon = withOnline ?? """data-icon="r""""
s"""<a $dataIcon $klass $href>&nbsp;$content</a>"""
val space = if (withOnline) "&nbsp;" else ""
val dataIcon = if (withOnline) """data-icon="r"""" else ""
s"""<a $dataIcon $klass $href>$space$content</a>"""
}
def userLink(
@ -105,8 +106,9 @@ trait UserHelper { self: I18nHelper with StringHelper =>
val href = userHref(user.username)
val content = text | withRating.fold(user.usernameWithRating, user.username)
val progress = withProgress ?? (" " + showProgress(user.progress))
val dataIcon = withOnline ?? """data-icon="r""""
s"""<a $dataIcon $klass $href>&nbsp;$content$progress</a>"""
val space = if (withOnline) "&nbsp;" else ""
val dataIcon = if (withOnline) """data-icon="r"""" else ""
s"""<a $dataIcon $klass $href>$space$content$progress</a>"""
}
def userInfosLink(
@ -118,8 +120,9 @@ trait UserHelper { self: I18nHelper with StringHelper =>
val klass = userClass(userId, cssClass, withOnline)
val href = userHref(username)
val content = rating.fold(username)(e => s"$username ($e)")
val dataIcon = withOnline ?? """data-icon="r""""
s"""<a $dataIcon $klass $href>&nbsp;$content</a>"""
val space = if (withOnline) "&nbsp;" else ""
val dataIcon = if (withOnline) """data-icon="r"""" else ""
s"""<a $dataIcon $klass $href>$space$content</a>"""
}
} await

View file

@ -1,24 +1,12 @@
@(forumRecent: List[lila.forum.PostLiteView], tours: List[lila.tournament.Created], leaderboard: List[User], progress: List[User])(implicit ctx: Context)
@if(tours.exists(_.scheduled)) {
<div class="open_tournaments undertable">
<div class="undertable_inner">
<table class="scheduled tournaments">
@tournament.createdTable(tours.filter(_.scheduled))
</table>
</div>
</div>
}
<div class="open_tournaments undertable">
<div class="undertable_top">
<a class="more" title="See all tournaments" href="@routes.Tournament.home()">@trans.more() »</a>
<span class="title" data-icon="g"> @trans.openTournaments()</span>
<span class="title"> @trans.openTournaments()</span>
</div>
<div class="undertable_inner">
<table class="tournaments">
@tournament.createdTable(tours.filterNot(_.scheduled))
</table>
<div id="enterable_tournaments" class="undertable_inner">
@tournament.enterable(tours)
</div>
</div>
<div class="clearfix">

View file

@ -32,7 +32,7 @@
<td data-icon="r">&nbsp;@tour.playerRatio</td>
<td>
<form action="@routes.Tournament.join(tour.id)" method="POST">
<input type="submit" class="submit button trans_me" value="Join" />
<button type="submit" class="submit button trans_me" data-icon="G"></button>
</form>
</td>
</tr>

View file

@ -0,0 +1,10 @@
@(tours: List[lila.tournament.Created])
@if(tours.exists(_.scheduled)) {
<table class="scheduled tournaments">
@tournament.createdTable(tours.filter(_.scheduled))
</table>
}
<table class="tournaments">
@tournament.createdTable(tours.filterNot(_.scheduled))
</table>

View file

@ -1,10 +1,12 @@
@(players: String = "all", rated: Option[Boolean] = None)
@(nbPlayers: Option[String] = Some("all"), rated: Option[Boolean] = None)
@text = {
@nbPlayers.map { players =>
## When will the tournament start?
As soon as @players players join it.
You will be notified when the tournament starts, so it is safe to
As soon as @players players join it.
}
You will be notified when the tournament starts, so it is safe to
<a target="_blank" href="@routes.Lobby.home()">play in another tab</a> while waiting.
## Is it rated?
@ -23,14 +25,14 @@ The players with the more points at the end of the tournament duration wins.
A game win is worth 2 points, a draw 1 point, and a defeat 0 points.
## How does the pairing work?
At the beginning of the tournament, players are paired randomly.
As soon as you finish a game, return to the tournament lobby:
At the beginning of the tournament, players are paired randomly.
As soon as you finish a game, return to the tournament lobby:
you will then be paired with the first available player. This ensures minimum wait time,
but you may not play every other players of the tournament. It is normal.
Play fast and return to the lobby to play more games and win more points.
## What is the win streak?
It represents the number of games won in a row. It is added to the points you win.
It represents the number of games won in a row. It is added to the points you win.
For instance, if you win 3 games in a row, you get 2 + 3 points for the last game.
If you lose a game, your win streak returns to zero.

View file

@ -1,47 +1,66 @@
@(createds: List[lila.tournament.Created], starteds: List[lila.tournament.Started], finisheds: List[lila.tournament.Finished])(implicit ctx: Context)
@joinButton(tour: lila.tournament.Created) = {
@ctx.me.map { me =>
@if(tour contains me) {
<form class="inline" action="@routes.Tournament.join(tour.id)" method="POST">
<button data-icon="@tour.hasPassword.fold("a", "G")" type="submit" class="submit button">&nbsp;@trans.join()</button>
</form>
} else {
<span class="joined label" data-icon="E">&nbsp;JOINED</span>
}
}
}
@tourMode(tour: lila.tournament.Tournament) = {
@tour.schedule.map { s =>
@momentFromNow(s.at)
}.getOrElse {
@if(tour.variant.exotic) { 960 }
@if(tour.rated) { @trans.rated() }
}
}
<div class="content_box tournament_box no_padding">
<h1>@trans.tournaments()</h1>
<table class="slist all_tournaments">
<table class="slist">
@if(createds.exists(_.scheduled)) {
<thead>
<tr>
<th class="large">Official tournaments</th>
<th>Starts</th>
<th>@trans.timeControl()</th>
<th colspan="2">@trans.players()</th>
</tr>
</thead>
<tbody class="scheduled">
@createds.filter(_.scheduled).map { tour =>
<tr>
<td>@linkTo(tour)</td>
<td class="small">@tourMode(tour)</td>
<td data-icon="p">&nbsp;@tour.clock.show | @{tour.minutes}m</td>
<td data-icon="r">&nbsp;@tour.nbPlayers</td>
<td>@joinButton(tour)</td>
</tr>
}
</tbody>
}
<thead>
<tr>
<th class="large">@trans.openTournaments()</th>
<th>@trans.mode()</th>
<th></th>
<th>@trans.timeControl()</th>
<th colspan="2">@trans.players()</th>
</tr>
</thead>
<tbody>
@createds.map { tour =>
@createds.filterNot(_.scheduled).map { tour =>
<tr>
<td>@linkTo(tour)</td>
<td class="small">
@if(tour.variant.exotic) {
960
}
@if(tour.rated) {
@trans.rated()
}
</td>
<td class="small">@tourMode(tour)</td>
<td data-icon="p">&nbsp;@tour.clock.show | @{tour.minutes}m</td>
<td data-icon="r">&nbsp;@tour.playerRatio</td>
<td>
@ctx.me.map { me =>
@if(tour contains me) {
<span class="label">JOINED</span>
} else {
<form class="inline" action="@routes.Tournament.join(tour.id)" method="POST">
<button type="submit" class="submit button">
@if(tour.hasPassword) {
<span data-icon="a"> @trans.join()</span>
} else {
@trans.join()
}
</button>
</form>
}
}
</td>
<td>@joinButton(tour)</td>
</tr>
}
@if(ctx.isAuth) {
@ -59,7 +78,7 @@
<thead>
<tr>
<th class="large">@trans.playingRightNow()</th>
<th>@trans.mode()</th>
<th></th>
<th>@trans.timeControl()</th>
<th>@trans.players()</th>
<th>Leader</th>
@ -69,14 +88,7 @@
@starteds.map { tour =>
<tr>
<td>@linkTo(tour)</td>
<td class="small">
@if(tour.variant.exotic) {
960
}
@if(tour.rated) {
@trans.rated()
}
</td>
<td class="small">@tourMode(tour)</td>
<td data-icon="p">&nbsp;@tour.clock.show | @tour.durationString</td>
<td data-icon="r">&nbsp;@tour.playerRatio</td>
<td>@tour.winner.map { player =>
@ -90,7 +102,7 @@
<thead>
<tr>
<th class="large">@trans.finished()</th>
<th>@trans.mode()</th>
<th></th>
<th>@trans.timeControl()</th>
<th>@trans.players()</th>
<th>@trans.winner()</th>
@ -100,14 +112,7 @@
@finisheds.map { tour =>
<tr>
<td>@linkTo(tour)</td>
<td class="small">
@if(tour.variant.exotic) {
960
}
@if(tour.rated) {
@trans.rated()
}
</td>
<td class="small">@tourMode(tour)</td>
<td data-icon="p">&nbsp;@tour.clock.show | @tour.durationString</td>
<td data-icon="r">&nbsp;@tour.playerRatio</td>
<td>@tour.winner.map { player =>

View file

@ -4,7 +4,11 @@
<div class="box">
@trans.createdBy() @userIdLink(tour.createdBy.some, withOnline = false)
<br />
@tour.schedule.map { s =>
@momentFormat(s.at, "calendar")
}.getOrElse {
@timeago(tour.createdAt)
}
@tour.password.map { password =>
<br />
<span data-icon="a">

View file

@ -11,11 +11,15 @@
<thead>
<tr>
<th class="large">
@tour.schedule.map { s =>
Starting @momentFromNow(s.at)
}.getOrElse {
@if(tour.enoughPlayersToStart) {
@trans.tournamentIsStarting()
} else {
@trans.waitingForNbPlayers(tour.missingPlayers)
}
}
</th>
@ctx.me.map { me =>
<th>
@ -44,13 +48,13 @@
</tr>
</thead>
<tbody>
@tour.players.map { player =>
<tr>
<td colspan="2">@userInfosLink(player.username, player.rating.some)</td>
</tr>
}
@tour.players.map { player =>
<tr>
<td colspan="2">@userInfosLink(player.username, player.rating.some)</td>
</tr>
}
</tbody>
</table>
</div>
<br />
<div class="content_box_content">@tournament.faq(tour.minPlayers.toString, tour.rated.some)</div>
<div class="content_box_content">@tournament.faq(!tour.scheduled option tour.minPlayers.toString, tour.rated.some)</div>

View file

@ -13,6 +13,7 @@ chat = chat.map(c => base.chat(c, trans.chatRoom.str())),
underchat = underchat.some) {
<div
id="tournament"
@if(tour.scheduled) { class="scheduled" }
data-href="@routes.Tournament.reload(tour.id)"
data-socket-url="@routes.Tournament.websocket(tour.id)">
@tournament.show.inner(side)(body)

View file

@ -7,7 +7,9 @@ case class Schedule(
speed: Schedule.Speed,
at: DateTime) {
def name = s"${freq.toString} ${speed.toString}"
def name = s"Lichess ${freq.toString} ${speed.toString}"
def like(other: Schedule) = speed == other.speed && at == other.at
}
object Schedule {

View file

@ -34,12 +34,13 @@ private[tournament] final class Scheduler(api: TournamentApi) extends Actor {
Schedule(Daily, Blitz, at(today, 16)),
Schedule(Daily, Slow, at(today, 18)),
Schedule(Hourly, Bullet, at(nextHourDate, nextHour)),
Schedule(Hourly, Bullet, at(nextHourDate, rightNow.getHourOfDay, 43)),
Schedule(Hourly, Blitz, at(nextHourDate, nextHour, 30))
).foldLeft(List[Schedule]()) {
case (scheds, sched) if sched.at.isBeforeNow => scheds
case (scheds, sched) if dbScheds.exists(_.at == sched.at) => scheds
case (scheds, sched) if scheds.exists(_.at == sched.at) => scheds
case (scheds, sched) => sched :: scheds
case (scheds, sched) if sched.at.isBeforeNow => scheds
case (scheds, sched) if dbScheds exists sched.like => scheds
case (scheds, sched) if scheds exists sched.like => scheds
case (scheds, sched) => sched :: scheds
}
scheds foreach api.createScheduled

View file

@ -70,7 +70,7 @@ private[tournament] final class TournamentApi(
created.enoughPlayersToEarlyStart option doStart(created.start)
private[tournament] def startScheduled(created: Created) =
if (created.nbPlayers >= 4) doStart(created.start) else doWipe(created)
if (created.nbPlayers >= 2) doStart(created.start) else doWipe(created)
private def doStart(started: Started): Funit =
$update(started) >>-
@ -142,7 +142,7 @@ private[tournament] final class TournamentApi(
.filter(_ => List(chess.Status.Timeout, chess.Status.Outoftime) contains game.status)
private def lobbyReload {
TournamentRepo.created foreach { tours =>
TournamentRepo.enterable foreach { tours =>
renderer ? TournamentTable(tours) map {
case view: play.api.templates.Html => ReloadTournaments(view.body)
} pipeToSelection lobby

View file

@ -57,7 +57,7 @@ object TournamentRepo {
"password" -> $exists(false)
) ++ $or(Seq(
Json.obj("schedule" -> $exists(false)),
Json.obj("schedule.at" -> $lt($date(DateTime.now plusMinutes 15)))
Json.obj("schedule.at" -> $lt($date(DateTime.now plusMinutes 30)))
))) sort BSONDocument(
"schedule.at" -> 1,
"createdAt" -> 1

View file

@ -591,6 +591,14 @@ var storage = {
setMoment();
$('body').on('lichess.content_loaded', setMoment);
function setMomentFromNow() {
$("time.moment-from-now").each(function() {
this.textContent = moment.parseZone(this.getAttribute('datetime')).fromNow();
});
}
setMomentFromNow();
setInterval(setMomentFromNow, 1000);
// Start game
var $game = $('div.lichess_game').orNot();
if ($game) $game.game(_ld_);
@ -859,12 +867,7 @@ var storage = {
self.options.endUrl = self.element.data('end-url');
self.options.socketUrl = self.element.data('socket-url');
$("div.game_tournament div.clock").each(function() {
$(this).clock({
time: $(this).data("time"),
showTenths: self.options.clockTenths
}).clock("start");
});
startTournamentClock();
if (self.options.tournament_id) {
$('body').data('tournament-id', self.options.tournament_id);
@ -1451,6 +1454,7 @@ var storage = {
self.initTable();
if (!(self.options.player.spectator && self.options.tv)) {
$('div.lichess_goodies').replaceWith(data.infobox);
startTournamentClock();
}
if (self.$chat) self.$chat.chat('resize');
if ($.isFunction(callback)) callback();
@ -1708,6 +1712,15 @@ var storage = {
}
});
var startTournamentClock = function() {
$("div.game_tournament div.clock").each(function() {
$(this).clock({
time: $(this).data("time"),
showTenths: false
}).clock("start");
});
};
$.widget("lichess.clock", {
_create: function() {
var o = this.options;
@ -2259,7 +2272,8 @@ var storage = {
}));
function reloadTournaments(data) {
$("table.tournaments tbody").html(data);
$("#enterable_tournaments").html(data);
$('body').trigger('lichess.content_loaded');
}
function reloadForum() {

View file

@ -53,7 +53,6 @@ time {
time::first-letter {
text-transform: uppercase;
}
@font-face {
font-family: 'Open Sans';
font-style: normal;
@ -79,7 +78,8 @@ time::first-letter {
font-weight: normal;
font-style: normal;
}
.is:before, [data-icon]:before {
.is:before,
[data-icon]:before {
font-size: 1.2em;
vertical-align: middle;
font-family: "lichess" !important;
@ -398,7 +398,7 @@ html {
min-height: 100%;
}
body {
font: 12px 'Open Sans', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, Sans-Serif;
font: 12px'Open Sans', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, Sans-Serif;
color: #747474;
background: #f7f7f7;
background-image: -moz-linear-gradient(top, #d7d7d7 0%, #f7f7f7 116px);
@ -478,7 +478,6 @@ body.tight #top {
#lichess {
margin-left: 211px;
}
body.tight #lichess {
margin-left: 10px;
}
@ -617,7 +616,7 @@ div.content_box_inter {
display: block;
}
div.content_box_inter .intertab {
display:inline-block;
display: inline-block;
text-align: center;
line-height: 1.4em;
margin: 5px 0 -1px 0;
@ -976,7 +975,7 @@ a#sound_state.unavailable {
}
#translation_call .button {
margin: 5px 0 0 0;
display:inline-block;
display: inline-block;
}
#translation_call span.name {
font-weight: bold;
@ -1074,7 +1073,7 @@ table.slist thead {
background: -o-linear-gradient(top, rgba(250, 250, 250, 1) 0%, rgba(245, 245, 245, 1) 100%);
background: -ms-linear-gradient(top, rgba(250, 250, 250, 1) 0%, rgba(245, 245, 245, 1) 100%);
background: linear-gradient(to bottom, rgba(250, 250, 250, 1) 0%, rgba(245, 245, 245, 1) 100%);
text-decoration: none;
text-decoration: none !important;
text-shadow: 0px 1px 0px #FFF;
color: #848484;
}
@ -1474,7 +1473,7 @@ div.game_config div.color_submits button.random span {
line-height: 20px;
}
#hooks_wrap > div.tabs > a {
display:inline-block;
display: inline-block;
text-decoration: none;
padding: 0px 10px;
border: 1px solid #ccc;
@ -1514,7 +1513,6 @@ div.game_config div.color_submits button.random span {
#hooks_wrap > a.filter.on span.number {
display: inline;
}
#hook_filter {
position: absolute;
z-index: 99;
@ -1582,7 +1580,7 @@ div.game_config div.color_submits button.random span {
transition: all 0.1s;
}
#hooks_wrap table th.reload:hover span {
opacity: 0.8;
opacity: 0.8;
}
#hooks_wrap table th.sorting-asc,
#hooks_wrap table th.sorting-desc {
@ -1868,18 +1866,39 @@ div.undertable.leaderboard td:first-child {
max-width: 150px;
overflow: hidden;
}
div.open_tournaments div.undertable_inner {
#enterable_tournaments {
height: auto;
}
div.open_tournaments input.submit {
padding: 0.2em 1em;
}
div.open_tournaments td:first-child {
#enterable_tournaments td:first-child {
padding-left: 1em;
}
div.open_tournaments tr.create td {
#enterable_tournaments tr.create td {
text-align: center;
}
#enterable_tournaments a {
text-decoration: none;
}
#enterable_tournaments button {
padding: 4px 8px;
opacity: 0;
transition: opacity 0.1s;
}
#enterable_tournaments tr:hover button {
opacity: 1;
}
#enterable_tournaments a:hover {
text-decoration: underline;
}
#enterable_tournaments table.scheduled {
border-bottom: 1px solid #ccc;
}
#enterable_tournaments table.scheduled a {
font-weight: bold;
color: #d59120;
}
#enterable_tournaments td:last-child {
width: 20px;
}
div.new_posts li {
margin: 0.6em 0;
padding-left: 9px;
@ -1907,7 +1926,6 @@ div.lichess_overboard.joining a.decline {
div.lichess_overboard.joining .mini_board {
margin: 10px auto;
}
#playing_crosstable {
margin: 40px 0;
}
@ -1941,7 +1959,6 @@ div.lichess_overboard.joining .mini_board {
#crosstable .score {
font-size: 1.3em;
}
#timeline {
margin-top: 2em;
border-top: 1px solid #e4e4e4;
@ -2082,7 +2099,7 @@ input.copyable {
#board_editor .copyables .name {
font-family: monospace;
font-size: 1.2em;
display:inline-block;
display: inline-block;
width: 40px;
}
#board_editor .copyable {

View file

@ -99,6 +99,7 @@ body.dark #hooks_chart > div.grid,
body.dark div.analysis_menu,
body.dark div.vstext,
body.dark div.user_show div.content_box_inter.tabs,
body.dark #enterable_tournaments table.scheduled,
body.dark div.analysis_menu > a {
border-color: #3d3d3d;
}

View file

@ -1,3 +1,16 @@
#tournament table.slist td:first-child a {
font-weight: bold;
text-decoration: none;
}
#tournament table.slist td:first-child a:hover {
text-decoration: underline;
}
#tournament table.slist tbody.scheduled a,
#tournament.scheduled h1,
#tournament table.standing tr:first-child td:first-child {
color: #d59120;
}
#tournament .title_tag {
float: right;
font-size: 20px;
@ -6,6 +19,10 @@
margin: 21px 25px 0 0;
}
#tournament.scheduled time.from-now::first-letter {
text-transform: none;
}
#tournament div.tournament_show {
width: 525px;
}
@ -37,7 +54,8 @@
font-size: 0.8em;
}
#tournament form.inline {
#tournament form.inline,
#tournament span.joined {
display: inline;
float: right;
padding-right: 0.6em;