Implement game room
This commit is contained in:
parent
bd5d611b95
commit
61588766a0
|
@ -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
|
||||
))
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -9,6 +9,7 @@ mongo {
|
|||
entry = lobby_entry
|
||||
message = lobby_message
|
||||
history = user_history
|
||||
room = game_room
|
||||
}
|
||||
}
|
||||
redis {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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",
|
||||
|
|
33
system/src/main/scala/db/RoomRepo.scala
Normal file
33
system/src/main/scala/db/RoomRepo.scala
Normal 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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
26
system/src/main/scala/model/Room.scala
Normal file
26
system/src/main/scala/model/Room.scala
Normal 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))
|
||||
}
|
Loading…
Reference in a new issue