complete round module
parent
a8d9f75035
commit
8c4cb4a95d
|
@ -1,14 +1,14 @@
|
|||
package lila.app
|
||||
package round
|
||||
package templating
|
||||
|
||||
import http.Context
|
||||
import game.Pov
|
||||
import templating.ConfigHelper
|
||||
import lila.user.Context
|
||||
import lila.game.Pov
|
||||
import lila.round.Env.{ current ⇒ roundEnv }
|
||||
|
||||
import play.api.libs.json.Json
|
||||
import scala.math.{ min, max, round }
|
||||
|
||||
trait RoundHelper { self: ConfigHelper ⇒
|
||||
trait RoundHelper {
|
||||
|
||||
def roundPlayerJsData(pov: Pov, version: Int) = {
|
||||
import pov._
|
||||
|
@ -73,6 +73,9 @@ trait RoundHelper { self: ConfigHelper ⇒
|
|||
}
|
||||
|
||||
private def animationDelay(pov: Pov) = round {
|
||||
gameAnimationDelay * max(0, min(1.2, ((pov.game.estimateTotalTime - 60) / 60) * 0.2))
|
||||
roundEnv.animationDelay.toMillis *
|
||||
max(0, min(1.2,
|
||||
((pov.game.estimateTotalTime - 60) / 60) * 0.2
|
||||
))
|
||||
}
|
||||
}
|
|
@ -26,6 +26,6 @@ object $update {
|
|||
docOption zmap (doc ⇒ $update($select(id), op(doc)))
|
||||
}
|
||||
|
||||
def field[ID: Writes, A: InColl, B: Writes](id: ID, field: String, value: B, upsert: Boolean = false): Funit =
|
||||
$update($select(id), $set(field -> value))
|
||||
def field[ID: Writes, A: InColl, B: Writes](id: ID, name: String, value: B, upsert: Boolean = false): Funit =
|
||||
apply($select(id), $set(name -> value))
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ final class PostApi(env: Env, indexer: ActorRef, maxPerPage: Int) extends Option
|
|||
// TODO
|
||||
// _ ← modLog.deletePost(mod, post.userId, post.author, post.ip,
|
||||
// text = "%s / %s / %s".format(view.categ.name, view.topic.name, post.text))
|
||||
} yield none[Post])
|
||||
} yield true.some)
|
||||
} yield ()).value.void
|
||||
|
||||
def cursor(selector: JsObject) = $query[Post](selector).cursor[Option[Post]]
|
||||
|
|
|
@ -3,7 +3,7 @@ package lila.game
|
|||
import chess.format.{ pgn ⇒ chessPgn }
|
||||
import chess.Status
|
||||
|
||||
private[game] object Rewind {
|
||||
object Rewind {
|
||||
|
||||
def apply(
|
||||
game: Game,
|
||||
|
@ -11,7 +11,7 @@ private[game] object Rewind {
|
|||
initialFen: Option[String]): Valid[(Progress, String)] = chessPgn.Reader.withSans(
|
||||
pgn = pgn,
|
||||
op = _.init,
|
||||
tags = ~initialFen.map(fen ⇒ List(
|
||||
tags = initialFen.zmap(fen ⇒ List(
|
||||
chessPgn.Tag(_.FEN, fen),
|
||||
chessPgn.Tag(_.Variant, game.variant.name)
|
||||
))
|
||||
|
|
|
@ -6,10 +6,8 @@ package object game extends PackageObject with WithPlay {
|
|||
|
||||
object tube {
|
||||
|
||||
private[game] implicit lazy val pgnTube =
|
||||
Tube.json inColl Env.current.pgnColl
|
||||
implicit lazy val pgnTube = Tube.json inColl Env.current.pgnColl
|
||||
|
||||
// expose game tube
|
||||
implicit lazy val gameTube = Game.tube inColl Env.current.gameColl
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,11 @@ import scala.concurrent.Future
|
|||
import scala.concurrent.duration._
|
||||
import akka.actor._
|
||||
import akka.pattern.{ ask, pipe }
|
||||
import akka.util.Timeout
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
import actorApi._
|
||||
|
||||
final class Broadcast(
|
||||
routees: List[ActorRef],
|
||||
timeout: FiniteDuration) extends Actor {
|
||||
|
||||
private implicit val akkaTimeout = makeTimeout(timeout)
|
||||
final class Broadcast(routees: List[ActorRef])(implicit timeout: Timeout) extends Actor {
|
||||
|
||||
def receive = {
|
||||
|
||||
|
|
|
@ -10,16 +10,18 @@ final class Env(config: Config, system: ActorSystem) {
|
|||
private val SocketHubTimeout = config duration "socket.hub.timeout"
|
||||
|
||||
object actor {
|
||||
val gameIndexer = actorFor("actor.game.indexer")
|
||||
val lobby = actorFor("actor.lobby")
|
||||
val renderer = actorFor("actor.renderer")
|
||||
val captcher = actorFor("actor.captcher")
|
||||
val forumIndexer = actorFor("actor.forum_indexer")
|
||||
val forumIndexer = actorFor("actor.forum.indexer")
|
||||
val messenger = actorFor("actor.messenger")
|
||||
val router = actorFor("actor.router")
|
||||
val forum = actorFor("actor.forum")
|
||||
val teamIndexer = actorFor("actor.team_indexer")
|
||||
val teamIndexer = actorFor("actor.team.indexer")
|
||||
val ai = actorFor("actor.ai")
|
||||
val monitor = actorFor("actor.monitor")
|
||||
val tournamentOrganizer = actorFor("actor.tournament.organizer")
|
||||
}
|
||||
|
||||
object socket {
|
||||
|
@ -27,7 +29,7 @@ final class Env(config: Config, system: ActorSystem) {
|
|||
val monitor = actorFor("socket.monitor")
|
||||
val hub = system.actorOf(Props(new Broadcast(List(
|
||||
socket.lobby
|
||||
), SocketHubTimeout)), name = SocketHubName)
|
||||
))(makeTimeout(SocketHubTimeout))), name = SocketHubName)
|
||||
}
|
||||
|
||||
private def actorFor(name: String) =
|
||||
|
|
|
@ -39,3 +39,12 @@ package forum {
|
|||
package ai {
|
||||
case class Analyse(pgn: String, initialFen: Option[String])
|
||||
}
|
||||
|
||||
package monitor {
|
||||
case object AddMove
|
||||
case object AddRequest
|
||||
}
|
||||
|
||||
package round {
|
||||
case class FinishGame(gameId: String)
|
||||
}
|
||||
|
|
|
@ -31,10 +31,10 @@ final class Env(
|
|||
)), name = ActorName)
|
||||
|
||||
// requests per second
|
||||
val rpsProvider = new RpsProvider(RpsIntervall)
|
||||
private lazy val rpsProvider = new RpsProvider(RpsIntervall)
|
||||
|
||||
// moves per second
|
||||
val mpsProvider = new RpsProvider(RpsIntervall)
|
||||
private lazy val mpsProvider = new RpsProvider(RpsIntervall)
|
||||
}
|
||||
|
||||
object Env {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package lila.monitor
|
||||
|
||||
import lila.socket.actorApi.GetNbMembers
|
||||
import lila.hub.actorApi.monitor._
|
||||
// import round.GetNbHubs
|
||||
|
||||
import akka.actor._
|
||||
|
@ -46,6 +47,9 @@ private[monitor] final class Reporting(
|
|||
|
||||
def receive = {
|
||||
|
||||
case AddMove ⇒ mpsProvider.add
|
||||
case AddRequest ⇒ rpsProvider.add
|
||||
|
||||
case GetNbMembers ⇒ sender ! allMembers
|
||||
|
||||
case GetNbGames ⇒ sender ! nbGames
|
||||
|
|
|
@ -9,12 +9,12 @@ private[monitor] final class RpsProvider(timeout: FiniteDuration) {
|
|||
|
||||
private val tms = timeout.toMillis
|
||||
|
||||
def countRequest = {
|
||||
def add {
|
||||
val current = nowMillis
|
||||
counter.single.transform {
|
||||
case (precedent, (count, millis)) if current > millis + tms ⇒ (0, (1, current))
|
||||
case (precedent, (count, millis)) if current > millis + (tms / 2) ⇒ (count, (1, current))
|
||||
case (precedent, (count, millis)) ⇒ (precedent, (count + 1, millis))
|
||||
case (precedent, (count, millis)) ⇒ (precedent, (count + 1, millis))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import lila.socket._
|
|||
private[monitor] final class SocketHandler(socket: ActorRef) {
|
||||
|
||||
def join(uid: String): Fu[JsSocketHandler] =
|
||||
Handler(socket, uid, Join(uid)) {
|
||||
Handler(socket, uid, Join(uid))(_ ⇒ {
|
||||
case _ ⇒
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import game.{ GameRepo, DbGame, Pov }
|
||||
import user.UserRepo
|
||||
import i18n.I18nKey.{ Select ⇒ SelectI18nKey }
|
||||
import elo.EloUpdater
|
||||
import chess.{ EloCalculator, Status, Color }
|
||||
import Status._
|
||||
import Color._
|
||||
|
||||
import scalaz.effects._
|
||||
import play.api.Play.current
|
||||
import play.api.libs.concurrent.Akka
|
||||
|
||||
final class Finisher(
|
||||
userRepo: UserRepo,
|
||||
gameRepo: GameRepo,
|
||||
messenger: Messenger,
|
||||
eloUpdater: EloUpdater,
|
||||
eloCalculator: EloCalculator,
|
||||
finisherLock: FinisherLock,
|
||||
indexGame: DbGame ⇒ IO[Unit],
|
||||
tournamentOrganizerActorName: String) {
|
||||
|
||||
type IOEvents = IO[List[Event]]
|
||||
type ValidIOEvents = Valid[IOEvents]
|
||||
|
||||
private lazy val tournamentOrganizerActor =
|
||||
Akka.system.actorFor("/user/" + tournamentOrganizerActorName)
|
||||
|
||||
def abort(pov: Pov): ValidIOEvents =
|
||||
if (pov.game.abortable) finish(pov.game, Aborted)
|
||||
else !!("game is not abortable")
|
||||
|
||||
def forceAbort(game: DbGame): ValidIOEvents =
|
||||
if (game.playable) finish(game, Aborted)
|
||||
else !!("game is not playable, cannot be force aborted")
|
||||
|
||||
def resign(pov: Pov): ValidIOEvents =
|
||||
if (pov.game.resignable) finish(pov.game, Resign, Some(!pov.color))
|
||||
else !!("game is not resignable")
|
||||
|
||||
def resignForce(pov: Pov): ValidIOEvents =
|
||||
if (pov.game.resignable && !pov.game.hasAi)
|
||||
finish(pov.game, Timeout, Some(pov.color))
|
||||
else !!("game is not resignable")
|
||||
|
||||
def drawClaim(pov: Pov): ValidIOEvents = pov match {
|
||||
case Pov(game, color) if game.playable && game.player.color == color && game.toChessHistory.threefoldRepetition ⇒ finish(game, Draw)
|
||||
case Pov(game, color) ⇒ !!("game is not threefold repetition")
|
||||
}
|
||||
|
||||
def drawAccept(pov: Pov): ValidIOEvents =
|
||||
if (pov.opponent.isOfferingDraw)
|
||||
finish(pov.game, Draw, None, Some(_.drawOfferAccepted))
|
||||
else !!("opponent is not proposing a draw")
|
||||
|
||||
def drawForce(game: DbGame): ValidIOEvents = finish(game, Draw, None, None)
|
||||
|
||||
def outoftime(game: DbGame): ValidIOEvents = game.outoftimePlayer.fold(
|
||||
!![IOEvents]("no outoftime applicable " + game.clock.fold("-")(_.remainingTimes.toString))
|
||||
) { player ⇒
|
||||
finish(game, Outoftime, Some(!player.color) filter game.toChess.board.hasEnoughMaterialToMate)
|
||||
}
|
||||
|
||||
def outoftimes(games: List[DbGame]): List[IO[Unit]] =
|
||||
games map { g ⇒
|
||||
outoftime(g).fold(
|
||||
msgs ⇒ putStrLn(g.id + " " + msgs.shows),
|
||||
_ map (_ ⇒ Unit) // events are lost
|
||||
): IO[Unit]
|
||||
}
|
||||
|
||||
def moveFinish(game: DbGame, color: Color): IO[List[Event]] =
|
||||
(game.status match {
|
||||
case Mate ⇒ finish(game, Mate, Some(color))
|
||||
case status @ (Stalemate | Draw) ⇒ finish(game, status)
|
||||
case _ ⇒ success(io(Nil)): ValidIOEvents
|
||||
}) | io(Nil)
|
||||
|
||||
private def finish(
|
||||
game: DbGame,
|
||||
status: Status,
|
||||
winner: Option[Color] = None,
|
||||
message: Option[SelectI18nKey] = None): ValidIOEvents =
|
||||
if (finisherLock isLocked game) !!("game finish is locked")
|
||||
else success(for {
|
||||
_ ← finisherLock lock game
|
||||
p1 = game.finish(status, winner)
|
||||
p2 ← message.fold(io(p1)) { m ⇒
|
||||
messenger.systemMessage(p1.game, m) map p1.++
|
||||
}
|
||||
_ ← gameRepo save p2
|
||||
g = p2.game
|
||||
winnerId = winner flatMap (g.player(_).userId)
|
||||
_ ← gameRepo.finish(g.id, winnerId)
|
||||
_ ← updateElo(g)
|
||||
_ ← incNbGames(g, White) doIf (g.status >= Status.Mate)
|
||||
_ ← incNbGames(g, Black) doIf (g.status >= Status.Mate)
|
||||
_ ← indexGame(g)
|
||||
_ ← io { tournamentOrganizerActor ! FinishGame(g) }
|
||||
} yield p2.events)
|
||||
|
||||
private def incNbGames(game: DbGame, color: Color): IO[Unit] =
|
||||
game.player(color).userId.fold(io()) { id ⇒
|
||||
userRepo.incNbGames(id, game.rated, game.hasAi,
|
||||
result = game.wonBy(color).fold(0)(_.fold(1, -1)).some filterNot (_ ⇒ game.hasAi || game.aborted)
|
||||
)
|
||||
}
|
||||
|
||||
private def updateElo(game: DbGame): IO[Unit] = ~{
|
||||
for {
|
||||
whiteUserId ← game.player(White).userId
|
||||
blackUserId ← game.player(Black).userId
|
||||
if whiteUserId != blackUserId
|
||||
} yield for {
|
||||
whiteUserOption ← userRepo byId whiteUserId
|
||||
blackUserOption ← userRepo byId blackUserId
|
||||
_ ← ~(whiteUserOption |@| blackUserOption).apply(
|
||||
(whiteUser, blackUser) ⇒ {
|
||||
val (whiteElo, blackElo) = eloCalculator.calculate(whiteUser, blackUser, game.winnerColor)
|
||||
val (whiteDiff, blackDiff) = (whiteElo - whiteUser.elo, blackElo - blackUser.elo)
|
||||
val cheaterWin = (whiteDiff > 0 && whiteUser.engine) || (blackDiff > 0 && blackUser.engine)
|
||||
gameRepo.setEloDiffs(game.id, whiteDiff, blackDiff) >>
|
||||
eloUpdater.game(whiteUser, whiteElo, blackUser.elo) >>
|
||||
eloUpdater.game(blackUser, blackElo, whiteUser.elo) doUnless cheaterWin
|
||||
}
|
||||
)
|
||||
} yield ()
|
||||
} doUnless (!game.finished || !game.rated || game.turns < 2)
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import game.DbGame
|
||||
import lila.common.memo.BooleanExpiryMemo
|
||||
|
||||
import scalaz.effects._
|
||||
|
||||
final class FinisherLock(timeout: Int) {
|
||||
|
||||
private val internal = new BooleanExpiryMemo(timeout)
|
||||
|
||||
def isLocked(game: DbGame): Boolean = internal get game.id
|
||||
|
||||
def lock(game: DbGame): IO[Unit] = internal put game.id
|
||||
}
|
|
@ -1,263 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import ai.Ai
|
||||
import game.{ GameRepo, PgnRepo, Pov, PovRef, Handler }
|
||||
import i18n.I18nKey.{ Select ⇒ SelectI18nKey }
|
||||
import chess.Role
|
||||
import chess.Pos.posAt
|
||||
import chess.format.Forsyth
|
||||
|
||||
import scalaz.effects._
|
||||
import akka.actor._
|
||||
import scala.concurrent.{ Future, Await }
|
||||
import akka.pattern.ask
|
||||
import scala.concurrent.duration._
|
||||
import akka.util.Timeout
|
||||
|
||||
final class Hand(
|
||||
gameRepo: GameRepo,
|
||||
pgnRepo: PgnRepo,
|
||||
messenger: Messenger,
|
||||
takeback: Takeback,
|
||||
ai: () ⇒ Ai,
|
||||
finisher: Finisher,
|
||||
hubMaster: ActorRef,
|
||||
moretimeSeconds: Int) extends Handler(gameRepo) {
|
||||
|
||||
type IOValidEvents = IO[Valid[List[Event]]]
|
||||
type PlayResult = Future[Valid[(List[Event], String, Option[String])]]
|
||||
|
||||
def play(
|
||||
povRef: PovRef,
|
||||
origString: String,
|
||||
destString: String,
|
||||
promString: Option[String] = None,
|
||||
blur: Boolean = false,
|
||||
lag: Int = 0): PlayResult = fromPovFuture(povRef) {
|
||||
case Pov(g1, color) ⇒ (for {
|
||||
g2 ← g1.validIf(g1 playableBy color, "Game not playable %s %s, on move %d".format(origString, destString, g1.toChess.fullMoveNumber))
|
||||
orig ← posAt(origString) toValid "Wrong orig " + origString
|
||||
dest ← posAt(destString) toValid "Wrong dest " + destString
|
||||
promotion = Role promotable promString
|
||||
chessGame = g2.toChess withPgnMoves (pgnRepo unsafeGet g2.id)
|
||||
newChessGameAndMove ← chessGame(orig, dest, promotion, lag)
|
||||
(newChessGame, move) = newChessGameAndMove
|
||||
} yield g2.update(newChessGame, move, blur)).prefixFailuresWith(povRef + " - ").fold(
|
||||
e ⇒ Future(failure(e)), {
|
||||
case (progress, pgn) ⇒ if (progress.game.finished) (for {
|
||||
_ ← gameRepo save progress
|
||||
_ ← pgnRepo.save(povRef.gameId, pgn)
|
||||
finishEvents ← finisher.moveFinish(progress.game, color)
|
||||
events = progress.events ::: finishEvents
|
||||
} yield playResult(events, progress)).toFuture
|
||||
else if (progress.game.player.isAi && progress.game.playable) for {
|
||||
initialFen ← progress.game.variant.standard.fold(
|
||||
io(none[String]),
|
||||
gameRepo initialFen progress.game.id).toFuture
|
||||
aiResult ← ai().play(progress.game, pgn, initialFen)
|
||||
eventsAndFen ← aiResult.fold(
|
||||
err ⇒ Future(failure(err)), {
|
||||
case (newChessGame, move) ⇒ {
|
||||
val (prog2, pgn2) = progress.game.update(newChessGame, move)
|
||||
val progress2 = progress flatMap { _ ⇒ prog2 }
|
||||
(for {
|
||||
_ ← gameRepo save progress2
|
||||
_ ← pgnRepo.save(povRef.gameId, pgn2)
|
||||
finishEvents ← finisher.moveFinish(progress2.game, !color)
|
||||
events = progress2.events ::: finishEvents
|
||||
} yield playResult(events, progress2)).toFuture
|
||||
}
|
||||
}): PlayResult
|
||||
} yield eventsAndFen
|
||||
else (for {
|
||||
_ ← gameRepo save progress
|
||||
_ ← pgnRepo.save(povRef.gameId, pgn)
|
||||
events = progress.events
|
||||
} yield playResult(events, progress)).toFuture
|
||||
})
|
||||
}
|
||||
|
||||
private def playResult(events: List[Event], progress: Progress) = success((
|
||||
events,
|
||||
Forsyth exportBoard progress.game.toChess.board,
|
||||
progress.game.lastMove
|
||||
))
|
||||
|
||||
def abort(fullId: String): IOValidEvents = attempt(fullId, finisher.abort)
|
||||
|
||||
def resign(fullId: String): IOValidEvents = attempt(fullId, finisher.resign)
|
||||
|
||||
def resignForce(fullId: String): IO[Valid[List[Event]]] =
|
||||
gameRepo pov fullId flatMap { povOption ⇒
|
||||
(povOption toValid "No such game" flatMap { pov ⇒
|
||||
implicit val timeout = Timeout(1 second)
|
||||
Await.result(
|
||||
hubMaster ? round.IsGone(pov.game.id, !pov.color) map {
|
||||
case true ⇒ finisher resignForce pov
|
||||
case _ ⇒ !!("Opponent is not gone")
|
||||
},
|
||||
1 second
|
||||
)
|
||||
}).fold(err ⇒ io(failure(err)), _ map success)
|
||||
}
|
||||
|
||||
def outoftime(ref: PovRef): IOValidEvents = attemptRef(ref, finisher outoftime _.game)
|
||||
|
||||
def drawClaim(fullId: String): IOValidEvents = attempt(fullId, finisher.drawClaim)
|
||||
|
||||
def drawAccept(fullId: String): IOValidEvents = attempt(fullId, finisher.drawAccept)
|
||||
|
||||
def drawOffer(fullId: String): IOValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1 playerCanOfferDraw color) {
|
||||
if (g1.player(!color).isOfferingDraw) finisher drawAccept pov
|
||||
else success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.drawOfferSent) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _ offerDraw g.turns) }
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
}
|
||||
else !!("invalid draw offer " + fullId)
|
||||
})
|
||||
|
||||
def drawCancel(fullId: String): IO[Valid[List[Event]]] = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (pov.player.isOfferingDraw) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.drawOfferCanceled) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.removeDrawOffer) }
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no draw offer to cancel " + fullId)
|
||||
})
|
||||
|
||||
def drawDecline(fullId: String): IO[Valid[List[Event]]] = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1.player(!color).isOfferingDraw) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.drawOfferDeclined) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(!color, _.removeDrawOffer) }
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no draw offer to decline " + fullId)
|
||||
})
|
||||
|
||||
def rematchCancel(fullId: String): IO[Valid[List[Event]]] = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
pov.player.isOfferingRematch.fold(
|
||||
success(for {
|
||||
p1 ← messenger.systemMessage(g1, _.rematchOfferCanceled) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.removeRematchOffer) }
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events),
|
||||
!!("no rematch offer to cancel " + fullId)
|
||||
)
|
||||
})
|
||||
|
||||
def rematchDecline(fullId: String): IO[Valid[List[Event]]] = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
g1.player(!color).isOfferingRematch.fold(
|
||||
success(for {
|
||||
p1 ← messenger.systemMessage(g1, _.rematchOfferDeclined) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(!color, _.removeRematchOffer) }
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events),
|
||||
!!("no rematch offer to decline " + fullId)
|
||||
)
|
||||
})
|
||||
|
||||
def takebackAccept(fullId: String): IOValidEvents = fromPov(fullId) { pov ⇒
|
||||
if (pov.opponent.isProposingTakeback && pov.game.nonTournament) for {
|
||||
fen ← gameRepo initialFen pov.game.id
|
||||
pgn ← pgnRepo get pov.game.id
|
||||
res ← takeback(pov.game, pgn, fen).sequence
|
||||
} yield res
|
||||
else io {
|
||||
!!("opponent is not proposing a takeback")
|
||||
}
|
||||
}
|
||||
|
||||
def takebackOffer(fullId: String): IOValidEvents = fromPov(fullId) {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1.playable && g1.bothPlayersHaveMoved && g1.nonTournament) {
|
||||
for {
|
||||
fen ← gameRepo initialFen pov.game.id
|
||||
pgn ← pgnRepo get pov.game.id
|
||||
result ← if (g1.player(!color).isAi)
|
||||
takeback.double(pov.game, pgn, fen).sequence
|
||||
else if (g1.player(!color).isProposingTakeback)
|
||||
takeback(pov.game, pgn, fen).sequence
|
||||
else for {
|
||||
p1 ← messenger.systemMessage(g1, _.takebackPropositionSent) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.proposeTakeback) }
|
||||
_ ← gameRepo save p2
|
||||
} yield success(p2.events)
|
||||
} yield result
|
||||
}
|
||||
else io {
|
||||
!!("invalid takeback proposition " + fullId)
|
||||
}
|
||||
}
|
||||
|
||||
def takebackCancel(fullId: String): IO[Valid[List[Event]]] = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (pov.player.isProposingTakeback) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.takebackPropositionCanceled) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.removeTakebackProposition) }
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no takeback proposition to cancel " + fullId)
|
||||
})
|
||||
|
||||
def takebackDecline(fullId: String): IO[Valid[List[Event]]] = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1.player(!color).isProposingTakeback) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.takebackPropositionDeclined) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(!color, _.removeTakebackProposition) }
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no takeback proposition to decline " + fullId)
|
||||
})
|
||||
|
||||
def moretime(ref: PovRef): IO[Valid[List[Event]]] = attemptRef(ref, pov ⇒
|
||||
pov.game.clock filter (_ ⇒ pov.game.moretimeable) map { clock ⇒
|
||||
val color = !pov.color
|
||||
val newClock = clock.giveTime(color, moretimeSeconds)
|
||||
val progress = pov.game withClock newClock
|
||||
for {
|
||||
events ← messenger.systemMessage(
|
||||
progress.game, ((_.untranslated(
|
||||
"%s + %d seconds".format(color, moretimeSeconds)
|
||||
)): SelectI18nKey)
|
||||
)
|
||||
progress2 = progress ++ (Event.Clock(newClock) :: events)
|
||||
_ ← gameRepo save progress2
|
||||
} yield progress2.events
|
||||
} toValid "cannot add moretime"
|
||||
)
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import socket.{ Broom, Close, GetNbMembers, GetUsernames, NbMembers, SendTo, SendTos }
|
||||
|
||||
import akka.actor._
|
||||
import scala.concurrent.duration._
|
||||
import akka.util.Timeout
|
||||
import akka.pattern.{ ask, pipe }
|
||||
import scala.concurrent.Future
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.concurrent._
|
||||
import play.api.Play.current
|
||||
|
||||
final class HubMaster(
|
||||
makeHistory: () ⇒ History,
|
||||
uidTimeout: Int,
|
||||
hubTimeout: Int,
|
||||
playerTimeout: Int) extends Actor {
|
||||
|
||||
implicit val timeout = Timeout(1 second)
|
||||
implicit val executor = Akka.system.dispatcher
|
||||
|
||||
var hubs = Map.empty[String, ActorRef]
|
||||
|
||||
def receive = {
|
||||
|
||||
case Broom ⇒ hubs.values foreach (_ ! Broom)
|
||||
|
||||
case msg @ SendTo(_, _) ⇒ hubs.values foreach (_ ! msg)
|
||||
|
||||
case msg @ SendTos(_, _) ⇒ hubs.values foreach (_ ! msg)
|
||||
|
||||
case msg @ GameEvents(gameId, events) ⇒ hubs get gameId foreach (_ forward msg)
|
||||
|
||||
case GetHub(gameId: String) ⇒ sender ! {
|
||||
(hubs get gameId) | {
|
||||
mkHub(gameId) ~ { h ⇒ hubs = hubs + (gameId -> h) }
|
||||
}
|
||||
}
|
||||
|
||||
case msg @ GetGameVersion(gameId) ⇒ (hubs get gameId).fold(sender ! 0) {
|
||||
_ ? msg pipeTo sender
|
||||
}
|
||||
|
||||
case msg @ AnalysisAvailable(gameId) ⇒
|
||||
hubs get gameId foreach (_ forward msg)
|
||||
|
||||
case CloseGame(gameId) ⇒ hubs get gameId foreach { hub ⇒
|
||||
hub ! Close
|
||||
hubs = hubs - gameId
|
||||
}
|
||||
|
||||
case msg @ IsConnectedOnGame(gameId, color) ⇒ (hubs get gameId).fold(sender ! false) {
|
||||
_ ? msg pipeTo sender
|
||||
}
|
||||
|
||||
case msg @ IsGone(gameId, color) ⇒ (hubs get gameId).fold(sender ! false) {
|
||||
_ ? msg pipeTo sender
|
||||
}
|
||||
|
||||
case GetNbHubs ⇒ sender ! hubs.size
|
||||
|
||||
case GetNbMembers ⇒ Future.traverse(hubs.values) { hub ⇒
|
||||
(hub ? GetNbMembers).mapTo[Int]
|
||||
} map (_.sum) pipeTo sender
|
||||
|
||||
case GetUsernames ⇒ Future.traverse(hubs.values) { hub ⇒
|
||||
(hub ? GetUsernames).mapTo[Iterable[String]]
|
||||
} map (_.flatten) pipeTo sender
|
||||
|
||||
case msg @ NbMembers(_) ⇒ hubs.values foreach (_ ! msg)
|
||||
}
|
||||
|
||||
private def mkHub(gameId: String): ActorRef = context.actorOf(Props(new Hub(
|
||||
gameId = gameId,
|
||||
history = makeHistory(),
|
||||
uidTimeout = uidTimeout,
|
||||
hubTimeout = hubTimeout,
|
||||
playerTimeout = playerTimeout
|
||||
)), name = "game_hub_" + gameId)
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import game.{ DbGame, GameRepo, PovRef, Pov }
|
||||
|
||||
import scalaz.effects._
|
||||
|
||||
final class Meddler(
|
||||
gameRepo: GameRepo,
|
||||
finisher: Finisher,
|
||||
socket: Socket) {
|
||||
|
||||
def forceAbort(id: String): IO[Unit] = for {
|
||||
gameOption ← gameRepo game id
|
||||
_ ← gameOption.fold(putStrLn("Cannot abort missing game " + id)) { game ⇒
|
||||
(finisher forceAbort game).fold(
|
||||
err ⇒ putStrLn(err.shows),
|
||||
ioEvents ⇒ for {
|
||||
events ← ioEvents
|
||||
_ ← io { socket.send(game.id, events) }
|
||||
} yield ()
|
||||
)
|
||||
}
|
||||
} yield ()
|
||||
|
||||
def resign(pov: Pov): IO[Unit] = (finisher resign pov).fold(
|
||||
err ⇒ putStrLn(err.shows),
|
||||
ioEvents ⇒ for {
|
||||
events ← ioEvents
|
||||
_ ← io { socket.send(pov.game.id, events) }
|
||||
} yield ()
|
||||
)
|
||||
|
||||
def resign(povRef: PovRef): IO[Unit] = for {
|
||||
povOption ← gameRepo pov povRef
|
||||
_ ← povOption.fold(putStrLn("Cannot resign missing game " + povRef))(resign)
|
||||
} yield ()
|
||||
|
||||
def finishAbandoned(game: DbGame): IO[Unit] = game.abandoned.fold(
|
||||
finisher.resign(Pov(game, game.player))
|
||||
.prefixFailuresWith("Finish abandoned game " + game.id)
|
||||
.fold(
|
||||
err ⇒ putStrLn(err.shows),
|
||||
_ map (_ ⇒ ()) // discard the events
|
||||
),
|
||||
putStrLn("Game is not abandoned")
|
||||
)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import socket.Fen
|
||||
|
||||
import scalaz.effects._
|
||||
import akka.actor._
|
||||
import scala.concurrent.duration._
|
||||
import akka.util.Timeout
|
||||
import play.api.libs.concurrent._
|
||||
import play.api.Play.current
|
||||
|
||||
final class MoveNotifier(
|
||||
hubNames: List[String],
|
||||
countMove: () ⇒ Unit) {
|
||||
|
||||
lazy val hubRefs = hubNames map { name ⇒ Akka.system.actorFor("/user/" + name) }
|
||||
|
||||
def apply(gameId: String, fen: String, lastMove: Option[String]) {
|
||||
countMove()
|
||||
val message = Fen(gameId, fen, lastMove)
|
||||
hubRefs foreach (_ ! message)
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import scalaz.effects._
|
||||
import com.mongodb.casbah.MongoCollection
|
||||
import akka.actor.Props
|
||||
import play.api.libs.concurrent._
|
||||
import play.api.Application
|
||||
|
||||
import game.{ GameRepo, PgnRepo, DbGame, Rewind }
|
||||
import user.{ UserRepo, User }
|
||||
import elo.EloUpdater
|
||||
import ai.Ai
|
||||
import core.Settings
|
||||
import i18n.I18nKeys
|
||||
import security.Flood
|
||||
|
||||
final class RoundEnv(
|
||||
app: Application,
|
||||
settings: Settings,
|
||||
mongodb: String ⇒ MongoCollection,
|
||||
gameRepo: GameRepo,
|
||||
pgnRepo: PgnRepo,
|
||||
rewind: Rewind,
|
||||
userRepo: UserRepo,
|
||||
eloUpdater: EloUpdater,
|
||||
i18nKeys: I18nKeys,
|
||||
ai: () ⇒ Ai,
|
||||
countMove: () ⇒ Unit,
|
||||
flood: Flood,
|
||||
indexGame: DbGame ⇒ IO[Unit]) {
|
||||
|
||||
implicit val ctx = app
|
||||
import settings._
|
||||
|
||||
lazy val history = () ⇒ new History(timeout = RoundMessageLifetime)
|
||||
|
||||
lazy val hubMaster = Akka.system.actorOf(Props(new HubMaster(
|
||||
makeHistory = history,
|
||||
uidTimeout = RoundUidTimeout,
|
||||
hubTimeout = RoundHubTimeout,
|
||||
playerTimeout = RoundPlayerTimeout
|
||||
)), name = ActorRoundHubMaster)
|
||||
|
||||
private lazy val moveNotifier = new MoveNotifier(
|
||||
hubNames = List(ActorSiteHub, ActorLobbyHub, ActorTournamentHubMaster),
|
||||
countMove = countMove)
|
||||
|
||||
lazy val socket = new Socket(
|
||||
getWatcherPov = gameRepo.pov,
|
||||
getPlayerPov = gameRepo.pov,
|
||||
hand = hand,
|
||||
hubMaster = hubMaster,
|
||||
messenger = messenger,
|
||||
moveNotifier = moveNotifier,
|
||||
flood = flood)
|
||||
|
||||
lazy val hand = new Hand(
|
||||
gameRepo = gameRepo,
|
||||
pgnRepo = pgnRepo,
|
||||
messenger = messenger,
|
||||
ai = ai,
|
||||
finisher = finisher,
|
||||
takeback = takeback,
|
||||
hubMaster = hubMaster,
|
||||
moretimeSeconds = RoundMoretime)
|
||||
|
||||
lazy val finisher = new Finisher(
|
||||
userRepo = userRepo,
|
||||
gameRepo = gameRepo,
|
||||
messenger = messenger,
|
||||
eloUpdater = eloUpdater,
|
||||
eloCalculator = eloCalculator,
|
||||
finisherLock = finisherLock,
|
||||
indexGame = indexGame,
|
||||
tournamentOrganizerActorName = ActorTournamentOrganizer)
|
||||
|
||||
lazy val eloCalculator = new chess.EloCalculator(false)
|
||||
|
||||
lazy val finisherLock = new FinisherLock(timeout = FinisherLockTimeout)
|
||||
|
||||
lazy val takeback = new Takeback(
|
||||
gameRepo = gameRepo,
|
||||
pgnRepo = pgnRepo,
|
||||
rewind = rewind,
|
||||
messenger = messenger)
|
||||
|
||||
lazy val messenger = new Messenger(
|
||||
roomRepo = roomRepo,
|
||||
watcherRoomRepo = watcherRoomRepo,
|
||||
i18nKeys = i18nKeys)
|
||||
|
||||
lazy val roomRepo = new RoomRepo(
|
||||
mongodb(RoundCollectionRoom))
|
||||
|
||||
lazy val watcherRoomRepo = new WatcherRoomRepo(
|
||||
mongodb(RoundCollectionWatcherRoom))
|
||||
|
||||
lazy val meddler = new Meddler(
|
||||
gameRepo = gameRepo,
|
||||
finisher = finisher,
|
||||
socket = socket)
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.ask
|
||||
import scala.concurrent.duration._
|
||||
import akka.util.Timeout
|
||||
import scala.concurrent.Await
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.iteratee._
|
||||
import play.api.libs.concurrent._
|
||||
import play.api.Play.current
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
import scalaz.effects._
|
||||
import scalaz.{ Success, Failure }
|
||||
|
||||
import game.{ Pov, PovRef }
|
||||
import user.User
|
||||
import chess.Color
|
||||
import socket.{ PingVersion, Quit, Resync }
|
||||
import socket.Util.connectionFail
|
||||
import security.Flood
|
||||
import implicits.RichJs._
|
||||
import http.Context
|
||||
|
||||
final class Socket(
|
||||
getWatcherPov: (String, String) ⇒ IO[Option[Pov]],
|
||||
getPlayerPov: String ⇒ IO[Option[Pov]],
|
||||
hand: Hand,
|
||||
hubMaster: ActorRef,
|
||||
messenger: Messenger,
|
||||
moveNotifier: MoveNotifier,
|
||||
flood: Flood) {
|
||||
|
||||
private val timeoutDuration = 1 second
|
||||
implicit private val timeout = Timeout(timeoutDuration)
|
||||
|
||||
def blockingVersion(gameId: String): Int = Await.result(
|
||||
hubMaster ? GetGameVersion(gameId) mapTo manifest[Int],
|
||||
timeoutDuration)
|
||||
|
||||
def send(progress: Progress) {
|
||||
send(progress.game.id, progress.events)
|
||||
}
|
||||
|
||||
def send(gameId: String, events: List[Event]) {
|
||||
hubMaster ! GameEvents(gameId, events)
|
||||
}
|
||||
|
||||
private def controller(
|
||||
hub: ActorRef,
|
||||
uid: String,
|
||||
member: Member,
|
||||
povRef: PovRef): JsValue ⇒ Unit =
|
||||
if (member.owner) (e: JsValue) ⇒ e str "t" match {
|
||||
case Some("p") ⇒ e int "v" foreach { v ⇒
|
||||
hub ! PingVersion(uid, v)
|
||||
}
|
||||
case Some("talk") ⇒ for {
|
||||
txt ← e str "d"
|
||||
if member.canChat
|
||||
if flood.allowMessage(uid, txt)
|
||||
} {
|
||||
val events = messenger.playerMessage(povRef, txt).unsafePerformIO
|
||||
hub ! Events(events)
|
||||
}
|
||||
case Some("move") ⇒ parseMove(e) foreach {
|
||||
case (orig, dest, prom, blur, lag) ⇒ {
|
||||
hub ! Ack(uid)
|
||||
hand.play(povRef, orig, dest, prom, blur, lag) onSuccess {
|
||||
case Failure(fs) ⇒ {
|
||||
hub ! Resync(uid)
|
||||
println(fs.shows)
|
||||
}
|
||||
case Success((events, fen, lastMove)) ⇒ {
|
||||
send(povRef.gameId, events)
|
||||
moveNotifier(povRef.gameId, fen, lastMove)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case Some("moretime") ⇒ (for {
|
||||
res ← hand moretime povRef
|
||||
op ← res.fold(_ ⇒ io(), events ⇒ io(hub ! Events(events)))
|
||||
} yield op).unsafePerformIO
|
||||
case Some("outoftime") ⇒ (for {
|
||||
res ← hand outoftime povRef
|
||||
op ← res.fold(_ ⇒ io(), events ⇒ io(hub ! Events(events)))
|
||||
} yield op).unsafePerformIO
|
||||
case _ ⇒
|
||||
}
|
||||
|
||||
else (e: JsValue) ⇒ e str "t" match {
|
||||
case Some("p") ⇒ e int "v" foreach { v ⇒
|
||||
hub ! PingVersion(uid, v)
|
||||
}
|
||||
case Some("talk") ⇒ for {
|
||||
txt ← e str "d"
|
||||
if member.canChat
|
||||
if flood.allowMessage(uid, txt)
|
||||
} {
|
||||
val events = messenger.watcherMessage(
|
||||
povRef.gameId,
|
||||
member.username,
|
||||
txt).unsafePerformIO
|
||||
hub ! Events(events)
|
||||
}
|
||||
case _ ⇒
|
||||
}
|
||||
|
||||
def joinWatcher(
|
||||
gameId: String,
|
||||
colorName: String,
|
||||
version: Option[Int],
|
||||
uid: Option[String],
|
||||
ctx: Context): IO[SocketFuture] =
|
||||
getWatcherPov(gameId, colorName) map { join(_, false, version, uid, none, ctx) }
|
||||
|
||||
def joinPlayer(
|
||||
fullId: String,
|
||||
version: Option[Int],
|
||||
uid: Option[String],
|
||||
token: Option[String],
|
||||
ctx: Context): IO[SocketFuture] = {
|
||||
getPlayerPov(fullId) map { join(_, true, version, uid, token, ctx) }
|
||||
}
|
||||
|
||||
private def parseMove(event: JsValue) = for {
|
||||
d ← event obj "d"
|
||||
orig ← d str "from"
|
||||
dest ← d str "to"
|
||||
prom = d str "promotion"
|
||||
blur = (d int "b") == Some(1)
|
||||
lag = d int "lag"
|
||||
} yield (orig, dest, prom, blur, lag | 0)
|
||||
|
||||
private def join(
|
||||
povOption: Option[Pov],
|
||||
owner: Boolean,
|
||||
versionOption: Option[Int],
|
||||
uidOption: Option[String],
|
||||
tokenOption: Option[String],
|
||||
ctx: Context): SocketFuture =
|
||||
((povOption |@| uidOption |@| versionOption) apply {
|
||||
(pov: Pov, uid: String, version: Int) ⇒
|
||||
(for {
|
||||
hub ← hubMaster ? GetHub(pov.gameId) mapTo manifest[ActorRef]
|
||||
socket ← hub ? Join(
|
||||
uid = uid,
|
||||
user = ctx.me,
|
||||
version = version,
|
||||
color = pov.color,
|
||||
owner = owner && !isHijack(pov, tokenOption, ctx)
|
||||
) map {
|
||||
case Connected(enumerator, member) ⇒ {
|
||||
(Iteratee.foreach[JsValue](
|
||||
controller(hub, uid, member, PovRef(pov.gameId, member.color))
|
||||
) mapDone { _ ⇒
|
||||
hub ! Quit(uid)
|
||||
},
|
||||
enumerator)
|
||||
}
|
||||
}
|
||||
} yield socket): SocketFuture
|
||||
}) | connectionFail
|
||||
|
||||
// full game ids that have been hijacked
|
||||
private val hijacks = collection.mutable.Set[String]()
|
||||
|
||||
private def isHijack(pov: Pov, token: Option[String], ctx: Context) =
|
||||
if (hijacks contains pov.fullId) true
|
||||
else if (token != pov.game.token.some) true ~ { _ ⇒
|
||||
println("[websocket] hijacking detected %s %s".format(pov.fullId, ctx.toString))
|
||||
hijacks += pov.fullId
|
||||
}
|
||||
else false
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package lila.app
|
||||
package round
|
||||
|
||||
import game.{ GameRepo, PgnRepo, DbGame, Rewind }
|
||||
import scalaz.effects._
|
||||
|
||||
final class Takeback(
|
||||
gameRepo: GameRepo,
|
||||
pgnRepo: PgnRepo,
|
||||
rewind: Rewind,
|
||||
messenger: Messenger) {
|
||||
|
||||
def apply(game: DbGame, pgn: String, initialFen: Option[String]): Valid[IO[List[Event]]] =
|
||||
rewind(game, pgn, initialFen) map {
|
||||
case (progress, newPgn) ⇒ pgnRepo.save(game.id, newPgn) flatMap { _ ⇒ save(progress) }
|
||||
} mapFail failInfo(game)
|
||||
|
||||
def double(game: DbGame, pgn: String, initialFen: Option[String]): Valid[IO[List[Event]]] = {
|
||||
for {
|
||||
first ← rewind(game, pgn, initialFen)
|
||||
(prog1, pgn1) = first
|
||||
second ← rewind(prog1.game, pgn1, initialFen) map {
|
||||
case (progress, newPgn) ⇒ (prog1 withGame progress.game, newPgn)
|
||||
}
|
||||
(prog2, pgn2) = second
|
||||
} yield pgnRepo.save(game.id, pgn2) flatMap { _ ⇒ save(prog2) }
|
||||
} mapFail failInfo(game)
|
||||
|
||||
def failInfo(game: DbGame) =
|
||||
(failures: Failures) ⇒ "Takeback %s".format(game.id) <:: failures
|
||||
|
||||
private def save(p1: Progress): IO[List[Event]] = for {
|
||||
_ ← messenger.systemMessage(p1.game, _.takebackPropositionAccepted)
|
||||
p2 = p1 + Event.Reload()
|
||||
_ ← gameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
package lila.app
|
||||
|
||||
import ornicar.scalalib._
|
||||
|
||||
package object round {
|
||||
|
||||
type ValidIOEvents = Valid[scalaz.effects.IO[List[Event]]]
|
||||
}
|
|
@ -91,7 +91,7 @@ object ApplicationBuild extends Build {
|
|||
)
|
||||
|
||||
lazy val round = project("round", Seq(
|
||||
common, db, memo, hub, socket, chess, game, user, i18n)).settings(
|
||||
common, db, memo, hub, socket, chess, game, user, security, i18n, ai)).settings(
|
||||
libraryDependencies ++= provided(
|
||||
playApi, reactivemongo, playReactivemongo)
|
||||
)
|
||||
|
|
|
@ -1,27 +1,88 @@
|
|||
package lila.round
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import lila.common.PimpedConfig._
|
||||
import com.typesafe.config.Config
|
||||
import akka.actor._
|
||||
|
||||
final class Env(
|
||||
config: Config,
|
||||
system: ActorSystem,
|
||||
eloUpdater: lila.user.EloUpdater,
|
||||
flood: lila.security.Flood,
|
||||
db: lila.db.Env,
|
||||
hub: lila.hub.Env) {
|
||||
hub: lila.hub.Env,
|
||||
ai: lila.ai.Ai,
|
||||
i18nKeys: lila.i18n.I18nKeys) {
|
||||
|
||||
private val settings = new {
|
||||
val MessageLifetime = config duration "message.lifetime"
|
||||
val MessageTtl = config duration "message.ttl"
|
||||
val UidTimeout = config duration "uid.timeout"
|
||||
val HubTimeout = config duration "hub.timeout"
|
||||
val PlayerTimeout = config duration "player.timeout"
|
||||
val AnimationDelay = config duration "animation.delay"
|
||||
val Moretime = config seconds "moretime"
|
||||
val Moretime = config duration "moretime"
|
||||
val CollectionRoom = config getString "collection.room"
|
||||
val CollectionWatcherRoom = config getString "collection.watcher_room"
|
||||
val SocketName = config getString "socket.name"
|
||||
val SocketTimeout = config duration "socket.timeout"
|
||||
val FinisherLockTimeout = config duration "finisher.lock.timeout"
|
||||
val HijackTimeout = config duration "hijack.timeout"
|
||||
}
|
||||
import settings._
|
||||
|
||||
lazy val history = () ⇒ new History(ttl = MessageTtl)
|
||||
|
||||
lazy val socketHub = system.actorOf(Props(new SocketHub(
|
||||
makeHistory = history,
|
||||
uidTimeout = UidTimeout,
|
||||
socketTimeout = SocketTimeout,
|
||||
playerTimeout = PlayerTimeout,
|
||||
gameSocketName = gameId ⇒ SocketName + "-" + gameId
|
||||
)), name = SocketName)
|
||||
|
||||
lazy val socketHandler = new SocketHandler(
|
||||
hand = hand,
|
||||
socketHub = socketHub,
|
||||
messenger = messenger,
|
||||
moveNotifier = moveNotifier,
|
||||
flood = flood,
|
||||
hijack = hijack)
|
||||
|
||||
lazy val hand = new Hand(
|
||||
messenger = messenger,
|
||||
ai = ai,
|
||||
finisher = finisher,
|
||||
takeback = takeback,
|
||||
socketHub = socketHub,
|
||||
moretimeDuration = Moretime)
|
||||
|
||||
lazy val finisher = new Finisher(
|
||||
messenger = messenger,
|
||||
eloUpdater = eloUpdater,
|
||||
eloCalculator = eloCalculator,
|
||||
finisherLock = finisherLock,
|
||||
indexer = hub.actor.gameIndexer,
|
||||
tournamentOrganizer = hub.actor.tournamentOrganizer)
|
||||
|
||||
lazy val meddler = new Meddler(
|
||||
finisher = finisher,
|
||||
socketHub = socketHub)
|
||||
|
||||
val animationDelay = AnimationDelay
|
||||
|
||||
private lazy val hijack = new Hijack(HijackTimeout)
|
||||
|
||||
private lazy val eloCalculator = new chess.EloCalculator(false)
|
||||
|
||||
private lazy val finisherLock = new FinisherLock(timeout = FinisherLockTimeout)
|
||||
|
||||
private lazy val takeback = new Takeback(messenger)
|
||||
|
||||
private lazy val messenger = new Messenger(i18nKeys)
|
||||
|
||||
private lazy val moveNotifier = new MoveNotifier(
|
||||
hub = hub.socket.hub,
|
||||
monitor = hub.socket.monitor)
|
||||
|
||||
private[round] lazy val roomColl = db(CollectionRoom)
|
||||
|
||||
private[round] lazy val watcherRoomColl = db(CollectionWatcherRoom)
|
||||
|
@ -32,6 +93,10 @@ object Env {
|
|||
lazy val current = "[boot] round" describes new Env(
|
||||
config = lila.common.PlayApp loadConfig "round",
|
||||
system = lila.common.PlayApp.system,
|
||||
eloUpdater = lila.user.Env.current.eloUpdater,
|
||||
flood = lila.security.Env.current.flood,
|
||||
db = lila.db.Env.current,
|
||||
hub = lila.hub.Env.current)
|
||||
hub = lila.hub.Env.current,
|
||||
ai = lila.ai.Env.current.ai,
|
||||
i18nKeys = lila.i18n.Env.current.keys)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package lila.round
|
||||
|
||||
import lila.game.{ GameRepo, Game, Pov, Event }
|
||||
import lila.user.{ User, UserRepo, EloUpdater }
|
||||
import lila.i18n.I18nKey.{ Select ⇒ SelectI18nKey }
|
||||
import chess.{ EloCalculator, Status, Color }
|
||||
import Status._
|
||||
import Color._
|
||||
import lila.game.tube.gameTube
|
||||
import lila.user.tube.userTube
|
||||
import lila.db.api._
|
||||
import lila.hub.actorApi.round._
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
import scalaz.{ OptionTs, Success }
|
||||
|
||||
private[round] final class Finisher(
|
||||
tournamentOrganizer: ActorRef,
|
||||
messenger: Messenger,
|
||||
eloUpdater: EloUpdater,
|
||||
eloCalculator: EloCalculator,
|
||||
finisherLock: FinisherLock,
|
||||
indexer: ActorRef) extends OptionTs {
|
||||
|
||||
private type FuEvents = Fu[List[Event]]
|
||||
private type ValidFuEvents = Valid[FuEvents]
|
||||
|
||||
def abort(pov: Pov): ValidFuEvents =
|
||||
if (pov.game.abortable) finish(pov.game, Aborted)
|
||||
else !!("game is not abortable")
|
||||
|
||||
def forceAbort(game: Game): ValidFuEvents =
|
||||
if (game.playable) finish(game, Aborted)
|
||||
else !!("game is not playable, cannot be force aborted")
|
||||
|
||||
def resign(pov: Pov): ValidFuEvents =
|
||||
if (pov.game.resignable) finish(pov.game, Resign, Some(!pov.color))
|
||||
else !!("game is not resignable")
|
||||
|
||||
def resignForce(pov: Pov): ValidFuEvents =
|
||||
if (pov.game.resignable && !pov.game.hasAi)
|
||||
finish(pov.game, Timeout, Some(pov.color))
|
||||
else !!("game is not resignable")
|
||||
|
||||
def drawClaim(pov: Pov): ValidFuEvents = pov match {
|
||||
case Pov(game, color) if game.playable && game.player.color == color && game.toChessHistory.threefoldRepetition ⇒ finish(game, Draw)
|
||||
case Pov(game, color) ⇒ !!("game is not threefold repetition")
|
||||
}
|
||||
|
||||
def drawAccept(pov: Pov): ValidFuEvents =
|
||||
if (pov.opponent.isOfferingDraw)
|
||||
finish(pov.game, Draw, None, Some(_.drawOfferAccepted))
|
||||
else !!("opponent is not proposing a draw")
|
||||
|
||||
def drawForce(game: Game): ValidFuEvents = finish(game, Draw, None, None)
|
||||
|
||||
def outoftime(game: Game): ValidFuEvents = game.outoftimePlayer.fold(
|
||||
!![FuEvents]("no outoftime applicable " + game.clock.fold("-")(_.remainingTimes.toString))
|
||||
) { player ⇒
|
||||
finish(game, Outoftime, Some(!player.color) filter game.toChess.board.hasEnoughMaterialToMate)
|
||||
}
|
||||
|
||||
def outoftimes(games: List[Game]): Funit =
|
||||
(games map outoftime collect {
|
||||
case Success(future) ⇒ future
|
||||
}).sequence.void
|
||||
|
||||
def moveFinish(game: Game, color: Color): Fu[List[Event]] =
|
||||
(game.status match {
|
||||
case Mate ⇒ finish(game, Mate, Some(color))
|
||||
case status @ (Stalemate | Draw) ⇒ finish(game, status)
|
||||
case _ ⇒ success(fuccess(Nil)): ValidFuEvents
|
||||
}) | fuccess(Nil)
|
||||
|
||||
private def finish(
|
||||
game: Game,
|
||||
status: Status,
|
||||
winner: Option[Color] = None,
|
||||
message: Option[SelectI18nKey] = None): ValidFuEvents =
|
||||
if (finisherLock isLocked game) !!("game finish is locked")
|
||||
else success(for {
|
||||
_ ← fuccess(finisherLock lock game)
|
||||
p1 = game.finish(status, winner)
|
||||
p2 ← message.fold(fuccess(p1)) { m ⇒
|
||||
messenger.systemMessage(p1.game, m) map p1.++
|
||||
}
|
||||
_ ← GameRepo save p2
|
||||
g = p2.game
|
||||
winnerId = winner flatMap (g.player(_).userId)
|
||||
_ ← GameRepo.finish(g.id, winnerId)
|
||||
_ ← updateElo(g)
|
||||
_ ← incNbGames(g, White) doIf (g.status >= Status.Mate)
|
||||
_ ← incNbGames(g, Black) doIf (g.status >= Status.Mate)
|
||||
_ ← fuccess(indexer ! lila.game.actorApi.InsertGame(g))
|
||||
_ ← fuccess(tournamentOrganizer ! FinishGame(g.id))
|
||||
} yield p2.events)
|
||||
|
||||
private def incNbGames(game: Game, color: Color): Funit =
|
||||
game.player(color).userId zmap { id ⇒
|
||||
UserRepo.incNbGames(id, game.rated, game.hasAi,
|
||||
result = game.wonBy(color).fold(0)(_.fold(1, -1)).some filterNot (_ ⇒ game.hasAi || game.aborted)
|
||||
)
|
||||
}
|
||||
|
||||
private def updateElo(game: Game): Funit = ~{
|
||||
for {
|
||||
whiteUserId ← game.player(White).userId
|
||||
blackUserId ← game.player(Black).userId
|
||||
if whiteUserId != blackUserId
|
||||
} yield (for {
|
||||
whiteUser ← optionT($find.byId[User](whiteUserId))
|
||||
blackUser ← optionT($find.byId[User](blackUserId))
|
||||
_ ← optionT {
|
||||
val (whiteElo, blackElo) = eloCalculator.calculate(whiteUser, blackUser, game.winnerColor)
|
||||
val (whiteDiff, blackDiff) = (whiteElo - whiteUser.elo, blackElo - blackUser.elo)
|
||||
val cheaterWin = (whiteDiff > 0 && whiteUser.engine) || (blackDiff > 0 && blackUser.engine)
|
||||
GameRepo.setEloDiffs(game.id, whiteDiff, blackDiff) >>
|
||||
eloUpdater.game(whiteUser, whiteElo, blackUser.elo) >>
|
||||
eloUpdater.game(blackUser, blackElo, whiteUser.elo) doUnless cheaterWin inject true.some
|
||||
}
|
||||
} yield ()).value.void
|
||||
} doIf (game.finished && game.rated && game.turns >= 2)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package lila.round
|
||||
|
||||
import lila.game.Game
|
||||
import lila.memo.ExpireSetMemo
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
|
||||
private[round] final class FinisherLock(timeout: Duration) {
|
||||
|
||||
private val cache = new ExpireSetMemo(timeout)
|
||||
|
||||
def isLocked(game: Game): Boolean = cache get game.id
|
||||
|
||||
def lock(game: Game) { cache put game.id }
|
||||
}
|
|
@ -0,0 +1,261 @@
|
|||
package lila.round
|
||||
|
||||
import lila.ai.Ai
|
||||
import lila.game.{ GameRepo, PgnRepo, Pov, PovRef, Handler, Event, Progress }
|
||||
import lila.i18n.I18nKey.{ Select ⇒ SelectI18nKey }
|
||||
import chess.Role
|
||||
import chess.Pos.posAt
|
||||
import chess.format.Forsyth
|
||||
import actorApi._
|
||||
import makeTimeout.large
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.ask
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
import scala.concurrent.duration.Duration
|
||||
|
||||
private[round] final class Hand(
|
||||
messenger: Messenger,
|
||||
takeback: Takeback,
|
||||
ai: Ai,
|
||||
finisher: Finisher,
|
||||
socketHub: ActorRef,
|
||||
moretimeDuration: Duration) extends Handler {
|
||||
|
||||
type FuValidEvents = Fu[Valid[List[Event]]]
|
||||
type PlayResult = Fu[Valid[(List[Event], String, Option[String])]]
|
||||
|
||||
def play(
|
||||
povRef: PovRef,
|
||||
origString: String,
|
||||
destString: String,
|
||||
promString: Option[String] = None,
|
||||
blur: Boolean = false,
|
||||
lag: Int = 0): PlayResult = fromPovFu(povRef) {
|
||||
case Pov(g1, color) ⇒ PgnRepo get g1.id flatMap { pgnString ⇒
|
||||
(for {
|
||||
g2 ← g1.validIf(g1 playableBy color, "Game not playable %s %s, on move %d".format(origString, destString, g1.toChess.fullMoveNumber))
|
||||
orig ← posAt(origString) toValid "Wrong orig " + origString
|
||||
dest ← posAt(destString) toValid "Wrong dest " + destString
|
||||
promotion = Role promotable promString
|
||||
chessGame = g2.toChess withPgnMoves pgnString
|
||||
newChessGameAndMove ← chessGame(orig, dest, promotion, lag)
|
||||
(newChessGame, move) = newChessGameAndMove
|
||||
} yield g2.update(newChessGame, move, blur)).prefixFailuresWith(povRef + " - ").fold(
|
||||
e ⇒ fuccess(failure(e)), {
|
||||
case (progress, pgn) ⇒ if (progress.game.finished) (for {
|
||||
_ ← GameRepo save progress
|
||||
_ ← PgnRepo.save(povRef.gameId, pgn)
|
||||
finishEvents ← finisher.moveFinish(progress.game, color)
|
||||
events = progress.events ::: finishEvents
|
||||
} yield playResult(events, progress))
|
||||
else if (progress.game.player.isAi && progress.game.playable) for {
|
||||
initialFen ← progress.game.variant.standard ?? {
|
||||
GameRepo initialFen progress.game.id
|
||||
}
|
||||
aiResult ← ai.play(
|
||||
progress.game.toChess,
|
||||
pgn,
|
||||
initialFen,
|
||||
~progress.game.aiLevel)
|
||||
eventsAndFen ← aiResult.fold(
|
||||
err ⇒ fuccess(failure(err)), {
|
||||
case (newChessGame, move) ⇒ {
|
||||
val (prog2, pgn2) = progress.game.update(newChessGame, move)
|
||||
val progress2 = progress flatMap { _ ⇒ prog2 }
|
||||
(for {
|
||||
_ ← GameRepo save progress2
|
||||
_ ← PgnRepo.save(povRef.gameId, pgn2)
|
||||
finishEvents ← finisher.moveFinish(progress2.game, !color)
|
||||
events = progress2.events ::: finishEvents
|
||||
} yield playResult(events, progress2))
|
||||
}
|
||||
}): PlayResult
|
||||
} yield eventsAndFen
|
||||
else (for {
|
||||
_ ← GameRepo save progress
|
||||
_ ← PgnRepo.save(povRef.gameId, pgn)
|
||||
events = progress.events
|
||||
} yield playResult(events, progress))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private def playResult(events: List[Event], progress: Progress) = success((
|
||||
events,
|
||||
Forsyth exportBoard progress.game.toChess.board,
|
||||
progress.game.lastMove
|
||||
))
|
||||
|
||||
def abort(fullId: String): FuValidEvents = attempt(fullId, finisher.abort)
|
||||
|
||||
def resign(fullId: String): FuValidEvents = attempt(fullId, finisher.resign)
|
||||
|
||||
def resignForce(fullId: String): FuValidEvents =
|
||||
GameRepo pov fullId flatMap { povOption ⇒
|
||||
povOption.fold(fuccess(!!("No such game")): FuValidEvents) { pov ⇒
|
||||
(socketHub ? IsGone(pov.game.id, !pov.color) flatMap {
|
||||
case true ⇒ finisher resignForce pov fold (
|
||||
err ⇒ fuccess(failure(err)): FuValidEvents,
|
||||
fu ⇒ fu map { success(_) }
|
||||
)
|
||||
case _ ⇒ fuccess(!!("Opponent is not gone")): FuValidEvents
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def outoftime(ref: PovRef): FuValidEvents = attemptRef(ref, finisher outoftime _.game)
|
||||
|
||||
def drawClaim(fullId: String): FuValidEvents = attempt(fullId, finisher.drawClaim)
|
||||
|
||||
def drawAccept(fullId: String): FuValidEvents = attempt(fullId, finisher.drawAccept)
|
||||
|
||||
def drawOffer(fullId: String): FuValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1 playerCanOfferDraw color) {
|
||||
if (g1.player(!color).isOfferingDraw) finisher drawAccept pov
|
||||
else success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.drawOfferSent) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _ offerDraw g.turns) }
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
}
|
||||
else !!("invalid draw offer " + fullId)
|
||||
})
|
||||
|
||||
def drawCancel(fullId: String): FuValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (pov.player.isOfferingDraw) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.drawOfferCanceled) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.removeDrawOffer) }
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no draw offer to cancel " + fullId)
|
||||
})
|
||||
|
||||
def drawDecline(fullId: String): FuValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1.player(!color).isOfferingDraw) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.drawOfferDeclined) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(!color, _.removeDrawOffer) }
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no draw offer to decline " + fullId)
|
||||
})
|
||||
|
||||
def rematchCancel(fullId: String): FuValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
pov.player.isOfferingRematch.fold(
|
||||
success(for {
|
||||
p1 ← messenger.systemMessage(g1, _.rematchOfferCanceled) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.removeRematchOffer) }
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events),
|
||||
!!("no rematch offer to cancel " + fullId)
|
||||
)
|
||||
})
|
||||
|
||||
def rematchDecline(fullId: String): FuValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
g1.player(!color).isOfferingRematch.fold(
|
||||
success(for {
|
||||
p1 ← messenger.systemMessage(g1, _.rematchOfferDeclined) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(!color, _.removeRematchOffer) }
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events),
|
||||
!!("no rematch offer to decline " + fullId)
|
||||
)
|
||||
})
|
||||
|
||||
def takebackAccept(fullId: String): FuValidEvents = fromPov(fullId) { pov ⇒
|
||||
if (pov.opponent.isProposingTakeback && pov.game.nonTournament) for {
|
||||
fen ← GameRepo initialFen pov.game.id
|
||||
pgn ← PgnRepo get pov.game.id
|
||||
res ← takeback(pov.game, pgn, fen).sequence
|
||||
} yield res
|
||||
else fuccess { !!("opponent is not proposing a takeback") }
|
||||
}
|
||||
|
||||
def takebackOffer(fullId: String): FuValidEvents = fromPov(fullId) {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1.playable && g1.bothPlayersHaveMoved && g1.nonTournament) {
|
||||
for {
|
||||
fen ← GameRepo initialFen pov.game.id
|
||||
pgn ← PgnRepo get pov.game.id
|
||||
result ← if (g1.player(!color).isAi)
|
||||
takeback.double(pov.game, pgn, fen).sequence
|
||||
else if (g1.player(!color).isProposingTakeback)
|
||||
takeback(pov.game, pgn, fen).sequence
|
||||
else for {
|
||||
p1 ← messenger.systemMessage(g1, _.takebackPropositionSent) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.proposeTakeback) }
|
||||
_ ← GameRepo save p2
|
||||
} yield success(p2.events)
|
||||
} yield result
|
||||
}
|
||||
else fuccess { !!("invalid takeback proposition " + fullId) }
|
||||
}
|
||||
|
||||
def takebackCancel(fullId: String): FuValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (pov.player.isProposingTakeback) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.takebackPropositionCanceled) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(color, _.removeTakebackProposition) }
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no takeback proposition to cancel " + fullId)
|
||||
})
|
||||
|
||||
def takebackDecline(fullId: String): FuValidEvents = attempt(fullId, {
|
||||
case pov @ Pov(g1, color) ⇒
|
||||
if (g1.player(!color).isProposingTakeback) success {
|
||||
for {
|
||||
p1 ← messenger.systemMessage(g1, _.takebackPropositionDeclined) map { es ⇒
|
||||
Progress(g1, Event.ReloadTable(!color) :: es)
|
||||
}
|
||||
p2 = p1 map { g ⇒ g.updatePlayer(!color, _.removeTakebackProposition) }
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
||||
else !!("no takeback proposition to decline " + fullId)
|
||||
})
|
||||
|
||||
def moretime(ref: PovRef): FuValidEvents = attemptRef(ref, pov ⇒
|
||||
pov.game.clock filter (_ ⇒ pov.game.moretimeable) map { clock ⇒
|
||||
val color = !pov.color
|
||||
val newClock = clock.giveTime(color, moretimeDuration.toSeconds)
|
||||
val progress = pov.game withClock newClock
|
||||
for {
|
||||
events ← messenger.systemMessage(
|
||||
progress.game, ((_.untranslated(
|
||||
"%s + %d seconds".format(color, moretimeDuration.toSeconds)
|
||||
)): SelectI18nKey)
|
||||
)
|
||||
progress2 = progress ++ (Event.Clock(newClock) :: events)
|
||||
_ ← GameRepo save progress2
|
||||
} yield progress2.events
|
||||
} toValid "cannot add moretime"
|
||||
)
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package lila.round
|
||||
|
||||
import lila.game.Pov
|
||||
import lila.user.Context
|
||||
import lila.memo.ExpireSetMemo
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
|
||||
private[round] final class Hijack(timeout: Duration) {
|
||||
|
||||
// full game ids that have been hijacked
|
||||
private val hijacks = new ExpireSetMemo(timeout)
|
||||
|
||||
def apply(pov: Pov, token: String, ctx: Context) =
|
||||
if (hijacks get pov.fullId) true
|
||||
else if (token != pov.game.token) true ~ { _ ⇒
|
||||
loginfo("[websocket] hijacking detected %s %s".format(pov.fullId, ctx.toString))
|
||||
hijacks put pov.fullId
|
||||
}
|
||||
else false
|
||||
}
|
|
@ -1,35 +1,40 @@
|
|||
package lila.round
|
||||
|
||||
import scala.concurrent.duration.Duration
|
||||
import akka.actor._
|
||||
|
||||
import actorApi._
|
||||
import lila.game.Event
|
||||
import lila.memo.{ Builder ⇒ MemoBuilder }
|
||||
|
||||
final class History(ttl: Duration) {
|
||||
private[round] final class History(ttl: Duration) extends Actor {
|
||||
|
||||
private var privateVersion = 0
|
||||
private var version = 0
|
||||
private val events = MemoBuilder.expiry[Int, VersionedEvent](ttl)
|
||||
|
||||
def version = privateVersion
|
||||
def receive = {
|
||||
|
||||
// none if version asked is > to history version
|
||||
// none if an event is missing (asked too old version)
|
||||
def since(v: Int): Option[List[VersionedEvent]] = version |> { u ⇒
|
||||
if (v > u) None
|
||||
else if (v == u) Some(Nil)
|
||||
else ((v + 1 to u).toList map get).flatten.some
|
||||
case GetVersion ⇒ sender ! version
|
||||
|
||||
// none if version asked is > to history version
|
||||
// none if an event is missing (asked too old version)
|
||||
case GetEventsSince(v: Int) ⇒ sender ! {
|
||||
if (v > version) None
|
||||
else if (v == version) Some(Nil)
|
||||
else ((v + 1 to version).toList map get).flatten.some
|
||||
}
|
||||
|
||||
case AddEvent(event) ⇒ {
|
||||
version = version + 1
|
||||
sender ! VersionedEvent(
|
||||
version = version,
|
||||
typ = event.typ,
|
||||
data = event.data,
|
||||
only = event.only,
|
||||
owner = event.owner,
|
||||
watcher = event.watcher) ~ { events.put(version, _) }
|
||||
}
|
||||
}
|
||||
|
||||
private def get(v: Int): Option[VersionedEvent] = Option(events getIfPresent v)
|
||||
|
||||
def +=(event: Event): VersionedEvent = {
|
||||
privateVersion = version + 1
|
||||
VersionedEvent(
|
||||
version = version,
|
||||
typ = event.typ,
|
||||
data = event.data,
|
||||
only = event.only,
|
||||
owner = event.owner,
|
||||
watcher = event.watcher) ~ { events.put(version, _) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package lila.round
|
||||
|
||||
import actorApi._
|
||||
import lila.game.{ Game, GameRepo, PovRef, Pov }
|
||||
import lila.game.tube.gameTube
|
||||
import lila.db.api._
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
|
||||
private[round] final class Meddler(
|
||||
finisher: Finisher,
|
||||
socketHub: ActorRef) {
|
||||
|
||||
def forceAbort(id: String) {
|
||||
$find.byId(id) foreach {
|
||||
_.fold(logwarn("Cannot abort missing game " + id)) { game ⇒
|
||||
(finisher forceAbort game) fold (
|
||||
err ⇒ logwarn(err.shows),
|
||||
_ foreach { events ⇒ socketHub ! GameEvents(game.id, events) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def resign(pov: Pov) {
|
||||
(finisher resign pov).fold(
|
||||
err ⇒ logwarn(err.shows),
|
||||
_ foreach { events ⇒ socketHub ! GameEvents(pov.game.id, events) }
|
||||
)
|
||||
}
|
||||
|
||||
def resign(povRef: PovRef) {
|
||||
GameRepo pov povRef foreach {
|
||||
_.fold(logwarn("Cannot resign missing game " + povRef))(resign)
|
||||
}
|
||||
}
|
||||
|
||||
def finishAbandoned(game: Game) {
|
||||
game.abandoned.fold(
|
||||
finisher.resign(Pov(game, game.player))
|
||||
.prefixFailuresWith("Finish abandoned game " + game.id)
|
||||
.fold(err ⇒ logwarn(err.shows), _.void),
|
||||
logwarn("Game is not abandoned")
|
||||
)
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import lila.db.api._
|
|||
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
|
||||
final class Messenger(i18nKeys: I18nKeys) {
|
||||
private[round] final class Messenger(i18nKeys: I18nKeys) {
|
||||
|
||||
private val nbMessagesCopiedToRematch = 20
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package lila.round
|
||||
|
||||
import lila.socket.actorApi.Fen
|
||||
import lila.hub.actorApi.monitor.AddMove
|
||||
|
||||
import akka.actor.ActorRef
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
|
||||
private[round] final class MoveNotifier(
|
||||
hub: ActorRef,
|
||||
monitor: ActorRef) {
|
||||
|
||||
def apply(gameId: String, fen: String, lastMove: Option[String]) {
|
||||
hub ! Fen(gameId, fen, lastMove)
|
||||
monitor ! AddMove
|
||||
}
|
||||
}
|
|
@ -1,29 +1,31 @@
|
|||
package lila.app
|
||||
package round
|
||||
package lila.round
|
||||
|
||||
import socket._
|
||||
import lila.socket._
|
||||
import lila.socket.actorApi._
|
||||
import chess.{ Color, White, Black }
|
||||
import lila.game.Event
|
||||
import actorApi._
|
||||
import makeTimeout.short
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.{ ask, pipe }
|
||||
import scala.concurrent.duration._
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.iteratee._
|
||||
import play.api.Play.current
|
||||
import play.api.libs.concurrent.Akka
|
||||
import scalaz.effects._
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
|
||||
final class Hub(
|
||||
private[round] final class Socket(
|
||||
gameId: String,
|
||||
history: History,
|
||||
uidTimeout: Int,
|
||||
hubTimeout: Int,
|
||||
playerTimeout: Int) extends HubActor[Member](uidTimeout) {
|
||||
history: ActorRef,
|
||||
uidTimeout: Duration,
|
||||
socketTimeout: Duration,
|
||||
playerTimeout: Duration) extends SocketActor[Member](uidTimeout) {
|
||||
|
||||
var lastPingTime = nowMillis
|
||||
private var lastPingTime = nowMillis
|
||||
|
||||
// when the players have been seen online for the last time
|
||||
var whiteTime = nowMillis
|
||||
var blackTime = nowMillis
|
||||
private var whiteTime = nowMillis
|
||||
private var blackTime = nowMillis
|
||||
|
||||
def receiveSpecific = {
|
||||
|
||||
|
@ -34,8 +36,10 @@ final class Hub(
|
|||
if (playerIsGone(o.color)) notifyGone(o.color, false)
|
||||
playerTime(o.color, lastPingTime)
|
||||
}
|
||||
withMember(uid) { m ⇒
|
||||
history.since(v).fold(resync(m))(batch(m, _))
|
||||
withMember(uid) { member ⇒
|
||||
history ? GetEventsSince(v) foreach {
|
||||
case MaybeEvents(events) ⇒ events.fold(resync(member))(batch(member, _))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +47,7 @@ final class Hub(
|
|||
|
||||
case Broom ⇒ {
|
||||
broom()
|
||||
if (lastPingTime < (nowMillis - hubTimeout)) {
|
||||
if (lastPingTime < (nowMillis - socketTimeout.toMillis)) {
|
||||
context.parent ! CloseGame(gameId)
|
||||
}
|
||||
Color.all foreach { c ⇒
|
||||
|
@ -51,7 +55,7 @@ final class Hub(
|
|||
}
|
||||
}
|
||||
|
||||
case GetGameVersion(_) ⇒ sender ! history.version
|
||||
case GetGameVersion(_) ⇒ history ? GetVersion pipeTo sender
|
||||
|
||||
case IsConnectedOnGame(_, color) ⇒ sender ! ownerOf(color).isDefined
|
||||
|
||||
|
@ -92,7 +96,7 @@ final class Hub(
|
|||
black = ownerOf(Black).isDefined,
|
||||
watchers = members.values
|
||||
.filter(_.watcher)
|
||||
.map(_.username)
|
||||
.map(_.userId)
|
||||
.toList.partition(_.isDefined) match {
|
||||
case (users, anons) ⇒ users.flatten.distinct |> { userList ⇒
|
||||
anons.size match {
|
||||
|
@ -104,8 +108,11 @@ final class Hub(
|
|||
})
|
||||
|
||||
def notify(events: List[Event]) {
|
||||
val vevents = events map history.+=
|
||||
members.values foreach { m ⇒ batch(m, vevents) }
|
||||
(events map { event ⇒
|
||||
history ? AddEvent(event) mapTo manifest[VersionedEvent]
|
||||
}).sequence foreach { vevents ⇒
|
||||
members.values foreach { m ⇒ batch(m, vevents) }
|
||||
}
|
||||
}
|
||||
|
||||
def batch(member: Member, vevents: List[VersionedEvent]) {
|
||||
|
@ -143,5 +150,6 @@ final class Hub(
|
|||
color.fold(whiteTime = time, blackTime = time)
|
||||
}
|
||||
|
||||
def playerIsGone(color: Color) = playerTime(color) < (nowMillis - playerTimeout)
|
||||
def playerIsGone(color: Color) =
|
||||
playerTime(color) < (nowMillis - playerTimeout.toMillis)
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package lila.round
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.ask
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
import play.api.libs.json.JsObject
|
||||
import scalaz.{ Success, Failure }
|
||||
|
||||
import actorApi._
|
||||
import lila.game.{ Pov, PovRef, GameRepo }
|
||||
import lila.user.{ User, Context }
|
||||
import chess.Color
|
||||
import lila.socket._
|
||||
import lila.socket.actorApi._
|
||||
import lila.security.Flood
|
||||
import lila.common.PimpedJson._
|
||||
import makeTimeout.short
|
||||
|
||||
private[round] final class SocketHandler(
|
||||
hand: Hand,
|
||||
socketHub: ActorRef,
|
||||
messenger: Messenger,
|
||||
moveNotifier: MoveNotifier,
|
||||
flood: Flood,
|
||||
hijack: Hijack) {
|
||||
|
||||
private def controller(
|
||||
socket: ActorRef,
|
||||
uid: String,
|
||||
povRef: PovRef)(member: Member): Handler.Controller =
|
||||
if (member.owner) {
|
||||
case ("p", o) ⇒ o int "v" foreach { v ⇒ socket ! PingVersion(uid, v) }
|
||||
case ("talk", o) ⇒ for {
|
||||
txt ← o str "d"
|
||||
if member.canChat
|
||||
if flood.allowMessage(uid, txt)
|
||||
} messenger.playerMessage(povRef, txt) foreach { events ⇒
|
||||
socket ! Events(events)
|
||||
}
|
||||
case ("move", o) ⇒ parseMove(o) foreach {
|
||||
case (orig, dest, prom, blur, lag) ⇒ {
|
||||
socket ! Ack(uid)
|
||||
hand.play(povRef, orig, dest, prom, blur, lag) onSuccess {
|
||||
case Failure(fs) ⇒ {
|
||||
socket ! Resync(uid)
|
||||
logwarn(fs.shows)
|
||||
}
|
||||
case Success((events, fen, lastMove)) ⇒ {
|
||||
socketHub ! GameEvents(povRef.gameId, events)
|
||||
moveNotifier(povRef.gameId, fen, lastMove)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case ("moretime", o) ⇒ hand moretime povRef foreach {
|
||||
_ foreach { events ⇒ socket ! Events(events) }
|
||||
}
|
||||
case ("outoftime", o) ⇒ hand outoftime povRef foreach {
|
||||
_ foreach { events ⇒ socket ! Events(events) }
|
||||
}
|
||||
}
|
||||
else {
|
||||
case ("p", o) ⇒ o int "v" foreach { v ⇒ socket ! PingVersion(uid, v) }
|
||||
case ("talk", o) ⇒ for {
|
||||
txt ← o str "d"
|
||||
if member.canChat
|
||||
if flood.allowMessage(uid, txt)
|
||||
} messenger.watcherMessage(
|
||||
povRef.gameId,
|
||||
member.userId,
|
||||
txt) foreach { events ⇒ socket ! Events(events) }
|
||||
}
|
||||
|
||||
def joinWatcher(
|
||||
gameId: String,
|
||||
colorName: String,
|
||||
version: Int,
|
||||
uid: String,
|
||||
ctx: Context): Fu[JsSocketHandler] =
|
||||
GameRepo.pov(gameId, colorName) flatMap {
|
||||
_ zmap { join(_, false, version, uid, "", ctx) }
|
||||
}
|
||||
|
||||
def joinPlayer(
|
||||
fullId: String,
|
||||
version: Int,
|
||||
uid: String,
|
||||
token: String,
|
||||
ctx: Context): Fu[JsSocketHandler] =
|
||||
GameRepo.pov(fullId) flatMap {
|
||||
_ zmap { join(_, true, version, uid, token, ctx) }
|
||||
}
|
||||
|
||||
private def join(
|
||||
pov: Pov,
|
||||
owner: Boolean,
|
||||
version: Int,
|
||||
uid: String,
|
||||
token: String,
|
||||
ctx: Context): Fu[JsSocketHandler] = for {
|
||||
socket ← socketHub ? GetSocket(pov.gameId) mapTo manifest[ActorRef]
|
||||
join = Join(
|
||||
uid = uid,
|
||||
user = ctx.me,
|
||||
version = version,
|
||||
color = pov.color,
|
||||
owner = owner && !hijack(pov, token, ctx))
|
||||
handler ← Handler(socket, uid, join)(controller(socket, uid, pov.ref))
|
||||
} yield handler
|
||||
|
||||
private def parseMove(o: JsObject) = for {
|
||||
d ← o obj "d"
|
||||
orig ← d str "from"
|
||||
dest ← d str "to"
|
||||
prom = d str "promotion"
|
||||
blur = (d int "b") == Some(1)
|
||||
lag = d int "lag"
|
||||
} yield (orig, dest, prom, blur, ~lag)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package lila.round
|
||||
|
||||
import actorApi._
|
||||
import lila.socket.actorApi._
|
||||
|
||||
import akka.actor._
|
||||
import akka.pattern.{ ask, pipe }
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.Future
|
||||
import play.api.libs.json._
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
import makeTimeout.short
|
||||
|
||||
private[round] final class SocketHub(
|
||||
makeHistory: () ⇒ History,
|
||||
uidTimeout: Duration,
|
||||
socketTimeout: Duration,
|
||||
playerTimeout: Duration,
|
||||
gameSocketName: String ⇒ String) extends Actor {
|
||||
|
||||
var sockets = Map.empty[String, ActorRef]
|
||||
|
||||
def receive = {
|
||||
|
||||
case Broom ⇒ broadcast(Broom)
|
||||
|
||||
case msg @ SendTo(_, _) ⇒ sockets.values foreach (_ ! msg)
|
||||
|
||||
case msg @ SendTos(_, _) ⇒ sockets.values foreach (_ ! msg)
|
||||
|
||||
case msg @ GameEvents(gameId, events) ⇒ sockets get gameId foreach (_ forward msg)
|
||||
|
||||
case GetSocket(id) ⇒ sender ! {
|
||||
(sockets get id) | {
|
||||
mkSocket(id) ~ { h ⇒ sockets = sockets + (id -> h) }
|
||||
}
|
||||
}
|
||||
|
||||
case msg @ GetGameVersion(gameId) ⇒ (sockets get gameId).fold(sender ! 0) {
|
||||
_ ? msg pipeTo sender
|
||||
}
|
||||
|
||||
case msg @ AnalysisAvailable(gameId) ⇒
|
||||
sockets get gameId foreach (_ forward msg)
|
||||
|
||||
case CloseGame(gameId) ⇒ sockets get gameId foreach { socket ⇒
|
||||
socket ! Close
|
||||
sockets = sockets - gameId
|
||||
}
|
||||
|
||||
case msg @ IsConnectedOnGame(gameId, color) ⇒ (sockets get gameId).fold(sender ! false) {
|
||||
_ ? msg pipeTo sender
|
||||
}
|
||||
|
||||
case msg @ IsGone(gameId, color) ⇒ (sockets get gameId).fold(sender ! false) {
|
||||
_ ? msg pipeTo sender
|
||||
}
|
||||
|
||||
case GetNbSockets ⇒ sender ! sockets.size
|
||||
|
||||
case GetNbMembers ⇒ Future.traverse(sockets.values) { socket ⇒
|
||||
(socket ? GetNbMembers).mapTo[Int]
|
||||
} map (_.sum) pipeTo sender
|
||||
|
||||
case GetUserIds ⇒ Future.traverse(sockets.values) { socket ⇒
|
||||
(socket ? GetUserIds).mapTo[Iterable[String]]
|
||||
} map (_.flatten) pipeTo sender
|
||||
|
||||
case msg @ NbMembers(_) ⇒ sockets.values foreach (_ ! msg)
|
||||
}
|
||||
|
||||
private def broadcast(msg: Any) {
|
||||
sockets.values foreach (_ ! msg)
|
||||
}
|
||||
|
||||
private def mkSocket(id: String): ActorRef = context.actorOf(Props(new Socket(
|
||||
gameId = id,
|
||||
history = context.actorOf(
|
||||
Props(makeHistory()),
|
||||
name = gameSocketName(id) + "-history"
|
||||
),
|
||||
uidTimeout = uidTimeout,
|
||||
socketTimeout = socketTimeout,
|
||||
playerTimeout = playerTimeout
|
||||
)), name = gameSocketName(id))
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package lila.round
|
||||
|
||||
import lila.game.{ GameRepo, Game, Rewind, Event, Progress }
|
||||
import lila.db.api._
|
||||
|
||||
import play.api.libs.concurrent.Execution.Implicits._
|
||||
|
||||
private[round] final class Takeback(messenger: Messenger) {
|
||||
|
||||
private type ValidFuEvents = Valid[Fu[List[Event]]]
|
||||
|
||||
def apply(game: Game, pgn: String, initialFen: Option[String]): ValidFuEvents =
|
||||
Rewind(game, pgn, initialFen) map {
|
||||
case (progress, newPgn) ⇒ savePgn(game.id, newPgn) >> save(progress)
|
||||
} mapFail failInfo(game)
|
||||
|
||||
def double(game: Game, pgn: String, initialFen: Option[String]): ValidFuEvents = {
|
||||
for {
|
||||
first ← Rewind(game, pgn, initialFen)
|
||||
(prog1, pgn1) = first
|
||||
second ← Rewind(prog1.game, pgn1, initialFen) map {
|
||||
case (progress, newPgn) ⇒ (prog1 withGame progress.game, newPgn)
|
||||
}
|
||||
(prog2, pgn2) = second
|
||||
} yield savePgn(game.id, pgn2) >> save(prog2)
|
||||
} mapFail failInfo(game)
|
||||
|
||||
def failInfo(game: Game) =
|
||||
(failures: Failures) ⇒ "Takeback %s".format(game.id) <:: failures
|
||||
|
||||
private def savePgn(gameId: String, pgn: String): Funit = {
|
||||
import lila.game.tube.pgnTube
|
||||
$update.field(gameId, "p", pgn, upsert = true)
|
||||
}
|
||||
|
||||
private def save(p1: Progress): Fu[List[Event]] = for {
|
||||
_ ← messenger.systemMessage(p1.game, _.takebackPropositionAccepted)
|
||||
p2 = p1 + Event.Reload
|
||||
_ ← GameRepo save p2
|
||||
} yield p2.events
|
||||
}
|
|
@ -57,13 +57,13 @@ case class Join(
|
|||
case class Events(events: List[Event])
|
||||
case class GameEvents(gameId: String, events: List[Event])
|
||||
case class GetGameVersion(gameId: String)
|
||||
case object GetVersion
|
||||
case class GetEventsSince(version: Int)
|
||||
case class MaybeEvents(events: Option[List[VersionedEvent]])
|
||||
case class AddEvent(event: Event)
|
||||
case object ClockSync
|
||||
case class IsConnectedOnGame(gameId: String, color: Color)
|
||||
case class IsGone(gameId: String, color: Color)
|
||||
case class CloseGame(gameId: String)
|
||||
case class GetHub(gameId: String)
|
||||
case object HubTimeout
|
||||
case object GetNbHubs
|
||||
case class AnalysisAvailable(gameId: String)
|
||||
case class Ack(uid: String)
|
||||
case class FinishGame(game: Game)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package lila
|
||||
|
||||
import lila.socket.WithSocket
|
||||
import lila.game.Event
|
||||
|
||||
package object round extends PackageObject with WithPlay with WithSocket {
|
||||
|
||||
|
|
|
@ -15,9 +15,9 @@ private[site] final class SocketHandler(socket: ActorRef) {
|
|||
uid: String,
|
||||
userId: Option[String],
|
||||
flag: Option[String]): Fu[JsSocketHandler] =
|
||||
Handler(socket, uid, Join(uid, userId, flag)) {
|
||||
Handler(socket, uid, Join(uid, userId, flag))(_ ⇒ {
|
||||
case ("liveGames", o) ⇒ o str "d" foreach { ids ⇒
|
||||
socket ! LiveGames(uid, ids.split(' ').toList)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,26 +15,27 @@ object Handler {
|
|||
|
||||
type Controller = PartialFunction[(String, JsObject), Unit]
|
||||
|
||||
def apply(
|
||||
def apply[M <: SocketMember](
|
||||
socket: ActorRef,
|
||||
uid: String,
|
||||
join: Any)(controller: Controller): Fu[JsSocketHandler] = {
|
||||
join: Any)(controller: M ⇒ Controller): Fu[JsSocketHandler] = {
|
||||
|
||||
val baseController: Controller = {
|
||||
case ("p", _) ⇒ socket ! Ping(uid)
|
||||
case _ ⇒
|
||||
}
|
||||
|
||||
val iteratee =
|
||||
def iteratee(member: M) =
|
||||
Iteratee.foreach[JsValue] { jsv ⇒
|
||||
jsv.asOpt[JsObject] foreach { obj ⇒
|
||||
obj str "t" foreach { t ⇒
|
||||
~(controller orElse baseController).lift(t -> obj)
|
||||
~(controller(member) orElse baseController).lift(t -> obj)
|
||||
}
|
||||
}
|
||||
} mapDone { _ ⇒ socket ! Quit(uid) }
|
||||
|
||||
(socket ? join map {
|
||||
case Connected(enumerator, member) ⇒ iteratee -> enumerator
|
||||
case Connected(enumerator, member: M) ⇒ iteratee(member) -> enumerator
|
||||
}) recover {
|
||||
case t: Throwable ⇒ errorHandler(t.getMessage)
|
||||
}
|
||||
|
|
|
@ -11,4 +11,9 @@ trait WithSocket extends Zeros {
|
|||
type JsChannel = Channel[JsValue]
|
||||
type JsEnumerator = Enumerator[JsValue]
|
||||
type JsSocketHandler = (Iteratee[JsValue, _], JsEnumerator)
|
||||
|
||||
implicit val LilaJsSocketHandlerZero = new Zero[JsSocketHandler] {
|
||||
|
||||
val zero: JsSocketHandler = Handler errorHandler "default error handler used"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,9 @@ package actorApi
|
|||
|
||||
import play.api.libs.json.JsObject
|
||||
|
||||
case class Connected[A <: SocketMember](
|
||||
case class Connected[M <: SocketMember](
|
||||
enumerator: JsEnumerator,
|
||||
member: A)
|
||||
member: M)
|
||||
case object Close
|
||||
case object GetUserIds
|
||||
case object GetNbMembers
|
||||
|
@ -20,3 +20,7 @@ case class SendTos(userIds: Set[String], message: JsObject)
|
|||
case class Fen(gameId: String, fen: String, lastMove: Option[String])
|
||||
case class LiveGames(uid: String, gameIds: List[String])
|
||||
case class Resync(uid: String)
|
||||
|
||||
case class GetSocket(id: String)
|
||||
case object GetNbSockets
|
||||
case object SocketTimeout
|
||||
|
|
Loading…
Reference in New Issue