let us join started tournaments

This commit is contained in:
Thibault Duplessis 2014-04-13 10:46:28 +02:00
parent f59273f9d0
commit d835734d93
15 changed files with 84 additions and 36 deletions

View file

@ -29,7 +29,7 @@ object Lobby extends LilaController {
def renderHome[A](status: Results.Status)(implicit ctx: Context): Fu[SimpleResult] =
Env.current.preloader(
posts = Env.forum.recent(ctx.me, Env.team.cached.teamIds.apply),
tours = TournamentRepo.enterable,
tours = Env.tournament.allCreatedSorted(true),
filter = Env.setup.filter
).map(_.fold(Redirect(_), {
case (preload, entries, posts, tours, featured, leaderboard, progress, puzzle) =>

View file

@ -40,7 +40,7 @@ object Tournament extends LilaController {
}
private def fetchTournaments =
repo.enterable zip repo.started zip repo.finished(20)
env allCreatedSorted true zip repo.started zip repo.finished(20)
def show(id: String) = Open { implicit ctx =>
repo byId id flatMap {
@ -78,7 +78,7 @@ object Tournament extends LilaController {
def join(id: String) = AuthBody { implicit ctx =>
implicit me =>
NoEngine {
TourOptionFuRedirect(repo createdById id) { tour =>
TourOptionFuRedirect(repo enterableById id) { tour =>
tour.hasPassword.fold(
fuccess(routes.Tournament.joinPassword(id)),
env.api.join(tour, me, none).fold(

View file

@ -21,7 +21,7 @@
}
<br />
@tour.map { t =>
@tournament.linkTo(t)
@tournamentLink(t)
<br /><br />
}
@if(game.finishedOrAborted) {

View file

@ -15,7 +15,7 @@
@scheduled.map { tour =>
@tour.schedule.map { s =>
<li>
@linkTo(tour)<br />
@tournamentLink(tour)<br />
@momentFormat(s.at, "calendar")
</li>
}

View file

@ -1,12 +1,12 @@
@(createds: List[lila.tournament.Created], starteds: List[lila.tournament.Started], finisheds: List[lila.tournament.Finished])(implicit ctx: Context)
@joinButton(tour: lila.tournament.Created) = {
@joinButton(tour: lila.tournament.Enterable) = {
@ctx.me.map { me =>
@if(tour contains me) {
@if(tour isActive me) {
<span class="joined label" data-icon="E">&nbsp;JOINED</span>
} else {
<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>
<button data-icon="@tour.hasPassword.fold("a", "G")" type="submit" class="submit button @if(!tour.isOpen) { shy }">&nbsp;@trans.join()</button>
</form>
}
}
@ -108,7 +108,7 @@
<tbody>
@finisheds.map { tour =>
<tr>
<td>@linkTo(tour)</td>
<td>@tournamentLink(tour)</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>

View file

@ -25,11 +25,17 @@
@{ tour.rated.fold(trans.rated(), trans.casual()) }
<br /><br />
@trans.duration(): @tour.minutes minutes
@if(tour.isRunning && (tour isActive ctx.me)) {
@if(tour.isRunning) {
<br /><br />
@if(tour isActive ctx.me) {
<form action="@routes.Tournament.withdraw(tour.id)" method="POST">
<button type="submit" class="submit button strong" data-icon="b"> @trans.withdraw()</button>
</form>
} else {
<form class="inline" action="@routes.Tournament.join(tour.id)" method="POST">
<button data-icon="@tour.hasPassword.fold("a", "G")" type="submit" class="submit button @if(!tour.isOpen) { shy }">&nbsp;@trans.join()</button>
</form>
}
}
@tour.winner.filter(_ => tour.isFinished).map { winner =>
<br /><br />

View file

@ -25,7 +25,7 @@ final class Env(
private val settings = new {
val CollectionTournament = config getString "collection.tournament"
val MessageTtl = config duration "message.ttl"
val EnterableCacheTtl = config duration "enterable.cache.ttl"
val CreatedCacheTtl = config duration "created.cache.ttl"
val UidTimeout = config duration "uid.timeout"
val SocketTimeout = config duration "socket.timeout"
val SocketName = config getString "socket.name"
@ -76,8 +76,8 @@ final class Env(
def version(tourId: String): Fu[Int] =
socketHub ? Ask(tourId, GetVersion) mapTo manifest[Int]
private val enterable =
lila.memo.AsyncCache.single(TournamentRepo.enterable, timeToLive = EnterableCacheTtl)
val allCreatedSorted =
lila.memo.AsyncCache.single(TournamentRepo.allCreatedSorted, timeToLive = CreatedCacheTtl)
def cli = new lila.common.Cli {
import tube.tournamentTube
@ -92,7 +92,7 @@ final class Env(
import scala.concurrent.duration._
scheduler.message(2 seconds) {
organizer -> actorApi.EnterableTournaments
organizer -> actorApi.AllCreatedTournaments
}
scheduler.message(3 seconds) {

View file

@ -19,7 +19,7 @@ private[tournament] final class Organizer(
def receive = {
case EnterableTournaments => TournamentRepo.unsortedEnterable foreach {
case AllCreatedTournaments => TournamentRepo.allCreated foreach {
_ foreach { tour =>
tour schedule match {
case None =>

View file

@ -16,8 +16,10 @@ private[tournament] case class Player(
def is(userId: String): Boolean = id == userId
def is(user: User): Boolean = is(user.id)
def is(other: Player): Boolean = is(other.id)
def doWithdraw = copy(withdraw = true)
def unWithdraw = copy(withdraw = false)
}
private[tournament] object Player {

View file

@ -51,6 +51,7 @@ sealed trait Tournament {
def userIds = players map (_.id)
def activeUserIds = players filter (_.active) map (_.id)
def nbActiveUsers = players count (_.active)
def withdrawnPlayers = players filterNot (_.active)
def nbPlayers = players.size
def minPlayers = data.minPlayers
def playerRatio = if (scheduled) nbPlayers.toString else s"$nbPlayers/$minPlayers"
@ -71,6 +72,20 @@ sealed trait Tournament {
def isCreator(userId: String) = data.createdBy == userId
}
sealed trait Enterable extends Tournament {
def withPlayers(s: Players): Enterable
def join(user: User, pass: Option[String]): Valid[Enterable]
def joinNew(user: User, pass: Option[String]): Valid[Enterable] = contains(user).fold(
!!("User %s is already part of the tournament" format user.id),
(pass != password).fold(
!!("Invalid tournament password"),
withPlayers(players :+ Player.make(user)).success
))
}
sealed trait StartedOrFinished extends Tournament {
def startedAt: DateTime
@ -106,7 +121,7 @@ sealed trait StartedOrFinished extends Tournament {
case class Created(
id: String,
data: Data,
players: Players) extends Tournament {
players: Players) extends Tournament with Enterable {
import data._
@ -137,26 +152,20 @@ case class Created(
createdBy = data.createdBy,
players = players)
def join(user: User, pass: Option[String]): Valid[Created] = contains(user).fold(
!!("User %s is already part of the tournament" format user.id),
(pass != password).fold(
!!("Invalid tournament password"),
withPlayers(players :+ Player.make(user)).success
)
)
def withdraw(userId: String): Valid[Created] = contains(userId).fold(
withPlayers(players filterNot (_ is userId)).success,
!!("User %s is not part of the tournament" format userId)
)
private def withPlayers(s: Players) = copy(players = s)
def withPlayers(s: Players) = copy(players = s)
def startIfReady = enoughPlayersToStart option start
def start = Started(id, data, DateTime.now, players, Nil)
def asScheduled = schedule map { Scheduled(this, _) }
def join(user: User, pass: Option[String]) = joinNew(user, pass)
}
case class Scheduled(tour: Created, schedule: Schedule) {
@ -166,14 +175,14 @@ case class Scheduled(tour: Created, schedule: Schedule) {
def overlaps(other: Scheduled) = interval overlaps other.interval
}
case class Enterable(tours: List[Created], scheduled: List[Created])
case class EnterableTournaments(tours: List[Created], scheduled: List[Created])
case class Started(
id: String,
data: Data,
startedAt: DateTime,
players: Players,
pairings: List[Pairing]) extends StartedOrFinished {
pairings: List[Pairing]) extends StartedOrFinished with Enterable {
override def isRunning = true
@ -218,17 +227,29 @@ case class Started(
!!("User %s is not part of the tournament" format userId)
)
def withPlayers(s: Players) = copy(players = s)
def quickLossStreak(user: String): Boolean = {
userPairings(user) takeWhile { pair => (pair lostBy user) && pair.quickLoss }
}.size >= 3
private def userPairings(user: String) = pairings filter (_ contains user)
private def withPlayers(s: Players) = copy(players = s)
private def refreshPlayers = withPlayers(Player refresh this)
def encode = refreshPlayers.encode(Status.Started)
def join(user: User, pass: Option[String]) = joinNew(user, pass) orElse joinBack(user, pass)
private def joinBack(user: User, pass: Option[String]) = withdrawnPlayers.find(_ is user) match {
case None => !!("User %s is already part of the tournament" format user.id)
case Some(player) => (pass != password).fold(
!!("Invalid tournament password"),
withPlayers(players map {
case p if p is player => p.unWithdraw
case p => p
}).success)
}
}
case class Finished(
@ -263,6 +284,9 @@ object Tournament {
private[tournament] val tube: JsTube[Tournament] =
JsTube(Reads(reader(_.decode)), writer)
private[tournament] val enterableTube: JsTube[Enterable] =
JsTube(Reads(reader(_.enterable)), writer)
private[tournament] val createdTube: JsTube[Created] =
JsTube(Reads(reader(_.created)), writer)
@ -354,6 +378,8 @@ private[tournament] case class RawTournament(
players = players,
pairings = decodePairings)
def enterable: Option[Enterable] = created orElse started
private def data = Data(
name,
clock,

View file

@ -97,7 +97,7 @@ private[tournament] final class TournamentApi(
finished.players.filter(_.score > 0).map(p => UserRepo.incToints(p.id)(p.score)).sequenceFu inject finished
}, fuccess(started))
def join(tour: Created, me: User, password: Option[String]): Funit =
def join(tour: Enterable, me: User, password: Option[String]): Funit =
(tour.join(me, password)).future flatMap { tour2 =>
TournamentRepo withdraw me.id flatMap { withdrawIds =>
$update(tour2) >>-
@ -144,7 +144,7 @@ private[tournament] final class TournamentApi(
.filter(_ => List(chess.Status.Timeout, chess.Status.Outoftime) contains game.status)
private def lobbyReload {
TournamentRepo.enterable foreach { tours =>
TournamentRepo.allCreatedSorted foreach { tours =>
renderer ? TournamentTable(tours) map {
case view: play.api.templates.Html => ReloadTournaments(view.body)
} pipeToSelection lobby

View file

@ -19,6 +19,10 @@ object TournamentRepo {
private def asFinished(tour: Tournament): Option[Finished] = tour.some collect {
case t: Finished => t
}
private def asEnterable(tour: Tournament): Option[Enterable] = tour.some collect {
case t: Created => t
case t: Started => t
}
def byId(id: String): Fu[Option[Tournament]] = $find byId id
@ -27,6 +31,8 @@ object TournamentRepo {
def createdById(id: String): Fu[Option[Created]] = byIdAs(id, asCreated)
def enterableById(id: String): Fu[Option[Enterable]] = byIdAs(id, asEnterable)
def startedById(id: String): Fu[Option[Started]] = byIdAs(id, asStarted)
def createdByIdAndCreator(id: String, userId: String): Fu[Option[Created]] =
@ -51,7 +57,7 @@ object TournamentRepo {
limit
) map { _.map(asFinished).flatten }
private val enterableQuery = $query(Json.obj(
private val allCreatedQuery = $query(Json.obj(
"status" -> Status.Created.id,
"password" -> $exists(false)
) ++ $or(Seq(
@ -59,11 +65,11 @@ object TournamentRepo {
Json.obj("schedule.at" -> $lt($date(DateTime.now plusMinutes 30)))
)))
def enterable: Fu[List[Created]] = $find(
enterableQuery sort BSONDocument("schedule.at" -> 1, "createdAt" -> 1)
def allCreatedSorted: Fu[List[Created]] = $find(
allCreatedQuery sort BSONDocument("schedule.at" -> 1, "createdAt" -> 1)
) map { _.map(asCreated).flatten }
def unsortedEnterable: Fu[List[Created]] = $find(enterableQuery) map { _.map(asCreated).flatten }
def allCreated: Fu[List[Created]] = $find(allCreatedQuery) map { _.map(asCreated).flatten }
def scheduled: Fu[List[Created]] = $find(
$query(Json.obj(

View file

@ -31,7 +31,7 @@ case class Joining(userId: String)
case class Connected(enumerator: JsEnumerator, member: Member)
// organizer
private[tournament] case object EnterableTournaments
private[tournament] case object AllCreatedTournaments
private[tournament] case object StartedTournaments
case class RemindTournaments(tours: List[Started])
case class RemindTournament(tour: Started)

View file

@ -13,6 +13,7 @@ package object tournament extends PackageObject with WithPlay with WithSocket{
implicit lazy val anyTube = tournamentTube
implicit lazy val createdTube = Tournament.createdTube inColl Env.current.tournamentColl
implicit lazy val startedTube = Tournament.startedTube inColl Env.current.tournamentColl
implicit lazy val enterableTube = Tournament.enterableTube inColl Env.current.tournamentColl
implicit lazy val finishedTube = Tournament.finishedTube inColl Env.current.tournamentColl
}
}

View file

@ -1106,9 +1106,16 @@ a.button,
.ui-widget-content {
border-radius: 2px;
}
.button.shy,
a.button.shy {
border-color: transparent;
background: transparent;
font-weight: normal;
}
.button:hover,
a.button:hover,
.ui-state-hover {
border-color: #e2e2e2;
background: rgb(253, 253, 253);
background: -moz-linear-gradient(top, rgba(253, 253, 253, 1) 0%, rgba(248, 248, 248, 1) 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, rgba(253, 253, 253, 1)), color-stop(100%, rgba(248, 248, 248, 1)));