complete round module

pull/83/head
Thibault Duplessis 2013-04-08 16:21:03 -03:00
parent a8d9f75035
commit 8c4cb4a95d
42 changed files with 925 additions and 981 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
package lila.app
import ornicar.scalalib._
package object round {
type ValidIOEvents = Valid[scalaz.effects.IO[List[Event]]]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package lila
import lila.socket.WithSocket
import lila.game.Event
package object round extends PackageObject with WithPlay with WithSocket {

View File

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

View File

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

View File

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

View File

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