Implement game room

This commit is contained in:
Thibault Duplessis 2012-03-29 15:25:14 +02:00
parent bd5d611b95
commit 61588766a0
16 changed files with 148 additions and 57 deletions

View file

@ -14,9 +14,8 @@ object DataForm {
"b" -> optional(number)
))
type TalkData = (String, String)
val talkForm = Form(tuple(
"author" -> nonEmptyText,
type TalkData = String
val talkForm = Form(single(
"message" -> nonEmptyText
))

View file

@ -10,10 +10,6 @@ object AppApiC extends LilaController {
private val api = env.appApi
def talk(gameId: String) = Action { implicit request
FormValidIOk[TalkData](talkForm)(talk api.talk(gameId, talk._1, talk._2))
}
def updateVersion(gameId: String) = Action {
IOk(api updateVersion gameId)
}
@ -22,6 +18,10 @@ object AppApiC extends LilaController {
IOk(api reloadTable gameId)
}
def room(gameId: String) = Action {
Ok(api.room(gameId).unsafePerformIO)
}
def alive(gameId: String, color: String) = Action {
IOk(api.alive(gameId, color))
}
@ -30,10 +30,6 @@ object AppApiC extends LilaController {
FormValidIOk[String](drawForm)(msgs api.draw(gameId, color, msgs))
}
def drawAccept(gameId: String, color: String) = Action { implicit request
FormValidIOk[String](drawForm)(msgs api.drawAccept(gameId, color, msgs))
}
def start(gameId: String) = Action { implicit request =>
FormValidIOk[EntryData](entryForm)(entryData api.start(gameId, entryData))
}

View file

@ -45,6 +45,17 @@ object AppXhrC extends LilaController {
ValidIORedir(xhr claimDraw fullId, fullId)
}
def drawAccept(fullId: String) = Action { implicit request
ValidIORedir(xhr drawAccept fullId, fullId)
}
def talk(fullId: String) = Action { implicit request
talkForm.bindFromRequest.fold(
form BadRequest(form.errors mkString "\n"),
message ValidOk(xhr.talk(fullId, message).unsafePerformIO)
)
}
def ping() = Action { implicit request
JsonIOk(env.pinger.ping(
username = get("username"),

View file

@ -9,6 +9,7 @@ mongo {
entry = lobby_entry
message = lobby_message
history = user_history
room = game_room
}
}
redis {

View file

@ -13,20 +13,22 @@ GET /resign/:fullId controllers.AppXhrC.resign(fullId: Strin
GET /force-resign/:fullId controllers.AppXhrC.forceResign(fullId: String)
GET /claim-draw/:fullId controllers.AppXhrC.claimDraw(fullId: String)
POST /outoftime/:fullId controllers.AppXhrC.outoftime(fullId: String)
POST /draw-accept/:fullId controllers.AppXhrC.drawAccept(fullId: String)
POST /talk/:fullId controllers.AppXhrC.talk(fullId: String)
# App Private API
POST /api/update-version/:gameId controllers.AppApiC.updateVersion(gameId: String)
POST /api/start/:gameId controllers.AppApiC.start(gameId: String)
POST /api/talk/:fullId controllers.AppApiC.talk(fullId: String)
POST /api/join/:fullId controllers.AppApiC.join(fullId: String)
POST /api/reload-table/:gameId controllers.AppApiC.reloadTable(gameId: String)
POST /api/rematch-accept/:gameId/:color/:newGameId controllers.AppApiC.rematchAccept(gameId: String, color: String, newGameId: String)
POST /api/alive/:gameId/:color controllers.AppApiC.alive(gameId: String, color: String)
POST /api/draw/:gameId/:color controllers.AppApiC.draw(gameId: String, color: String)
POST /api/draw-accept/:gameId/:color controllers.AppApiC.drawAccept(gameId: String, color: String)
GET /api/activity/:gameId/:color controllers.AppApiC.activity(gameId: String, color: String)
GET /api/possible-moves/:gameId/:color controllers.AppApiC.possibleMoves(gameId: String, color: String)
GET /api/nb-players controllers.AppXhrC.nbPlayers
GET /api/room/:gameId controllers.AppApiC.room(gameId: String)
GET /api/nb-players controllers.AppXhrC.nbPlayers
POST /api/outoftime/:fullId controllers.AppXhrC.outoftime(fullId: String)
# Lobby XHR
GET /lobby/sync/:hookId controllers.LobbyXhrC.syncWithHook(hookId: String)

View file

@ -2,12 +2,13 @@ package lila.system
import model._
import memo._
import db.GameRepo
import db.{ GameRepo, RoomRepo }
import lila.chess.{ Color, White, Black }
import scalaz.effects._
case class AppApi(
gameRepo: GameRepo,
roomRepo: RoomRepo,
ai: Ai,
versionMemo: VersionMemo,
aliveMemo: AliveMemo,
@ -19,18 +20,12 @@ case class AppApi(
messages: String,
entryData: String): IO[Unit] = for {
pov gameRepo pov fullId
g2 = pov.game withEvents decodeMessages(messages)
g2 systemMessages(pov.game, messages)
g3 = g2.withEvents(!pov.color, List(RedirectEvent(url)))
_ save(pov.game, g3)
_ addEntry(g3, entryData)
} yield ()
def talk(gameId: String, author: String, message: String): IO[Unit] = for {
g1 gameRepo game gameId
g2 = g1 withEvents List(MessageEvent(author, message))
_ save(g1, g2)
} yield ()
def start(gameId: String, entryData: String): IO[Unit] = for {
game gameRepo game gameId
_ addEntry(game, entryData)
@ -77,21 +72,17 @@ case class AppApi(
def draw(gameId: String, colorName: String, messages: String): IO[Unit] = for {
color ioColor(colorName)
g1 gameRepo game gameId
g2 = g1 withEvents decodeMessages(messages)
g2 systemMessages(g1, messages)
g3 = g2.withEvents(!color, List(ReloadTableEvent()))
_ save(g1, g3)
} yield ()
def drawAccept(gameId: String, colorName: String, messages: String): IO[Unit] = for {
color ioColor(colorName)
g1 gameRepo game gameId
g2 = g1 withEvents (EndEvent() :: decodeMessages(messages))
_ save(g1, g2)
} yield ()
def activity(gameId: String, colorName: String): Int =
Color(colorName) some { aliveMemo.activity(gameId, _) } none 0
def room(gameId: String): IO[String] =
roomRepo room gameId map (_.render)
def possibleMoves(gameId: String, colorName: String): IO[Map[String, Any]] =
for {
color ioColor(colorName)
@ -100,6 +91,12 @@ case class AppApi(
case (from, dests) from.key -> (dests.mkString)
} toMap
private def decodeMessages(messages: String): List[MessageEvent] =
(messages split '$').toList map { MessageEvent("system", _) }
private def systemMessages(game: DbGame, messageString: String): IO[DbGame] =
if (game.invited.isHuman) {
val messages = (messageString split '$').toList
roomRepo.addSystemMessages(game.id, messages) map { _
game withEvents (messages map { msg MessageEvent("system", msg) })
}
}
else io(game)
}

View file

@ -7,7 +7,6 @@ import db.GameRepo
import scalaz.effects._
import scala.annotation.tailrec
import scala.math.max
import org.apache.commons.lang3.StringEscapeUtils.escapeXml
final class AppSyncer(
gameRepo: GameRepo,
@ -53,10 +52,9 @@ final class AppSyncer(
case _ true
} map (_.export)
// TODO author=system messages should be translated!!
private def renderMessage(author: String, message: String) = Map(
"type" -> "message",
"html" -> """<li class="%s">%s</li>""".format(author, escapeXml(message))
"html" -> Room.render(author, message)
)
private def versionWait(gameId: String, color: Color, version: Int) = io {

View file

@ -2,28 +2,21 @@ package lila.system
import model._
import memo._
import db.GameRepo
import db.{ GameRepo, RoomRepo }
import lila.chess._
import Pos.posAt
import scalaz.effects._
final class AppXhr(
gameRepo: GameRepo,
val gameRepo: GameRepo,
roomRepo: RoomRepo,
ai: Ai,
finisher: Finisher,
versionMemo: VersionMemo,
aliveMemo: AliveMemo) {
val versionMemo: VersionMemo,
aliveMemo: AliveMemo) extends IOTools {
type IOValid = IO[Valid[Unit]]
def playMove(
fullId: String,
moveString: String,
promString: Option[String] = None): IOValid = moveString match {
case MoveString(orig, dest) play(fullId, orig, dest, promString)
case _ io(failure("Wrong move" wrapNel))
}
def play(
fullId: String,
origString: String,
@ -61,6 +54,18 @@ final class AppXhr(
def outoftime(fullId: String): IOValid = attempt(fullId, finisher.outoftime)
def drawAccept(fullId: String): IOValid = attempt(fullId, finisher.drawAccept)
def talk(fullId: String, message: String): IOValid = attempt(fullId, pov
if (pov.game.invited.isHuman && message.size <= 140 && message.nonEmpty)
success(for {
_ roomRepo.addMessage(pov.game.id, pov.color.name, message)
g2 = pov.game withEvents List(MessageEvent(pov.color.name, message))
_ save(pov.game, g2)
} yield ())
else failure("Cannot talk" wrapNel)
)
private def attempt(fullId: String, action: Pov Valid[IO[Unit]]): IOValid =
fromPov(fullId) { pov action(pov).sequence }

View file

@ -1,6 +1,6 @@
package lila.system
import db.{ UserRepo, GameRepo, HistoryRepo }
import db._
import model._
import memo.{ VersionMemo, AliveMemo, FinisherLock }
import lila.chess.{ Color, White, Black, EloCalculator }
@ -11,6 +11,7 @@ final class Finisher(
historyRepo: HistoryRepo,
userRepo: UserRepo,
gameRepo: GameRepo,
roomRepo: RoomRepo,
versionMemo: VersionMemo,
aliveMemo: AliveMemo,
eloCalculator: EloCalculator,
@ -32,10 +33,15 @@ final class Finisher(
else !!("game is not force-resignable")
def claimDraw(pov: Pov): ValidIO = pov match {
case Pov(game, color) if game.playable && game.player.color == color && game.toChessHistory.threefoldRepetition finish(game, Draw, Some(pov.color))
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): ValidIO =
if (pov.opponent.isOfferingDraw)
finish(pov.game, Draw, None, Some("Draw offer accepted"))
else !!("opponent is not proposing a draw")
def outoftime(pov: Pov): ValidIO =
pov.game.outoftimePlayer some { player
finish(pov.game, Outoftime,
@ -52,9 +58,14 @@ final class Finisher(
if (finisherLock isLocked game) !!("game finish is locked")
else success(for {
_ finisherLock lock game
g2 = game.finish(status, winner, message)
_ gameRepo.applyDiff(game, g2)
_ versionMemo put g2
g2 = game.finish(status, winner)
g3 message filter (_ g2.invited.isHuman) some { msg
roomRepo.addSystemMessage(g2.id, msg) map { _
g2 withEvents List(MessageEvent("system", msg))
}
} none io(g2)
_ gameRepo.applyDiff(game, g3)
_ versionMemo put g3
_ updateElo(g2)
_ incNbGames(g2, White)
_ incNbGames(g2, Black)

View file

@ -12,6 +12,7 @@ final class SystemEnv(config: Config) {
lazy val appXhr = new AppXhr(
gameRepo = gameRepo,
roomRepo = roomRepo,
ai = ai,
finisher = finisher,
versionMemo = versionMemo,
@ -19,6 +20,7 @@ final class SystemEnv(config: Config) {
lazy val appApi = new AppApi(
gameRepo = gameRepo,
roomRepo = roomRepo,
ai = ai,
versionMemo = versionMemo,
aliveMemo = aliveMemo,
@ -69,6 +71,7 @@ final class SystemEnv(config: Config) {
historyRepo = historyRepo,
userRepo = userRepo,
gameRepo = gameRepo,
roomRepo = roomRepo,
versionMemo = versionMemo,
aliveMemo = aliveMemo,
eloCalculator = new EloCalculator,
@ -99,8 +102,10 @@ final class SystemEnv(config: Config) {
max = config getInt "lobby.message.max")
lazy val historyRepo = new HistoryRepo(
collection = mongodb(config getString "mongo.collection.history")
)
collection = mongodb(config getString "mongo.collection.history"))
lazy val roomRepo = new RoomRepo(
collection = mongodb(config getString "mongo.collection.room"))
lazy val mongodb = MongoConnection(
config getString "mongo.host",

View file

@ -0,0 +1,33 @@
package lila.system
package db
import model.Room
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
class RoomRepo(collection: MongoCollection)
extends SalatDAO[Room, String](collection) {
def room(id: String): IO[Room] = io {
findOneByID(id) | Room(id, Nil)
}
def addMessage(id: String, author: String, message: String): IO[Unit] = io {
collection.update(
DBObject("_id" -> id),
$push("messages" -> List(author, message)),
upsert = true,
multi = false
)
}
def addSystemMessage(id: String, message: String) =
addMessage(id, "system", message)
def addSystemMessages(id: String, messages: Seq[String]): IO[Unit] =
(messages map { addSystemMessage(id, _) }).sequence map (_ Unit)
}

View file

@ -167,13 +167,13 @@ case class DbGame(
def resignable = playable && !abortable
def finish(status: Status, winner: Option[Color], msg: Option[String]) = copy(
def finish(status: Status, winner: Option[Color]) = copy(
status = status,
winnerId = winner flatMap (player(_).userId),
whitePlayer = whitePlayer finish (winner == Some(White)),
blackPlayer = blackPlayer finish (winner == Some(Black)),
positionHashes = ""
) withEvents (EndEvent() :: msg.map(MessageEvent("system", _)).toList)
) withEvents List(EndEvent())
def rated = isRated
@ -187,6 +187,9 @@ case class DbGame(
if c outoftime player.color
} yield player
def creator = player(creatorColor)
def invited = player(!creatorColor)
}
object DbGame {

View file

@ -36,6 +36,8 @@ case class DbPlayer(
def isAi = aiLevel.isDefined
def isHuman = !isAi
def userId: Option[String] = user map (_.getId.toString)
def wins = isWinner getOrElse false

View file

@ -1,7 +1,7 @@
package lila.system
package model
import com.novus.salat.annotations._
import com.novus.salat.annotations.Key
case class Message(
@Key("_id") id: Int,

View file

@ -7,6 +7,8 @@ case class Pov(game: DbGame, color: Color) {
def player = game player color
def opponent = game player !color
def isPlayerFullId(fullId: Option[String]): Boolean =
fullId some { game.isPlayerFullId(player, _) } none false
}

View file

@ -0,0 +1,26 @@
package lila.system
package model
import com.novus.salat.annotations.Key
import com.mongodb.BasicDBList
import org.apache.commons.lang3.StringEscapeUtils.escapeXml
import collection.JavaConversions._
case class Room(
@Key("_id") id: String,
messages: List[BasicDBList]) {
def render: String = messages map (_.toList) map {
case author :: message :: Nil Room.render(author.toString, message.toString)
case _ ""
} mkString
}
object Room {
def render(author: String, message: String): String =
"""<li class="%s%s">%s</li>""".format(
author,
if (author == "system") " trans_me" else "",
escapeXml(message))
}