work on tournament api

pull/83/head
Thibault Duplessis 2013-05-11 18:20:24 -03:00
parent f54a24c70f
commit f94e9a8115
19 changed files with 305 additions and 272 deletions

View File

@ -3,7 +3,7 @@ package actor
import akka.actor._
import play.api.templates._
import play.api.templates.Html
import views.{ html => V }
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) =>
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)
}
}

View File

@ -31,6 +31,7 @@ private[app] final class Router(
case Watcher(gameId, color) sender ! R.Round.watcher(gameId, color).url
case Replay(gameId, color) sender ! R.Analyse.replay(gameId, color).url
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

View File

@ -26,6 +26,10 @@ package captcha {
package lobby {
case class TimelineEntry(rendered: 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 {
@ -44,6 +48,7 @@ package router {
case class Watcher(gameId: String, color: String)
case class Replay(gameId: String, color: String)
case class Pgn(gameId: String)
case class Tourney(tourId: String)
}
package forum {

View File

@ -9,6 +9,7 @@ import actorApi._
import lila.common.PimpedJson._
import lila.socket.Handler
import lila.socket.actorApi.{ Connected _, _ }
import lila.hub.actorApi.lobby._
import lila.user.{ User, Context }
import lila.security.Flood
import makeTimeout.short

View File

@ -26,7 +26,6 @@ object Member {
}
case class Connected(enumerator: JsEnumerator, member: Member)
case class ReloadTournaments(html: String)
case class WithHooks(op: Iterable[String] Unit)
case class AddHook(hook: Hook)
case class RemoveHook(hook: Hook)
@ -36,6 +35,3 @@ case class Join(
uid: String,
user: Option[User],
hookOwnerId: Option[String])
case class Talk(u: String, txt: String)
case class SysTalk(txt: String)
case class UnTalk(r: util.matching.Regex)

View File

@ -7,9 +7,7 @@ import lila.db.api._
import akka.actor.ActorRef
private[round] final class Meddler(
finisher: Finisher,
socketHub: ActorRef) {
final class Meddler(finisher: Finisher, socketHub: ActorRef) {
def forceAbort(id: String) {
$find.byId(id) foreach {

View File

@ -2,7 +2,7 @@ package lila.site
import actorApi._
import lila.socket._
import lila.socket.actorApi.Connected
import lila.socket.actorApi.{ Connected, SendToFlag }
import akka.actor._
import play.api.libs.json._

View File

@ -13,6 +13,4 @@ case class Member(
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])

View File

@ -23,3 +23,5 @@ case class Resync(uid: String)
case class GetSocket(id: String)
case object GetNbSockets
case object SocketTimeout
case class SendToFlag(flag: String, message: JsObject)

View File

@ -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)
}
}
}
}

View File

@ -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 }
}
}
}
}
}

View File

@ -222,20 +222,29 @@ object Tournament {
import lila.db.Tube
import play.api.libs.json._
private[tournament] lazy val tube = Tube(
reader = Reads[Tournament](js
~(for {
obj js.asOpt[JsObject]
rawTour RawTournament.tube.read(obj).asOpt
tour rawTour.decode
} yield JsSuccess(tour): JsResult[Tournament])
),
writer = Writes[Tournament](tour
RawTournament.tube.write(tour.encode) getOrElse JsUndefined("[db] Can't write tournament " + tour.id)
)
private def reader[T <: Tournament](decode: RawTournament Option[T])(js: JsValue): JsResult[T] = ~(for {
obj js.asOpt[JsObject]
rawTour RawTournament.tube.read(obj).asOpt
tour decode(rawTour)
} yield JsSuccess(tour): JsResult[T])
private lazy val writer = Writes[Tournament](tour
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,
clock: TournamentClock,
minutes: Int,
@ -322,7 +331,7 @@ private[tournament] object RawTournament {
private[tournament] lazy val tube = Tube(
(__.json update (
merge(defaults) andThen readDateOpt('startedAt)
merge(defaults) andThen readDateOpt('startedAt)
)) andThen Json.reads[RawTournament],
Json.writes[RawTournament] andThen (__.json update (
writeDateOpt('startedAt)

View File

@ -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)
}
}

View File

@ -48,3 +48,5 @@ case object StartPairings
case class StartPairing(tour: Started)
case class GetTournamentUserIds(tournamentId: String)
case class RemindTournaments(tours: List[Started])
case class RemindTournament(tour: Started)
case class TournamentTable(tours: List[Created])

View File

@ -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] 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
}

View File

@ -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()
)
)
}
}

View File

@ -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 }
}
}
}

View File

@ -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
View File

@ -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
publish scalastic 0.90.0-thib
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
----------