work on tournament api
parent
f54a24c70f
commit
f94e9a8115
|
@ -3,7 +3,7 @@ package actor
|
||||||
|
|
||||||
import akka.actor._
|
import akka.actor._
|
||||||
|
|
||||||
import play.api.templates._
|
import play.api.templates.Html
|
||||||
import views.{ html => V }
|
import views.{ html => V }
|
||||||
|
|
||||||
private[app] final class Renderer extends Actor {
|
private[app] final class Renderer extends Actor {
|
||||||
|
@ -15,5 +15,13 @@ private[app] final class Renderer extends Actor {
|
||||||
|
|
||||||
case lila.notification.actorApi.RenderNotification(id, from, body) =>
|
case lila.notification.actorApi.RenderNotification(id, from, body) =>
|
||||||
V.notification.view(id, from)(Html(body))
|
V.notification.view(id, from)(Html(body))
|
||||||
|
|
||||||
|
case lila.tournament.actorApi.RemindTournament(tournament) =>
|
||||||
|
// TODO
|
||||||
|
// V.notification.view(id, from)(Html(body))
|
||||||
|
|
||||||
|
case lila.tournament.actorApi.TournamentTable(tour) =>
|
||||||
|
// TODO
|
||||||
|
// V.tournament.createdTable(tours)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ private[app] final class Router(
|
||||||
case Watcher(gameId, color) ⇒ sender ! R.Round.watcher(gameId, color).url
|
case Watcher(gameId, color) ⇒ sender ! R.Round.watcher(gameId, color).url
|
||||||
case Replay(gameId, color) ⇒ sender ! R.Analyse.replay(gameId, color).url
|
case Replay(gameId, color) ⇒ sender ! R.Analyse.replay(gameId, color).url
|
||||||
case Pgn(gameId) ⇒ sender ! R.Analyse.pgn(gameId)
|
case Pgn(gameId) ⇒ sender ! R.Analyse.pgn(gameId)
|
||||||
|
case Tourney(tourId) ⇒ // TODO sender ! R.tournament.show(tourId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy val noLangBaseUrl = protocol + I18nDomain(domain).commonDomain
|
private lazy val noLangBaseUrl = protocol + I18nDomain(domain).commonDomain
|
||||||
|
|
|
@ -26,6 +26,10 @@ package captcha {
|
||||||
package lobby {
|
package lobby {
|
||||||
case class TimelineEntry(rendered: String)
|
case class TimelineEntry(rendered: String)
|
||||||
case class Censor(username: String)
|
case class Censor(username: String)
|
||||||
|
case class Talk(u: String, txt: String)
|
||||||
|
case class SysTalk(txt: String)
|
||||||
|
case class UnTalk(r: scala.util.matching.Regex)
|
||||||
|
case class ReloadTournaments(html: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
package game {
|
package game {
|
||||||
|
@ -44,6 +48,7 @@ package router {
|
||||||
case class Watcher(gameId: String, color: String)
|
case class Watcher(gameId: String, color: String)
|
||||||
case class Replay(gameId: String, color: String)
|
case class Replay(gameId: String, color: String)
|
||||||
case class Pgn(gameId: String)
|
case class Pgn(gameId: String)
|
||||||
|
case class Tourney(tourId: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
package forum {
|
package forum {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import actorApi._
|
||||||
import lila.common.PimpedJson._
|
import lila.common.PimpedJson._
|
||||||
import lila.socket.Handler
|
import lila.socket.Handler
|
||||||
import lila.socket.actorApi.{ Connected ⇒ _, _ }
|
import lila.socket.actorApi.{ Connected ⇒ _, _ }
|
||||||
|
import lila.hub.actorApi.lobby._
|
||||||
import lila.user.{ User, Context }
|
import lila.user.{ User, Context }
|
||||||
import lila.security.Flood
|
import lila.security.Flood
|
||||||
import makeTimeout.short
|
import makeTimeout.short
|
||||||
|
|
|
@ -26,7 +26,6 @@ object Member {
|
||||||
}
|
}
|
||||||
|
|
||||||
case class Connected(enumerator: JsEnumerator, member: Member)
|
case class Connected(enumerator: JsEnumerator, member: Member)
|
||||||
case class ReloadTournaments(html: String)
|
|
||||||
case class WithHooks(op: Iterable[String] ⇒ Unit)
|
case class WithHooks(op: Iterable[String] ⇒ Unit)
|
||||||
case class AddHook(hook: Hook)
|
case class AddHook(hook: Hook)
|
||||||
case class RemoveHook(hook: Hook)
|
case class RemoveHook(hook: Hook)
|
||||||
|
@ -36,6 +35,3 @@ case class Join(
|
||||||
uid: String,
|
uid: String,
|
||||||
user: Option[User],
|
user: Option[User],
|
||||||
hookOwnerId: Option[String])
|
hookOwnerId: Option[String])
|
||||||
case class Talk(u: String, txt: String)
|
|
||||||
case class SysTalk(txt: String)
|
|
||||||
case class UnTalk(r: util.matching.Regex)
|
|
||||||
|
|
|
@ -7,9 +7,7 @@ import lila.db.api._
|
||||||
|
|
||||||
import akka.actor.ActorRef
|
import akka.actor.ActorRef
|
||||||
|
|
||||||
private[round] final class Meddler(
|
final class Meddler(finisher: Finisher, socketHub: ActorRef) {
|
||||||
finisher: Finisher,
|
|
||||||
socketHub: ActorRef) {
|
|
||||||
|
|
||||||
def forceAbort(id: String) {
|
def forceAbort(id: String) {
|
||||||
$find.byId(id) foreach {
|
$find.byId(id) foreach {
|
||||||
|
|
|
@ -2,7 +2,7 @@ package lila.site
|
||||||
|
|
||||||
import actorApi._
|
import actorApi._
|
||||||
import lila.socket._
|
import lila.socket._
|
||||||
import lila.socket.actorApi.Connected
|
import lila.socket.actorApi.{ Connected, SendToFlag }
|
||||||
|
|
||||||
import akka.actor._
|
import akka.actor._
|
||||||
import play.api.libs.json._
|
import play.api.libs.json._
|
||||||
|
|
|
@ -13,6 +13,4 @@ case class Member(
|
||||||
def hasFlag(f: String) = flag zmap (f ==)
|
def hasFlag(f: String) = flag zmap (f ==)
|
||||||
}
|
}
|
||||||
|
|
||||||
case class SendToFlag(flag: String, message: JsObject)
|
|
||||||
|
|
||||||
case class Join(uid: String, userId: Option[String], flag: Option[String])
|
case class Join(uid: String, userId: Option[String], flag: Option[String])
|
||||||
|
|
|
@ -23,3 +23,5 @@ case class Resync(uid: String)
|
||||||
case class GetSocket(id: String)
|
case class GetSocket(id: String)
|
||||||
case object GetNbSockets
|
case object GetNbSockets
|
||||||
case object SocketTimeout
|
case object SocketTimeout
|
||||||
|
|
||||||
|
case class SendToFlag(flag: String, message: JsObject)
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package lila.tournament
|
||||||
|
|
||||||
|
import chess.Color
|
||||||
|
import lila.game.{ Game, Player ⇒ GamePlayer, GameRepo, Pov, PovRef, Source }
|
||||||
|
import lila.user.{ User, UserRepo }
|
||||||
|
import lila.round.Meddler
|
||||||
|
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import akka.actor.{ ActorRef, ActorSystem }
|
||||||
|
|
||||||
|
final class GameJoiner(
|
||||||
|
roundMeddler: Meddler,
|
||||||
|
timelinePush: ActorRef,
|
||||||
|
system: ActorSystem) {
|
||||||
|
|
||||||
|
private val secondsToMove = 20
|
||||||
|
|
||||||
|
def apply(tour: Started)(pairing: Pairing): Fu[Game] = for {
|
||||||
|
user1 ← getUser(pairing.user1)
|
||||||
|
user2 ← getUser(pairing.user2)
|
||||||
|
game = Game.make(
|
||||||
|
game = chess.Game(
|
||||||
|
board = chess.Board init tour.variant,
|
||||||
|
clock = tour.clock.chessClock.some
|
||||||
|
),
|
||||||
|
ai = None,
|
||||||
|
whitePlayer = GamePlayer.white withUser user1,
|
||||||
|
blackPlayer = GamePlayer.black withUser user2,
|
||||||
|
creatorColor = chess.Color.White,
|
||||||
|
mode = tour.mode,
|
||||||
|
variant = tour.variant,
|
||||||
|
source = Source.Tournament,
|
||||||
|
pgnImport = None
|
||||||
|
).withTournamentId(tour.id)
|
||||||
|
.withId(pairing.gameId)
|
||||||
|
.start
|
||||||
|
.startClock(2)
|
||||||
|
_ ← (GameRepo insertDenormalized game) >>-
|
||||||
|
(timelinePush ! game) >>-
|
||||||
|
scheduleIdleCheck(PovRef(game.id, Color.White), secondsToMove)
|
||||||
|
} yield game
|
||||||
|
|
||||||
|
private def getUser(username: String): Fu[User] =
|
||||||
|
UserRepo named username flatMap {
|
||||||
|
_.fold(fufail[User]("No user named " + username))(fuccess)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def scheduleIdleCheck(povRef: PovRef, in: Int) {
|
||||||
|
system.scheduler.scheduleOnce(in seconds)(idleCheck(povRef))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def idleCheck(povRef: PovRef) {
|
||||||
|
GameRepo pov povRef foreach {
|
||||||
|
_.filter(_.game.playable) foreach { pov ⇒
|
||||||
|
pov.game.playerHasMoved(pov.color).fold(
|
||||||
|
(pov.color.white && !pov.game.playerHasMoved(Color.Black)) ?? {
|
||||||
|
scheduleIdleCheck(!pov.ref, pov.game.lastMoveTime.fold(secondsToMove) { lmt ⇒
|
||||||
|
lmt - nowSeconds + secondsToMove
|
||||||
|
})
|
||||||
|
},
|
||||||
|
roundMeddler resign pov)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package lila.tournament
|
||||||
|
|
||||||
|
import actorApi._
|
||||||
|
import lila.hub.actorApi.SendTos
|
||||||
|
import makeTimeout.short
|
||||||
|
|
||||||
|
import akka.actor._
|
||||||
|
import akka.pattern.{ ask, pipe }
|
||||||
|
import play.api.libs.json.Json
|
||||||
|
import play.api.templates.Html
|
||||||
|
|
||||||
|
private[tournament] final class Reminder(
|
||||||
|
sockets: List[ActorRef],
|
||||||
|
renderer: ActorRef) extends Actor {
|
||||||
|
|
||||||
|
def receive = {
|
||||||
|
|
||||||
|
case RemindTournaments(tours) ⇒ tours foreach { tour ⇒
|
||||||
|
renderer ? RemindTournament(tour) foreach {
|
||||||
|
case html: Html ⇒ {
|
||||||
|
val msg = SendTos(tour.activeUserIds.toSet, Json.obj(
|
||||||
|
"t" -> "tournamentReminder",
|
||||||
|
"d" -> Json.obj(
|
||||||
|
"id" -> tour.id,
|
||||||
|
"html" -> html.toString
|
||||||
|
)))
|
||||||
|
sockets foreach { _ ! msg }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -222,20 +222,29 @@ object Tournament {
|
||||||
import lila.db.Tube
|
import lila.db.Tube
|
||||||
import play.api.libs.json._
|
import play.api.libs.json._
|
||||||
|
|
||||||
private[tournament] lazy val tube = Tube(
|
private def reader[T <: Tournament](decode: RawTournament ⇒ Option[T])(js: JsValue): JsResult[T] = ~(for {
|
||||||
reader = Reads[Tournament](js ⇒
|
|
||||||
~(for {
|
|
||||||
obj ← js.asOpt[JsObject]
|
obj ← js.asOpt[JsObject]
|
||||||
rawTour ← RawTournament.tube.read(obj).asOpt
|
rawTour ← RawTournament.tube.read(obj).asOpt
|
||||||
tour ← rawTour.decode
|
tour ← decode(rawTour)
|
||||||
} yield JsSuccess(tour): JsResult[Tournament])
|
} yield JsSuccess(tour): JsResult[T])
|
||||||
),
|
|
||||||
writer = Writes[Tournament](tour ⇒
|
private lazy val writer = Writes[Tournament](tour ⇒
|
||||||
RawTournament.tube.write(tour.encode) getOrElse JsUndefined("[db] Can't write tournament " + tour.id)
|
RawTournament.tube.write(tour.encode) getOrElse JsUndefined("[db] Can't write tournament " + tour.id)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
def apply(
|
private[tournament] lazy val tube: Tube[Tournament] =
|
||||||
|
Tube(Reads(reader(_.decode)), writer)
|
||||||
|
|
||||||
|
private[tournament] lazy val createdTube: Tube[Created] =
|
||||||
|
Tube(Reads(reader(_.created)), writer)
|
||||||
|
|
||||||
|
private[tournament] lazy val startedTube: Tube[Started] =
|
||||||
|
Tube(Reads(reader(_.started)), writer)
|
||||||
|
|
||||||
|
private[tournament] lazy val finishedTube: Tube[Finished] =
|
||||||
|
Tube(Reads(reader(_.finished)), writer)
|
||||||
|
|
||||||
|
def make(
|
||||||
createdBy: User,
|
createdBy: User,
|
||||||
clock: TournamentClock,
|
clock: TournamentClock,
|
||||||
minutes: Int,
|
minutes: Int,
|
||||||
|
|
|
@ -0,0 +1,156 @@
|
||||||
|
package lila.tournament
|
||||||
|
|
||||||
|
import actorApi._
|
||||||
|
import tube.roomTube
|
||||||
|
import tube.tournamentTubes._
|
||||||
|
import lila.db.api._
|
||||||
|
import chess.{ Mode, Variant }
|
||||||
|
import lila.game.Game
|
||||||
|
import lila.user.User
|
||||||
|
import lila.hub.actorApi.lobby.{ SysTalk, UnTalk, ReloadTournaments }
|
||||||
|
import lila.hub.actorApi.router.Tourney
|
||||||
|
import lila.socket.actorApi.SendToFlag
|
||||||
|
import makeTimeout.short
|
||||||
|
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
import org.scala_tools.time.Imports._
|
||||||
|
import scalaz.NonEmptyList
|
||||||
|
import akka.actor.ActorRef
|
||||||
|
import akka.pattern.{ ask, pipe }
|
||||||
|
import play.api.libs.json._
|
||||||
|
|
||||||
|
private[tournament] final class TournamentApi(
|
||||||
|
joiner: GameJoiner,
|
||||||
|
router: ActorRef,
|
||||||
|
renderer: ActorRef,
|
||||||
|
socketHub: ActorRef,
|
||||||
|
site: ActorRef,
|
||||||
|
lobby: ActorRef,
|
||||||
|
roundMeddler: lila.round.Meddler,
|
||||||
|
incToints: String ⇒ Int ⇒ Funit) {
|
||||||
|
|
||||||
|
def makePairings(tour: Started, pairings: NonEmptyList[Pairing]): Funit =
|
||||||
|
(tour addPairings pairings) |> { tour2 ⇒
|
||||||
|
$update(tour2) >> (pairings map joiner(tour2)).sequence
|
||||||
|
} map {
|
||||||
|
_.list foreach { game ⇒
|
||||||
|
game.tournamentId foreach { tid ⇒
|
||||||
|
socketHub ! Forward(tid, StartGame(game))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def createTournament(setup: TournamentSetup, me: User): Fu[Created] =
|
||||||
|
TournamentRepo withdraw me.id flatMap { withdrawIds ⇒
|
||||||
|
val created = Tournament.make(
|
||||||
|
createdBy = me,
|
||||||
|
clock = TournamentClock(setup.clockTime * 60, setup.clockIncrement),
|
||||||
|
minutes = setup.minutes,
|
||||||
|
minPlayers = setup.minPlayers,
|
||||||
|
mode = Mode orDefault ~setup.mode,
|
||||||
|
variant = Variant orDefault setup.variant)
|
||||||
|
$insert(created) >>
|
||||||
|
(withdrawIds map socket.reload).sequence >>
|
||||||
|
reloadSiteSocket >>
|
||||||
|
lobbyReload >>
|
||||||
|
sendLobbyMessage(created) inject created
|
||||||
|
}
|
||||||
|
|
||||||
|
def startIfReady(created: Created): Option[Fu[Unit]] = created.startIfReady map doStart
|
||||||
|
|
||||||
|
def earlyStart(created: Created): Option[Fu[Unit]] =
|
||||||
|
created.readyToEarlyStart option doStart(created.start)
|
||||||
|
|
||||||
|
private def doStart(started: Started): Fu[Unit] =
|
||||||
|
$update(started) >> (socket start started.id) >> reloadSiteSocket >> lobbyReload
|
||||||
|
|
||||||
|
def wipeEmpty(created: Created): Fu[Unit] = (for {
|
||||||
|
_ ← $remove(created)
|
||||||
|
_ ← $remove[Room](created.id)
|
||||||
|
_ ← reloadSiteSocket
|
||||||
|
_ ← lobbyReload
|
||||||
|
_ ← removeLobbyMessage(created)
|
||||||
|
} yield ()) doIf created.isEmpty
|
||||||
|
|
||||||
|
def finish(started: Started): Fu[Tournament] = started.readyToFinish.fold({
|
||||||
|
val pairingsToAbort = started.playingPairings
|
||||||
|
val finished = started.finish
|
||||||
|
for {
|
||||||
|
_ ← $update(finished)
|
||||||
|
_ ← socket reloadPage finished.id
|
||||||
|
_ ← reloadSiteSocket
|
||||||
|
_ ← (pairingsToAbort map (_.gameId) map roundMeddler.forceAbort).sequence
|
||||||
|
_ ← finished.players.filter(_.score > 0).map(p ⇒ incToints(p.id)(p.score)).sequence
|
||||||
|
} yield finished
|
||||||
|
}, fuccess(started))
|
||||||
|
|
||||||
|
def join(tour: Created, me: User): Funit =
|
||||||
|
(tour join me).future flatMap { tour2 ⇒
|
||||||
|
TournamentRepo withdraw me.id flatMap { withdrawIds ⇒
|
||||||
|
$update(tour2) >>-
|
||||||
|
(socketHub ! Forward(tour.id, Joining(me.id))) >>-
|
||||||
|
((tour.id :: withdrawIds) foreach { tourId ⇒
|
||||||
|
socketHub ! Forward(tourId, Reload)
|
||||||
|
}) >>-
|
||||||
|
reloadSiteSocket >>-
|
||||||
|
lobbyReload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def withdraw(tour: Tournament, userId: String): Funit = tour match {
|
||||||
|
case created: Created ⇒ (created withdraw userId).fold(
|
||||||
|
err ⇒ fufail(err.shows),
|
||||||
|
tour2 ⇒ $update(tour2) >> (socket reload tour2.id) >> reloadSiteSocket >> lobbyReload
|
||||||
|
)
|
||||||
|
case started: Started ⇒ (started withdraw userId).fold(
|
||||||
|
err ⇒ fufail(err.shows),
|
||||||
|
tour2 ⇒ $update(tour2) >>
|
||||||
|
~(tour2 userCurrentPov userId map roundMeddler.resign) >>
|
||||||
|
(socket reload tour2.id) >>
|
||||||
|
reloadSiteSocket
|
||||||
|
)
|
||||||
|
case finished: Finished ⇒ fufail("Cannot withdraw from finished tournament " + finished.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
def finishGame(game: Game): Fu[Option[Tournament]] = for {
|
||||||
|
tourOption ← ~(game.tournamentId map TournamentRepo.startedById)
|
||||||
|
result ← ~(tourOption.filter(_ ⇒ game.finished).map(tour ⇒ {
|
||||||
|
val tour2 = tour.updatePairing(game.id, _.finish(game.status, game.winnerUserId, game.turns))
|
||||||
|
$update(tour2) >> tripleQuickLossWithdraw(tour2, game.loserUserId) inject tour2.some
|
||||||
|
}))
|
||||||
|
} yield result
|
||||||
|
|
||||||
|
private def tripleQuickLossWithdraw(tour: Started, loser: Option[String]): Funit =
|
||||||
|
loser.filter(tour.quickLossStreak).zmap(withdraw(tour, _))
|
||||||
|
|
||||||
|
private def userIdWhoLostOnTimeWithoutMoving(game: Game): Option[String] =
|
||||||
|
game.playerWhoDidNotMove
|
||||||
|
.flatMap(_.userId)
|
||||||
|
.filter(_ ⇒ List(chess.Status.Timeout, chess.Status.Outoftime) contains game.status)
|
||||||
|
|
||||||
|
private def lobbyReload {
|
||||||
|
TournamentRepo.created foreach { tours ⇒
|
||||||
|
renderer ? TournamentTable(tours) map {
|
||||||
|
case view: play.api.templates.Html ⇒ ReloadTournaments(view)
|
||||||
|
} pipeTo lobby
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val reloadMessage = Json.obj("t" -> "reload", "d" -> JsNull)
|
||||||
|
private def reloadSiteSocket {
|
||||||
|
site ! SendToFlag("tournament", reloadMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def sendLobbyMessage(tour: Created) {
|
||||||
|
router ? Tourney(tour.id) map {
|
||||||
|
case url: String ⇒ SysTalk(
|
||||||
|
"""<a href="%s">%s tournament created</a>""".format(url, tour.name)
|
||||||
|
)
|
||||||
|
} pipeTo lobby
|
||||||
|
}
|
||||||
|
|
||||||
|
private def removeLobbyMessage(tour: Created) {
|
||||||
|
lobby ! UnTalk("%s tournament created".format(tour.name).r)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -48,3 +48,5 @@ case object StartPairings
|
||||||
case class StartPairing(tour: Started)
|
case class StartPairing(tour: Started)
|
||||||
case class GetTournamentUserIds(tournamentId: String)
|
case class GetTournamentUserIds(tournamentId: String)
|
||||||
case class RemindTournaments(tours: List[Started])
|
case class RemindTournaments(tours: List[Started])
|
||||||
|
case class RemindTournament(tour: Started)
|
||||||
|
case class TournamentTable(tours: List[Created])
|
||||||
|
|
|
@ -9,6 +9,13 @@ package object tournament extends PackageObject with WithPlay with WithSocket{
|
||||||
|
|
||||||
private[tournament] implicit lazy val tournamentTube = Tournament.tube inColl Env.current.tournamentColl
|
private[tournament] implicit lazy val tournamentTube = Tournament.tube inColl Env.current.tournamentColl
|
||||||
|
|
||||||
|
private[tournament] object tournamentTubes {
|
||||||
|
implicit lazy val any = tournamentTube
|
||||||
|
implicit lazy val created = Tournament.createdTube inColl Env.current.tournamentColl
|
||||||
|
implicit lazy val started = Tournament.startedTube inColl Env.current.tournamentColl
|
||||||
|
implicit lazy val finished = Tournament.finishedTube inColl Env.current.tournamentColl
|
||||||
|
}
|
||||||
|
|
||||||
private[tournament] implicit lazy val roomTube = Room.tube inColl Env.current.roomColl
|
private[tournament] implicit lazy val roomTube = Room.tube inColl Env.current.roomColl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
package lila.app
|
|
||||||
package tournament
|
|
||||||
|
|
||||||
import chess.Color
|
|
||||||
import game.{ DbGame, DbPlayer, GameRepo, Pov, PovRef, Source }
|
|
||||||
import user.User
|
|
||||||
import round.Meddler
|
|
||||||
|
|
||||||
import scalaz.effects._
|
|
||||||
import play.api.libs.concurrent._
|
|
||||||
import play.api.Play.current
|
|
||||||
import play.api.libs.concurrent.Execution.Implicits._
|
|
||||||
import scala.concurrent.duration._
|
|
||||||
|
|
||||||
final class GameJoiner(
|
|
||||||
gameRepo: GameRepo,
|
|
||||||
roundMeddler: Meddler,
|
|
||||||
timelinePush: DbGame ⇒ IO[Unit],
|
|
||||||
getUser: String ⇒ IO[Option[User]]) {
|
|
||||||
|
|
||||||
private val secondsToMove = 20
|
|
||||||
|
|
||||||
def apply(tour: Started)(pairing: Pairing): IO[DbGame] = for {
|
|
||||||
user1 ← getUser(pairing.user1) map (_ err "No such user " + pairing)
|
|
||||||
user2 ← getUser(pairing.user2) map (_ err "No such user " + pairing)
|
|
||||||
game = DbGame(
|
|
||||||
game = chess.Game(
|
|
||||||
board = chess.Board init tour.variant,
|
|
||||||
clock = tour.clock.chessClock.some
|
|
||||||
),
|
|
||||||
ai = None,
|
|
||||||
whitePlayer = DbPlayer.white withUser user1,
|
|
||||||
blackPlayer = DbPlayer.black withUser user2,
|
|
||||||
creatorColor = chess.Color.White,
|
|
||||||
mode = tour.mode,
|
|
||||||
variant = tour.variant,
|
|
||||||
source = Source.Tournament,
|
|
||||||
pgnImport = None
|
|
||||||
).withTournamentId(tour.id)
|
|
||||||
.withId(pairing.gameId)
|
|
||||||
.start
|
|
||||||
.startClock(2)
|
|
||||||
_ ← gameRepo insert game
|
|
||||||
_ ← gameRepo denormalize game
|
|
||||||
_ ← timelinePush(game)
|
|
||||||
_ ← scheduleIdleCheck(PovRef(game.id, Color.White), secondsToMove)
|
|
||||||
} yield game
|
|
||||||
|
|
||||||
private def scheduleIdleCheck(povRef: PovRef, in: Int) = io {
|
|
||||||
Akka.system.scheduler.scheduleOnce(in seconds)(idleCheck(povRef))
|
|
||||||
} map (_ ⇒ ())
|
|
||||||
|
|
||||||
private def idleCheck(povRef: PovRef) {
|
|
||||||
(for {
|
|
||||||
povOption ← gameRepo pov povRef
|
|
||||||
_ ← ~(povOption filter (_.game.playable) map idleResult)
|
|
||||||
} yield ()).unsafePerformIO
|
|
||||||
}
|
|
||||||
|
|
||||||
private def idleResult(pov: Pov): IO[Unit] = {
|
|
||||||
val idle = !pov.game.playerHasMoved(pov.color)
|
|
||||||
idle.fold(
|
|
||||||
roundMeddler resign pov,
|
|
||||||
(pov.color.white && !pov.game.playerHasMoved(Color.Black)).fold(
|
|
||||||
scheduleIdleCheck(!pov.ref, pov.game.lastMoveTime.fold(secondsToMove) { lmt ⇒
|
|
||||||
lmt - nowSeconds + secondsToMove
|
|
||||||
}),
|
|
||||||
io()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
package lila.app
|
|
||||||
package tournament
|
|
||||||
|
|
||||||
import akka.actor._
|
|
||||||
import akka.actor.ReceiveTimeout
|
|
||||||
import scala.concurrent.duration._
|
|
||||||
import akka.util.Timeout
|
|
||||||
import akka.pattern.{ ask, pipe }
|
|
||||||
import scala.concurrent.{ Future, Promise }
|
|
||||||
import play.api.libs.concurrent._
|
|
||||||
import play.api.Play.current
|
|
||||||
import play.api.libs.json._
|
|
||||||
|
|
||||||
import socket.SendTos
|
|
||||||
|
|
||||||
private[tournament] final class Reminder(hubNames: List[String]) extends Actor {
|
|
||||||
|
|
||||||
lazy val hubRefs = hubNames map { name ⇒ Akka.system.actorFor("/user/" + name) }
|
|
||||||
|
|
||||||
implicit val timeout = Timeout(1 second)
|
|
||||||
|
|
||||||
def receive = {
|
|
||||||
|
|
||||||
case RemindTournaments(tours) ⇒ tours foreach { tour =>
|
|
||||||
val msg = SendTos(tour.activeUserIds.toSet, JsObject(Seq(
|
|
||||||
"t" -> JsString("tournamentReminder"),
|
|
||||||
"d" -> JsObject(Seq(
|
|
||||||
"id" -> JsString(tour.id),
|
|
||||||
"html" -> JsString(views.html.tournament.reminder(tour).toString)
|
|
||||||
))
|
|
||||||
)))
|
|
||||||
hubRefs foreach { _ ! msg }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
package lila.app
|
|
||||||
package tournament
|
|
||||||
|
|
||||||
import org.joda.time.DateTime
|
|
||||||
import org.scala_tools.time.Imports._
|
|
||||||
import scalaz.effects._
|
|
||||||
import scalaz.{ NonEmptyList, Success, Failure }
|
|
||||||
import play.api.libs.json._
|
|
||||||
|
|
||||||
import chess.{ Mode, Variant }
|
|
||||||
import controllers.routes
|
|
||||||
import game.DbGame
|
|
||||||
import user.User
|
|
||||||
import lobby.{ Socket ⇒ LobbySocket }
|
|
||||||
|
|
||||||
private[tournament] final class TournamentApi(
|
|
||||||
repo: TournamentRepo,
|
|
||||||
roomRepo: RoomRepo,
|
|
||||||
joiner: GameJoiner,
|
|
||||||
socket: Socket,
|
|
||||||
siteSocket: site.Socket,
|
|
||||||
lobbySocket: LobbySocket,
|
|
||||||
roundMeddler: round.Meddler,
|
|
||||||
incToints: String ⇒ Int ⇒ IO[Unit]) {
|
|
||||||
|
|
||||||
def makePairings(tour: Started, pairings: NonEmptyList[Pairing]): IO[Unit] =
|
|
||||||
(tour addPairings pairings) |> { tour2 ⇒
|
|
||||||
for {
|
|
||||||
_ ← repo saveIO tour2
|
|
||||||
games ← (pairings map joiner(tour)).sequence
|
|
||||||
_ ← (games map socket.notifyPairing).sequence
|
|
||||||
} yield ()
|
|
||||||
}
|
|
||||||
|
|
||||||
def createTournament(setup: TournamentSetup, me: User): IO[Created] = for {
|
|
||||||
withdrawIds ← repo withdraw me.id
|
|
||||||
created = Tournament(
|
|
||||||
createdBy = me,
|
|
||||||
clock = TournamentClock(setup.clockTime * 60, setup.clockIncrement),
|
|
||||||
minutes = setup.minutes,
|
|
||||||
minPlayers = setup.minPlayers,
|
|
||||||
mode = Mode orDefault ~setup.mode,
|
|
||||||
variant = Variant orDefault setup.variant)
|
|
||||||
_ ← repo saveIO created
|
|
||||||
_ ← (withdrawIds map socket.reload).sequence
|
|
||||||
_ ← reloadSiteSocket
|
|
||||||
_ ← lobbyReload
|
|
||||||
_ ← sendLobbyMessage(created)
|
|
||||||
} yield created
|
|
||||||
|
|
||||||
def startIfReady(created: Created): Option[IO[Unit]] = created.startIfReady map doStart
|
|
||||||
|
|
||||||
def earlyStart(created: Created): Option[IO[Unit]] =
|
|
||||||
created.readyToEarlyStart option doStart(created.start)
|
|
||||||
|
|
||||||
private def doStart(started: Started): IO[Unit] =
|
|
||||||
(repo saveIO started) >> (socket start started.id) >> reloadSiteSocket >> lobbyReload
|
|
||||||
|
|
||||||
def wipeEmpty(created: Created): IO[Unit] = (for {
|
|
||||||
_ ← repo removeIO created
|
|
||||||
_ ← roomRepo removeIO created.id
|
|
||||||
_ ← reloadSiteSocket
|
|
||||||
_ ← lobbyReload
|
|
||||||
_ ← removeLobbyMessage(created)
|
|
||||||
} yield ()) doIf created.isEmpty
|
|
||||||
|
|
||||||
def finish(started: Started): IO[Tournament] = started.readyToFinish.fold({
|
|
||||||
val pairingsToAbort = started.playingPairings
|
|
||||||
val finished = started.finish
|
|
||||||
for {
|
|
||||||
_ ← repo saveIO finished
|
|
||||||
_ ← socket reloadPage finished.id
|
|
||||||
_ ← reloadSiteSocket
|
|
||||||
_ ← (pairingsToAbort map (_.gameId) map roundMeddler.forceAbort).sequence
|
|
||||||
_ ← finished.players.filter(_.score > 0).map(p ⇒ incToints(p.id)(p.score)).sequence
|
|
||||||
} yield finished
|
|
||||||
}, io(started))
|
|
||||||
|
|
||||||
def join(tour: Created, me: User): Valid[IO[Unit]] = for {
|
|
||||||
tour2 ← tour join me
|
|
||||||
} yield for {
|
|
||||||
withdrawIds ← repo withdraw me.id
|
|
||||||
_ ← repo saveIO tour2
|
|
||||||
_ ← socket.notifyJoining(tour.id, me.id)
|
|
||||||
_ ← ((tour.id :: withdrawIds) map socket.reload).sequence
|
|
||||||
_ ← reloadSiteSocket
|
|
||||||
_ ← lobbyReload
|
|
||||||
} yield ()
|
|
||||||
|
|
||||||
def withdraw(tour: Tournament, userId: String): IO[Unit] = tour match {
|
|
||||||
case created: Created ⇒ (created withdraw userId).fold(
|
|
||||||
err ⇒ putStrLn(err.shows),
|
|
||||||
tour2 ⇒ (repo saveIO tour2) >> (socket reload tour2.id) >> reloadSiteSocket >> lobbyReload
|
|
||||||
)
|
|
||||||
case started: Started ⇒ (started withdraw userId).fold(
|
|
||||||
err ⇒ putStrLn(err.shows),
|
|
||||||
tour2 ⇒ (repo saveIO tour2) >>
|
|
||||||
~(tour2 userCurrentPov userId map roundMeddler.resign) >>
|
|
||||||
(socket reload tour2.id) >>
|
|
||||||
reloadSiteSocket
|
|
||||||
)
|
|
||||||
case finished: Finished ⇒ putStrLn("Cannot withdraw from finished tournament " + finished.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
def finishGame(game: DbGame): IO[Option[Tournament]] = for {
|
|
||||||
tourOption ← ~(game.tournamentId map repo.startedById)
|
|
||||||
result ← ~(tourOption.filter(_ ⇒ game.finished).map(tour ⇒ {
|
|
||||||
val tour2 = tour.updatePairing(game.id, _.finish(game.status, game.winnerUserId, game.turns))
|
|
||||||
repo saveIO tour2 inject tour2.some
|
|
||||||
(repo saveIO tour2) >>
|
|
||||||
tripleQuickLossWithdraw(tour2, game.loserUserId) inject tour2.some
|
|
||||||
}))
|
|
||||||
} yield result
|
|
||||||
|
|
||||||
private def tripleQuickLossWithdraw(tour: Started, loser: Option[String]): IO[Unit] =
|
|
||||||
~loser.filter(tour.quickLossStreak).map(withdraw(tour, _))
|
|
||||||
|
|
||||||
private def userIdWhoLostOnTimeWithoutMoving(game: DbGame): Option[String] =
|
|
||||||
game.playerWhoDidNotMove
|
|
||||||
.flatMap(_.userId)
|
|
||||||
.filter(_ ⇒ List(chess.Status.Timeout, chess.Status.Outoftime) contains game.status)
|
|
||||||
|
|
||||||
private def lobbyReload = repo.created flatMap { tours ⇒
|
|
||||||
lobbySocket reloadTournaments views.html.tournament.createdTable(tours).toString
|
|
||||||
}
|
|
||||||
|
|
||||||
private val reloadMessage = JsObject(Seq("t" -> JsString("reload"), "d" -> JsNull))
|
|
||||||
private def sendToSiteSocket(message: JsObject) = io {
|
|
||||||
siteSocket.sendToFlag("tournament", message)
|
|
||||||
}
|
|
||||||
private val reloadSiteSocket = sendToSiteSocket(reloadMessage)
|
|
||||||
|
|
||||||
private def sendLobbyMessage(tour: Created) = lobbySocket sysTalk {
|
|
||||||
"""<a href="%s">%s tournament created</a>""".format(routes.Tournament.show(tour.id), tour.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
private def removeLobbyMessage(tour: Created) = lobbySocket unTalk {
|
|
||||||
("%s tournament created" format tour.name).r
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
1
todo
1
todo
|
@ -79,6 +79,7 @@ FIX TOUCHPAD SIGN IN http://en.lichess.org/forum/lichess-feedback/cant-sign-in#6
|
||||||
chess variants https://github.com/ornicar/lila/issues/2://github.com/ornicar/lila/issues/25
|
chess variants https://github.com/ornicar/lila/issues/2://github.com/ornicar/lila/issues/25
|
||||||
publish scalastic 0.90.0-thib
|
publish scalastic 0.90.0-thib
|
||||||
stream game export
|
stream game export
|
||||||
|
show fen only after game is finished http://en.lichess.org/forum/lichess-feedback/please-disable-live-fen-notation?page=1
|
||||||
|
|
||||||
DEPLOY p21
|
DEPLOY p21
|
||||||
----------
|
----------
|
||||||
|
|
Loading…
Reference in New Issue