handle rematch negociation through websockets

pull/83/head
Thibault Duplessis 2013-05-18 10:51:26 -03:00
parent 10c6109377
commit 27e24b7da6
15 changed files with 95 additions and 105 deletions

View File

@ -102,19 +102,6 @@ object Round extends LilaController with TheftPrevention with RoundEventPerforme
def drawCancel(fullId: String) = performAndRedirect(fullId, DrawCancel(_))
def drawDecline(fullId: String) = performAndRedirect(fullId, DrawDecline(_))
def rematch(fullId: String) = Open { implicit ctx
Env.setup.rematcher offerOrAccept PlayerRef(fullId) fold (
_ Redirect(routes.Round.player(fullId)), {
case (nextFullId, events) {
sendEvents(fullId)(events)
Redirect(routes.Round.player(nextFullId))
}
}
)
}
def rematchCancel(fullId: String) = performAndRedirect(fullId, RematchCancel(_))
def rematchDecline(fullId: String) = performAndRedirect(fullId, RematchDecline(_))
def takebackAccept(fullId: String) = performAndRedirect(fullId, TakebackAccept(_))
def takebackOffer(fullId: String) = performAndRedirect(fullId, TakebackOffer(_))
def takebackCancel(fullId: String) = performAndRedirect(fullId, TakebackCancel(_))

View File

@ -3,29 +3,27 @@ package controllers
import lila.app._
import lila.game.Event
import lila.socket.actorApi.Forward
import lila.hub.actorApi.map.Ask
import lila.hub.actorApi.map.Tell
import lila.game.Game.{ takeGameId, takePlayerId }
import makeTimeout.large
import akka.pattern.ask
import play.api.mvc._, Results._
trait RoundEventPerformer {
protected def performAndRedirect(fullId: String, makeMessage: String Any) =
Action {
Async {
perform(fullId, makeMessage) inject Redirect(routes.Round.player(fullId))
}
}
protected def performAndRedirect(fullId: String, makeMessage: String Any) = Action {
perform(fullId, makeMessage)
Redirect(routes.Round.player(fullId))
}
protected def perform(fullId: String, makeMessage: String Any): Fu[List[Event]] =
Env.round.roundMap ?
Ask(takeGameId(fullId), makeMessage(takePlayerId(fullId))) mapTo
manifest[List[Event]] logFailure
"[round] fail to perform on game %s".format(fullId)
protected def perform(fullId: String, makeMessage: String Any) {
Env.round.roundMap ! Tell(
takeGameId(fullId),
makeMessage(takePlayerId(fullId))
)
}
protected def sendEvents(gameId: String)(events: List[Event]) {
Env.round.socketHub ! Forward(gameId, events)
Env.round.roundMap ! Tell(gameId, events)
}
}

View File

@ -64,7 +64,7 @@ object Setup extends LilaController with TheftPrevention with RoundEventPerforme
Redirect(routes.Round.watcher(id, game.creatorColor.name)),
_ map {
case (p, events) {
sendEvents(p.gameId)(events)
Env.round.roundMap ! lila.hub.actorApi.map.Tell(p.gameId, events)
Redirect(routes.Round.player(p.fullId))
}
})

View File

@ -16,18 +16,18 @@
@if(opponent.isOfferingRematch) {
<div class="lichess_play_again_join rematch_alert">
@trans.yourOpponentWantsToPlayANewGameWithYou().&nbsp;
<a class="lichess_play_again lichess_rematch" title="@trans.playWithTheSameOpponentAgain()" href="@routes.Round.rematch(fullId)">@trans.joinTheGame()</a><br />
<a class="lichess_rematch_decline" href="@routes.Round.rematchDecline(fullId)">@trans.declineInvitation()</a>
<a class="lichess_play_again lichess_rematch socket-link" title="@trans.playWithTheSameOpponentAgain()" data-msg="rematch-yes">@trans.joinTheGame()</a><br />
<a class="lichess_rematch_decline socket-link" data-msg="rematch-no">@trans.declineInvitation()</a>
</div>
} else {
@if(player.isOfferingRematch) {
<div class="lichess_play_again_join rematch_wait">
@trans.rematchOfferSent().<br />
@trans.waitingForOpponent()...<br /><br />
<a class="lichess_rematch_cancel" href="@routes.Round.rematchCancel(fullId)">@trans.cancelRematchOffer()</a>
<a class="lichess_rematch_cancel socket-link" data-msg="rematch-no">@trans.cancelRematchOffer()</a>
</div>
} else {
<a class="lichess_rematch button" title="@trans.playWithTheSameOpponentAgain()" href="@routes.Round.rematch(fullId)">@trans.rematch()</a>
<a class="lichess_rematch button socket-link" title="@trans.playWithTheSameOpponentAgain()" data-msg="rematch-yes">@trans.rematch()</a>
}
}
} else {

View File

@ -51,9 +51,6 @@ GET /$fullId<[\w\-]{12}>/draw/accept controllers.Round.drawAccep
GET /$fullId<[\w\-]{12}>/draw/offer controllers.Round.drawOffer(fullId: String)
GET /$fullId<[\w\-]{12}>/draw/cancel controllers.Round.drawCancel(fullId: String)
GET /$fullId<[\w\-]{12}>/draw/decline controllers.Round.drawDecline(fullId: String)
GET /$fullId<[\w\-]{12}>/rematch controllers.Round.rematch(fullId: String)
GET /$fullId<[\w\-]{12}>/rematch/cancel controllers.Round.rematchCancel(fullId: String)
GET /$fullId<[\w\-]{12}>/rematch/decline controllers.Round.rematchDecline(fullId: String)
GET /$fullId<[\w\-]{12}>/takeback/accept controllers.Round.takebackAccept(fullId: String)
GET /$fullId<[\w\-]{12}>/takeback/offer controllers.Round.takebackOffer(fullId: String)
GET /$fullId<[\w\-]{12}>/takeback/cancel controllers.Round.takebackCancel(fullId: String)

View File

@ -148,7 +148,7 @@ trait WithPlay extends Zeros { self: PackageObject ⇒
case e throw e
})
def addEffect(effect: A => Unit) = fua ~ (_ foreach effect)
def addEffect(effect: A Unit) = fua ~ (_ foreach effect)
def thenPp: Fu[A] = fua ~ {
_.effectFold(

View File

@ -140,6 +140,11 @@ object Event {
override def only = Some(color)
}
case object ReloadTables extends Event {
def typ = "reload_table"
def data = JsNull
}
case class Premove(color: Color) extends Empty {
def typ = "premove"
override def only = Some(color)

View File

@ -22,7 +22,7 @@ final class ActorMap[A <: Actor](mkActor: String ⇒ A) extends Actor {
case Count sender ! actors.size
case Ask(id, msg) get(id) flatMap { _ ? msg } pipeTo sender
// case Ask(id, msg) get(id.pp) flatMap { _ ? msg.pp } pipeTo sender
case Tell(id, msg, _) get(id) foreach { _ forward msg }

View File

@ -46,6 +46,7 @@ final class Env(
takeback = takeback,
ai = ai,
finisher = finisher,
rematcher = rematcher,
notifyMove = notifyMove,
socketHub = socketHub,
moretimeDuration = Moretime)
@ -73,6 +74,11 @@ final class Env(
indexer = hub.actor.gameIndexer,
tournamentOrganizer = hub.actor.tournamentOrganizer)
private lazy val rematcher = new Rematcher(
messenger = messenger,
router = hub.actor.router,
timeline = hub.actor.timeline)
lazy val meddler = new Meddler(
roundMap = roundMap,
socketHub = socketHub)

View File

@ -1,10 +1,9 @@
package lila.setup
package lila.round
import chess.{ Game ChessGame, Board, Clock, Variant, Color ChessColor }
import ChessColor.{ White, Black }
import chess.format.Forsyth
import lila.game.{ GameRepo, Game, Event, Progress, Pov, PlayerRef, Namer, Source }
import lila.round.Messenger
import lila.user.User
import lila.hub.actorApi.router.Player
import makeTimeout.short
@ -13,31 +12,44 @@ import lila.game.tube.gameTube
import lila.user.tube.userTube
import lila.db.api._
import play.api.libs.json.{ Json, JsObject }
import akka.pattern.ask
private[setup] final class Rematcher(
private[round] final class Rematcher(
messenger: Messenger,
router: lila.hub.ActorLazyRef,
timeline: lila.hub.ActorLazyRef) {
private type Result = (String, List[Event])
def yes(pov: Pov): Fu[Events] = pov match {
case Pov(game, color) (game playerCanRematch color) ??
game.opponent(color).isOfferingRematch.fold(
game.next.fold(rematchJoin(pov))(rematchExists(pov)),
rematchCreate(pov)
)
}
def offerOrAccept(playerRef: PlayerRef): Fu[Result] =
GameRepo pov playerRef flatten "No such game" flatMap {
case pov @ Pov(game, color) (game playerCanRematch color) ??
game.opponent(color).isOfferingRematch.fold(
game.next.fold(rematchJoin(pov))(rematchExists(pov)),
rematchCreate(pov)
)
def no(pov: Pov): Fu[Events] = pov match {
case Pov(g1, color) if (pov.player.isOfferingRematch) for {
p1 messenger.systemMessage(g1, _.rematchOfferCanceled) map { es
Progress(g1, Event.ReloadTables :: es)
}
p2 = p1 map { g g.updatePlayer(color, _.removeRematchOffer) }
_ GameRepo save p2
} yield p2.events
case Pov(g1, color) if (g1.player(!color).isOfferingRematch) for {
p1 messenger.systemMessage(g1, _.rematchOfferDeclined) map { es
Progress(g1, Event.ReloadTables :: es)
}
p2 = p1 map { g g.updatePlayer(!color, _.removeRematchOffer) }
_ GameRepo save p2
} yield p2.events
}
private def rematchExists(pov: Pov)(nextId: String): Fu[Events] =
GameRepo game nextId flatMap {
_.fold(rematchJoin(pov))(redirectEvents)
}
private def rematchExists(pov: Pov)(nextId: String): Fu[Result] =
GameRepo.pov(nextId, !pov.color) map {
_.fold(pov.fullId -> Nil)(_.fullId -> Nil)
}
private def rematchJoin(pov: Pov): Fu[Result] = for {
private def rematchJoin(pov: Pov): Fu[Events] = for {
nextGame returnGame(pov) map (_.start)
nextId = nextGame.id
_ (GameRepo insertDenormalized nextGame) >>
@ -46,25 +58,16 @@ private[setup] final class Rematcher(
// messenges are not sent to the next game socket
// as nobody is there to see them yet
messenger.rematch(pov.game, nextGame)
result router ? Player(nextGame fullIdOf White) zip
router ? Player(nextGame fullIdOf Black) collect {
case (whiteUrl: String, blackUrl: String)
(nextGame fullIdOf !pov.color) -> List(
Event.RedirectOwner(White, blackUrl),
Event.RedirectOwner(Black, whiteUrl),
// tell spectators to reload the table
Event.ReloadTable(White),
Event.ReloadTable(Black))
}
} yield result
events redirectEvents(nextGame)
} yield events
private def rematchCreate(pov: Pov): Fu[Result] = for {
private def rematchCreate(pov: Pov): Fu[Events] = for {
p1 messenger.systemMessage(pov.game, _.rematchOfferSent) map { es
Progress(pov.game, Event.ReloadTable(!pov.color) :: es)
Progress(pov.game, Event.ReloadTables :: es)
}
p2 = p1 map { g g.updatePlayer(pov.color, _ offerRematch) }
_ GameRepo save p2
} yield pov.fullId -> p2.events
} yield p2.events
private def returnGame(pov: Pov): Fu[Game] = for {
pieces pov.game.variant.standard.fold(
@ -99,4 +102,15 @@ private[setup] final class Rematcher(
$find.byId[User](userId) map { _.fold(player)(player.withUser) }
}
}
private def redirectEvents(nextGame: Game): Fu[Events] =
router ? Player(nextGame fullIdOf White) zip
router ? Player(nextGame fullIdOf Black) collect {
case (whiteUrl: String, blackUrl: String) List(
Event.RedirectOwner(White, blackUrl),
Event.RedirectOwner(Black, whiteUrl),
// tell spectators to reload the table
Event.ReloadTable(White),
Event.ReloadTable(Black))
}
}

View File

@ -20,19 +20,21 @@ private[round] final class Round(
takeback: Takeback,
ai: Ai,
finisher: Finisher,
rematcher: Rematcher,
notifyMove: (String, String, Option[String]) Unit,
socketHub: ActorRef,
moretimeDuration: Duration) extends Handler(gameId) with Actor {
// TODO 30 seconds sounds good
context setReceiveTimeout 5.seconds
context setReceiveTimeout 30.seconds
def receive = {
case ReceiveTimeout self ! PoisonPill
// useful to get the game after all messages have been processed
case GetGame GameRepo game gameId pipeTo sender
// guaranty that all previous blocking events were performed
case Await sender ! ()
case Send(events) sendEvents(events)
case Play(playerId, origS, destS, promS, blur, lag) blocking[PlayResult](playerId) {
case Pov(g1, color) PgnRepo get g1.id flatMap { pgnString
@ -161,25 +163,8 @@ private[round] final class Round(
} yield p2.events)
}
case RematchCancel(playerRef) sender ! blocking(playerRef) {
case pov @ Pov(g1, color) (pov.player.isOfferingRematch) ?? (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)
}
case RematchDecline(playerRef) sender ! blocking(playerRef) {
case pov @ Pov(g1, color) (g1.player(!color).isOfferingRematch) ?? (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)
}
case RematchYes(playerRef) blocking(playerRef)(rematcher.yes) ~ sendEvents
case RematchNo(playerRef) blocking(playerRef)(rematcher.no) ~ sendEvents
case TakebackAccept(playerRef) sender ! blocking(playerRef) { pov
(pov.opponent.isProposingTakeback && pov.game.nonTournament) ?? (for {

View File

@ -33,8 +33,6 @@ private[round] final class SocketHandler(
case ("p", o) o int "v" foreach { v socket ! PingVersion(uid, v) }
case ("talk", o) for {
txt o str "d"
// TODO troll
// if member.canChat
if flood.allowMessage(uid, txt)
} messenger.watcherMessage(
ref.gameId,
@ -45,10 +43,10 @@ private[round] final class SocketHandler(
case ("p", o) o int "v" foreach { v socket ! PingVersion(uid, v) }
case ("talk", o) for {
txt o str "d"
// TODO troll
// if member.canChat
if flood.allowMessage(uid, txt)
} messenger.playerMessage(ref, txt) pipeTo socket
case ("rematch-yes", o) roundMap ! Tell(gameId, RematchYes(playerId))
case ("rematch-no", o) roundMap ! Tell(gameId, RematchNo(playerId))
case ("move", o) parseMove(o) foreach {
case (orig, dest, prom, blur, lag) {
socket ! Ack(uid)

View File

@ -76,7 +76,8 @@ package round {
case class PlayResult(events: Events, fen: String, lastMove: Option[String])
case object GetGame
case object Await
case class Send(events: Events)
case class Abort(playerId: String)
case object AbortForce
case class Resign(playerId: String)
@ -88,8 +89,8 @@ package round {
case class DrawCancel(playerId: String)
case class DrawDecline(playerId: String)
case object DrawForce
case class RematchCancel(playerId: String)
case class RematchDecline(playerId: String)
case class RematchYes(playerId: String)
case class RematchNo(playerId: String)
case class TakebackAccept(playerId: String)
case class TakebackOffer(playerId: String)
case class TakebackCancel(playerId: String)

View File

@ -31,11 +31,6 @@ final class Env(
router = hub.actor.router,
ai = ai)
lazy val rematcher = new Rematcher(
messenger = messenger,
router = hub.actor.router,
timeline = hub.actor.timeline)
lazy val friendJoiner = new FriendJoiner(
messenger = messenger,
router = hub.actor.router,

View File

@ -89,7 +89,7 @@ var lichess_translations = [];
t: t,
d: data
});
self.debug(message);
self.debug("send " + message);
self.ws.send(message);
},
scheduleConnect: function(delay) {
@ -586,6 +586,10 @@ var lichess_translations = [];
$('body').data('tournament-id', self.options.tournament_id);
}
self.$tableInner.on('click', 'a.socket-link', function() {
lichess.socket.send($(this).data('msg'));
});
if (self.options.game.started) {
self.indicateTurn();
self.initSquaresAndPieces();