Implement rematches (and refactor stuff)

This commit is contained in:
Thibault Duplessis 2012-05-19 02:49:04 +02:00
parent 4dbb6ca5fd
commit 8777401a55
20 changed files with 235 additions and 78 deletions

View file

@ -17,6 +17,7 @@ object Round extends LilaController {
private val gameRepo = env.game.gameRepo
private val socket = env.round.socket
private val hand = env.round.hand
private val rematcher = env.setup.rematcher
def websocketWatcher(gameId: String, color: String) = WebSocket.async[JsValue] { req
implicit val ctx = reqToCtx(req)
@ -54,8 +55,19 @@ object Round extends LilaController {
def drawOffer(fullId: String) = performAndRedirect(fullId, hand.drawOffer)
def drawCancel(fullId: String) = performAndRedirect(fullId, hand.drawCancel)
def drawDecline(fullId: String) = performAndRedirect(fullId, hand.drawDecline)
def rematchOffer(fullId: String) = TODO
def rematchAccept(fullId: String) = TODO
def rematch(fullId: String) = Action {
rematcher offerOrAccept fullId flatMap { validResult
validResult.fold(
err putFailures(err) map { _
Redirect(routes.Round.player(fullId))
}, {
case (nextFullId, events) performEvents(fullId)(events) map { _
Redirect(routes.Round.player(nextFullId))
}
}
)
} unsafePerformIO
}
def rematchCancel(fullId: String) = TODO
def rematchDecline(fullId: String) = TODO
def takebackAccept(fullId: String) = performAndRedirect(fullId, hand.takebackAccept)
@ -70,7 +82,7 @@ object Round extends LilaController {
def tablePlayer(fullId: String) = Open { implicit ctx
IOption(gameRepo pov fullId) { pov
pov.game.playable.fold(
html.round.table.playing(pov),
html.round.table.playing(pov),
html.round.table.end(pov))
}
}

View file

@ -37,7 +37,9 @@ final class CoreEnv private (application: Application, val settings: Settings) {
settings = settings,
mongodb = mongodb.apply _,
gameRepo = game.gameRepo,
userRepo = user.userRepo,
timelinePush = timeline.push.apply,
roundMessenger = round.messenger,
ai = ai.ai,
dbRef = user.userRepo.dbRef)
@ -81,15 +83,6 @@ final class CoreEnv private (application: Application, val settings: Settings) {
messageRepo = lobby.messageRepo,
entryRepo = timeline.entryRepo)
//lazy val appApi = new AppApi(
//userRepo = user.userRepo,
//gameRepo = game.gameRepo,
//roundSocket = round.socket,
//messenger = round.messenger,
//starter = lobby.starter,
//eloUpdater = user.eloUpdater,
//gameInfo = analyse.gameInfo)
lazy val mongodb = MongoConnection(
new MongoServer(MongoHost, MongoPort),
mongoOptions

View file

@ -55,10 +55,12 @@ case class DbGame(
def isPlayerFullId(player: DbPlayer, fullId: String): Boolean =
(fullId.size == DbGame.fullIdSize) && player.id == (fullId drop 8)
def opponent(p: DbPlayer): DbPlayer = player(!(p.color))
def player: DbPlayer = player(turnColor)
def opponent(p: DbPlayer): DbPlayer = opponent(p.color)
def opponent(c: Color): DbPlayer = player(!c)
def turnColor = Color(0 == turns % 2)
def turnOf(p: DbPlayer) = p == player
@ -203,7 +205,8 @@ case class DbGame(
def start = started.fold(this, copy(
status = Status.Started,
isRated = isRated && (players forall (_.hasUser))
isRated = isRated && (players forall (_.hasUser)),
updatedAt = DateTime.now.some
))
def recordMoveTimes = !hasAi
@ -231,16 +234,19 @@ case class DbGame(
started && playable &&
turns >= 2 &&
!player(color).isOfferingDraw &&
!(player(!color).isAi) &&
!(opponent(color).isAi) &&
!(playerHasOfferedDraw(color))
def playerHasOfferedDraw(color: Color) =
player(color).lastDrawOffer some (_ >= turns - 1) none false
player(color).lastDrawOffer.fold(_ >= turns - 1, false)
def playerCanRematch(color: Color) =
finishedOrAborted && opponent(color).isHuman
def playerCanProposeTakeback(color: Color) =
started && playable &&
turns >= 2 &&
!player(color).isProposingTakeback
opponent(color).isProposingTakeback
def abortable = status == Status.Started && turns < 2
@ -286,7 +292,9 @@ case class DbGame(
def creator = player(creatorColor)
def invited = player(!creatorColor)
def invitedColor = !creatorColor
def invited = opponent(invitedColor)
def pgnList = pgn.split(' ').toList
@ -345,8 +353,7 @@ object DbGame {
ai: Option[(Color, Int)],
creatorColor: Color,
isRated: Boolean,
variant: Variant,
createdAt: DateTime): DbGame = DbGame(
variant: Variant): DbGame = DbGame(
id = IdGenerator.game,
whitePlayer = whitePlayer withEncodedPieces game.allPieces,
blackPlayer = blackPlayer withEncodedPieces game.allPieces,
@ -362,7 +369,7 @@ object DbGame {
isRated = isRated,
variant = variant,
lastMoveTime = None,
createdAt = createdAt.some)
createdAt = DateTime.now.some)
}
case class RawDbGame(

View file

@ -67,6 +67,10 @@ case class DbPlayer(
def removeDrawOffer = copy(isOfferingDraw = false)
def offerRematch = copy(isOfferingRematch = true)
def removeRematchOffer = copy(isOfferingRematch = false)
def proposeTakeback = copy(isProposingTakeback = true)
def removeTakebackProposition = copy(isProposingTakeback = false)
@ -97,6 +101,10 @@ object DbPlayer {
id = IdGenerator.player,
color = color,
aiLevel = aiLevel)
def white = apply(Color.White, None)
def black = apply(Color.Black, None)
}
case class RawDbPlayer(

View file

@ -25,6 +25,7 @@ final class GameDiff(a: RawDbGame, b: RawDbGame) {
d(name + "w", _.players(i).w)
d(name + "lastDrawOffer", _.players(i).lastDrawOffer)
d(name + "isOfferingDraw", _.players(i).isOfferingDraw)
d(name + "isOfferingRematch", _.players(i).isOfferingRematch)
d(name + "isProposingTakeback", _.players(i).isProposingTakeback)
d(name + "blurs", _.players(i).blurs)
d(name + "mts", _.players(i).mts)

View file

@ -24,10 +24,9 @@ trait GameHelper { self: I18nHelper with UserHelper ⇒
def clockName(clock: Option[Clock])(implicit ctx: Context): String =
clock.fold(clockName, trans.unlimited.str())
def clockName(clock: Clock): String = "%d minutes/side + %d seconds/move".format(
clock.limitInMinutes, clock.increment)
def clockName(clock: Clock): String = Namer clock clock
def usernameWithElo(player: DbPlayer) = PlayerNamer(player)(userIdToUsername)
def usernameWithElo(player: DbPlayer) = Namer.player(player)(userIdToUsername)
def playerLink(player: DbPlayer, cssClass: Option[String] = None) = Html {
player.userId.fold(

31
app/game/Handler.scala Normal file
View file

@ -0,0 +1,31 @@
package lila
package game
import scalaz.effects._
abstract class Handler(gameRepo: GameRepo) {
protected def attempt[A](
fullId: String,
action: Pov Valid[IO[A]]): IO[Valid[A]] =
fromPov(fullId) { pov action(pov).sequence }
protected def attemptRef[A](
ref: PovRef,
action: Pov Valid[IO[A]]): IO[Valid[A]] =
fromPov(ref) { pov action(pov).sequence }
protected def fromPov[A](ref: PovRef)(op: Pov IO[Valid[A]]): IO[Valid[A]] =
fromPov(gameRepo pov ref)(op)
protected def fromPov[A](fullId: String)(op: Pov IO[Valid[A]]): IO[Valid[A]] =
fromPov(gameRepo pov fullId)(op)
protected def fromPov[A](povIO: IO[Option[Pov]])(op: Pov IO[Valid[A]]): IO[Valid[A]] =
povIO flatMap { povOption
povOption.fold(
pov op(pov),
io { "No such game".failNel }
)
}
}

View file

@ -1,15 +1,20 @@
package lila
package game
object PlayerNamer {
import chess.Clock
object Namer {
val anonPlayerName = "Anonymous"
def apply(player: DbPlayer)(getUsername: String String) =
def player(player: DbPlayer)(getUsername: String String) =
player.aiLevel.fold(
level "A.I. level " + level,
(player.userId map getUsername).fold(
username "%s (%s)".format(username, player.elo getOrElse "?"),
anonPlayerName)
)
def clock(clock: Clock): String = "%d minutes/side + %d seconds/move".format(
clock.limitInMinutes, clock.increment)
}

View file

@ -2,7 +2,7 @@ package lila
package round
import ai.Ai
import game.{ GameRepo, Pov, PovRef }
import game.{ GameRepo, Pov, PovRef, Handler }
import chess.Role
import chess.Pos.posAt
@ -20,7 +20,7 @@ final class Hand(
ai: () Ai,
finisher: Finisher,
hubMaster: ActorRef,
moretimeSeconds: Int) {
moretimeSeconds: Int) extends Handler(gameRepo) {
type IOValidEvents = IO[Valid[List[Event]]]
@ -34,7 +34,7 @@ final class Hand(
g2 (g1.playable).fold(success(g1), failure("Game not playable" wrapNel))
orig posAt(origString) toValid "Wrong orig " + origString
dest posAt(destString) toValid "Wrong dest " + destString
promotion = Role promotable promString
promotion = Role promotable promString
newChessGameAndMove g2.toChess(orig, dest, promotion)
(newChessGame, move) = newChessGameAndMove
} yield g2.update(newChessGame, move, blur)).fold(
@ -127,6 +127,29 @@ final class Hand(
else !!("no draw offer to decline " + fullId)
})
def rematch(fullId: String): IO[Valid[(String, List[Event])]] =
attempt(fullId, {
case pov @ Pov(game, color) if game playerCanRematch color
if (game.opponent(color).isOfferingRematch) success {
game.nextId.fold(
nextId io(nextId -> Nil),
io(fullId -> Nil) // accept
)
}
//$nextOpponent = $this->gameGenerator->createReturnGame($opponent);
//$nextPlayer = $nextOpponent->getOpponent();
//$nextGame = $nextOpponent->getGame();
//$messages = $this->starter->start($nextGame);
//$this->objectManager->persist($nextGame);
else success {
val progress = Progress(game, Event.ReloadTable(!color)) map { g
g.updatePlayer(color, _.offerRematch)
}
gameRepo save progress map { _ fullId -> progress.events }
}
case _ !!("invalid rematch offer " + fullId)
})
def takebackAccept(fullId: String): IOValidEvents = fromPov(fullId) { pov
if (pov.opponent.isProposingTakeback) for {
fen gameRepo initialFen pov.game.id
@ -201,28 +224,4 @@ final class Hand(
} yield progress2.events
} toValid "cannot add moretime"
)
private def attempt[A](
fullId: String,
action: Pov Valid[IO[A]]): IO[Valid[A]] =
fromPov(fullId) { pov action(pov).sequence }
private def attemptRef[A](
ref: PovRef,
action: Pov Valid[IO[A]]): IO[Valid[A]] =
fromPov(ref) { pov action(pov).sequence }
private def fromPov[A](ref: PovRef)(op: Pov IO[Valid[A]]): IO[Valid[A]] =
fromPov(gameRepo pov ref)(op)
private def fromPov[A](fullId: String)(op: Pov IO[Valid[A]]): IO[Valid[A]] =
fromPov(gameRepo pov fullId)(op)
private def fromPov[A](povIO: IO[Option[Pov]])(op: Pov IO[Valid[A]]): IO[Valid[A]] =
povIO flatMap { povOption
povOption.fold(
pov op(pov),
io { "No such game".failNel }
)
}
}

View file

@ -19,13 +19,15 @@ final class Messenger(roomRepo: RoomRepo) {
else io(Nil)
def systemMessages(game: DbGame, encodedMessages: String): IO[List[Event]] =
if (game.invited.isHuman) {
val messages = (encodedMessages split '$').toList
systemMessages(game, (encodedMessages split '$').toList)
def systemMessages(game: DbGame, messages: List[String]): IO[List[Event]] =
game.invited.isHuman.fold(
roomRepo.addSystemMessages(game.id, messages) map { _
messages map { Message("system", _) }
}
}
else io(Nil)
},
io(Nil)
)
def systemMessage(game: DbGame, message: String): IO[List[Event]] =
if (game.invited.isHuman)

View file

@ -26,4 +26,7 @@ object Progress {
def apply(game: DbGame, events: List[Event]): Progress =
new Progress(game, game, events)
def apply(game: DbGame, events: Event): Progress =
new Progress(game, game, events :: Nil)
}

View file

@ -1,14 +1,13 @@
package lila
package round
import scalaz.effects._
import com.mongodb.casbah.MongoCollection
import akka.actor._
import akka.actor.Props
import play.api.libs.concurrent._
import play.api.Application
import game.{ GameRepo }
import game.{ GameRepo, DbGame }
import user.{ UserRepo, EloUpdater }
import ai.Ai
import core.Settings
@ -20,7 +19,7 @@ final class RoundEnv(
gameRepo: GameRepo,
userRepo: UserRepo,
eloUpdater: EloUpdater,
ai: () => Ai) {
ai: () Ai) {
implicit val ctx = app
import settings._
@ -63,7 +62,7 @@ final class RoundEnv(
lazy val finisherLock = new FinisherLock(timeout = FinisherLockTimeout)
lazy val takeback = new Takeback(
gameRepo = gameRepo,
gameRepo = gameRepo,
messenger = messenger)
lazy val messenger = new Messenger(roomRepo = roomRepo)

View file

@ -5,8 +5,6 @@ import chess.{ Game, Board, Variant, Color ⇒ ChessColor }
import elo.EloRange
import game.{ DbGame, DbPlayer }
import org.joda.time.DateTime
case class AiConfig(
variant: Variant,
level: Int,
@ -25,8 +23,7 @@ case class AiConfig(
aiLevel = creatorColor.white option level),
creatorColor = creatorColor,
isRated = false,
variant = variant,
createdAt = DateTime.now).start
variant = variant).start
def encode = RawAiConfig(
v = variant.id,

93
app/setup/Rematcher.scala Normal file
View file

@ -0,0 +1,93 @@
package lila
package setup
import chess.{ Game, Board, Clock, Color ChessColor }
import ChessColor.{ White, Black }
import chess.format.Forsyth
import game.{ GameRepo, DbGame, DbPlayer, Pov, Handler, Namer }
import round.{ Event, Progress, Messenger }
import user.UserRepo
import controllers.routes
import scalaz.effects._
final class Rematcher(
gameRepo: GameRepo,
userRepo: UserRepo,
messenger: Messenger,
timelinePush: DbGame IO[Unit]) extends Handler(gameRepo) {
def offerOrAccept(fullId: String): IO[Valid[(String, List[Event])]] =
attempt(fullId, {
case pov @ Pov(game, color) if game playerCanRematch color
if (game.opponent(color).isOfferingRematch) success {
game.nextId.fold(
nextId io(nextId -> Nil),
for {
nextGame returnGame(pov) map (_.start)
_ gameRepo insert nextGame
nextId = nextGame.id
_ game.variant.standard.fold(io(), gameRepo saveInitialFen game)
_ timelinePush(game)
// messenges are not sent to the next game socket
// as nobody is there to see them yet
_ messenger.systemMessages(nextGame, List(
Some(nextGame.creatorColor + " creates the game"),
Some(nextGame.invitedColor + " joins the game"),
nextGame.clock map Namer.clock,
nextGame.isRated option "This game is rated").flatten)
} yield nextId -> List(
Event.RedirectOwner(White, playerUrl(nextGame, White)),
Event.RedirectOwner(Black, playerUrl(nextGame, Black)),
// tell spectators to reload the table
Event.ReloadTable(White),
Event.ReloadTable(Black))
)
}
else success {
val progress = Progress(game, Event.ReloadTable(!color)) map { g
g.updatePlayer(color, _.offerRematch)
}
gameRepo save progress map { _ fullId -> progress.events }
}
case _ !!("invalid rematch offer " + fullId)
})
private def returnGame(pov: Pov): IO[DbGame] = for {
board pov.game.variant.standard.fold(
io(pov.game.variant.pieces),
gameRepo initialFen pov.game.id map { fenOption
(fenOption flatMap Forsyth.<< map { situation
situation.board.pieces
}) | pov.game.variant.pieces
})
whitePlayer returnPlayer(pov.game, White)
blackPlayer returnPlayer(pov.game, Black)
} yield DbGame(
game = Game(
board = Board(board),
clock = pov.game.clock map (_.reset)),
whitePlayer = whitePlayer,
blackPlayer = blackPlayer,
ai = None,
creatorColor = !pov.color,
isRated = pov.game.isRated,
variant = pov.game.variant)
private def returnPlayer(game: DbGame, color: ChessColor): IO[DbPlayer] =
DbPlayer(color = color, aiLevel = None) |> { player
game.player(color).userId.fold(
userId userRepo user userId map { userOption
userOption.fold(
user player.withUser(user)(userRepo.dbRef),
player)
},
io(player))
}
private def clockName(clock: Option[Clock]): String =
clock.fold(Namer.clock, "Unlimited")
private def playerUrl(game: DbGame, color: ChessColor): String =
routes.Round.player(game fullIdOf color).url
}

View file

@ -3,8 +3,9 @@ package setup
import core.Settings
import game.{ DbGame, GameRepo }
import round.Messenger
import ai.Ai
import user.User
import user.{ User, UserRepo }
import com.mongodb.casbah.MongoCollection
import scalaz.effects._
@ -14,7 +15,9 @@ final class SetupEnv(
settings: Settings,
mongodb: String MongoCollection,
gameRepo: GameRepo,
userRepo: UserRepo,
timelinePush: DbGame IO[Unit],
roundMessenger: Messenger,
ai: () Ai,
dbRef: User DBRef) {
@ -31,4 +34,10 @@ final class SetupEnv(
timelinePush = timelinePush,
ai = ai,
dbRef = dbRef)
lazy val rematcher = new Rematcher(
gameRepo = gameRepo,
userRepo = userRepo,
messenger = roundMessenger,
timelinePush = timelinePush)
}

View file

@ -2,7 +2,7 @@ package lila
package timeline
import chess.Color
import game.{ DbGame, PlayerNamer }
import game.{ DbGame, Namer }
import scalaz.effects._
@ -32,5 +32,5 @@ final class Push(
(game player color).userId
private def usernameElo(game: DbGame, color: Color): String =
PlayerNamer(game player color)(getUsername)
Namer.player(game player color)(getUsername)
}

View file

@ -10,7 +10,7 @@ final class TimelineEnv(
settings: Settings,
mongodb: String MongoCollection,
lobbyNotify: Entry IO[Unit],
getUsername: String => String) {
getUsername: String String) {
import settings._

View file

@ -11,7 +11,7 @@
@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.rematchAccept(fullId)">@trans.joinTheGame()</a><br />
<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>
</div>
} else {
@ -22,7 +22,7 @@
<a class="lichess_rematch_cancel" href="@routes.Round.rematchCancel(fullId)">@trans.cancelRematchOffer()</a>
</div>
} else {
<a class="lichess_rematch button" title="@trans.playWithTheSameOpponentAgain()" href="@routes.Round.rematchOffer(fullId)">@trans.rematch()</a>
<a class="lichess_rematch button" title="@trans.playWithTheSameOpponentAgain()" href="@routes.Round.rematch(fullId)">@trans.rematch()</a>
}
}
} else {

View file

@ -18,8 +18,7 @@ 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/offer controllers.Round.rematchOffer(fullId: String)
GET /$fullId<[\w\-]{12}>/rematch/accept controllers.Round.rematchAccept(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)

View file

@ -12,7 +12,7 @@ trait Resolvers {
}
trait Dependencies {
val scalachess = "com.github.ornicar" %% "scalachess" % "1.1"
val scalachess = "com.github.ornicar" %% "scalachess" % "1.2"
val scalaz = "org.scalaz" %% "scalaz-core" % "6.0.4"
val specs2 = "org.specs2" %% "specs2" % "1.8.2"
val casbah = "com.mongodb.casbah" %% "casbah" % "2.1.5-1"