Start lobby implementation, fix tests, and more

pull/1/merge
Thibault Duplessis 2012-03-21 01:08:32 +01:00
parent 59c8081005
commit e1fb905119
14 changed files with 192 additions and 35 deletions

View File

@ -11,7 +11,7 @@ import scala.io.Codec
import com.codahale.jerkson.Json
import scalaz.effects.IO
trait LilaController extends Controller with ContentTypes {
trait LilaController extends Controller with ContentTypes with RequestGetter {
lazy val env = Global.env
@ -30,12 +30,6 @@ trait LilaController extends Controller with ContentTypes {
def IOk(op: IO[Unit]) = Ok(op.unsafePerformIO)
def get(name: String)(implicit request: Request[_]) =
request.queryString get name flatMap (_.headOption)
def getInt(name: String)(implicit request: Request[_]) =
get(name)(request) map (_.toInt)
// I like Unit requests.
implicit def wUnit: Writeable[Unit] =
Writeable[Unit](_ Codec toUTF8 "ok")

View File

@ -10,7 +10,15 @@ object LobbyXhrC extends LilaController {
private val xhr = env.lobbyXhr
def sync() = Action {
Ok("")
def syncWithHook(hookId: String) = sync(Some(hookId))
def syncWithoutHook() = sync(None)
private def sync(hookId: Option[String]) = Action { implicit request =>
JsonOk(xhr.sync(
getIntOr("auth", 0) == 1,
getOr("l", "en"),
getIntOr("state", 0)
).unsafePerformIO)
}
}

View File

@ -0,0 +1,18 @@
package controllers
import play.api.mvc.Request
trait RequestGetter {
def get(name: String)(implicit request: Request[_]) =
request.queryString get name flatMap (_.headOption)
def getInt(name: String)(implicit request: Request[_]) =
get(name)(request) map (_.toInt)
def getOr(name: String, default: String)(implicit request: Request[_]) =
get(name) getOrElse default
def getIntOr(name: String, default: Int)(implicit request: Request[_]) =
getInt(name) getOrElse default
}

View File

@ -5,6 +5,7 @@ mongo {
collection {
game = game2
user = user
hook = hook
}
}
redis {
@ -15,6 +16,12 @@ sync {
duration = 7 seconds
sleep = 200 milliseconds
}
lobby {
poll {
duration = 10 seconds
sleep = 300 milliseconds
}
}
memo {
version.timeout = 30 minutes
alive.hard_timeout = 100 seconds

View File

@ -10,23 +10,24 @@ GET /ping controllers.AppXhrC.ping()
GET /how-many-players-now controllers.AppXhrC.nbPlayers()
# App Private API
POST /internal/update-version/:gameId controllers.AppApiC.updateVersion(gameId: String)
POST /internal/end/:gameId controllers.AppApiC.end(gameId: String)
POST /internal/talk/:fullId controllers.AppApiC.talk(fullId: String)
POST /internal/join/:fullId controllers.AppApiC.join(fullId: String)
POST /internal/reload-table/:gameId controllers.AppApiC.reloadTable(gameId: String)
POST /internal/accept-rematch/:gameId/:color/:newGameId controllers.AppApiC.acceptRematch(gameId: String, color: String, newGameId: String)
POST /internal/alive/:gameId/:color controllers.AppApiC.alive(gameId: String, color: String)
POST /internal/draw/:gameId/:color controllers.AppApiC.draw(gameId: String, color: String)
POST /internal/draw-accept/:gameId/:color controllers.AppApiC.drawAccept(gameId: String, color: String)
GET /internal/activity/:gameId/:color controllers.AppApiC.activity(gameId: String, color: String)
GET /internal/nb-players controllers.AppXhrC.nbPlayers()
POST /api/update-version/:gameId controllers.AppApiC.updateVersion(gameId: String)
POST /api/end/:gameId controllers.AppApiC.end(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/accept-rematch/:gameId/:color/:newGameId controllers.AppApiC.acceptRematch(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/nb-players controllers.AppXhrC.nbPlayers()
# Lobby XHR
GET /lobby/sync controllers.LobbyXhrC.sync()
GET /lobby/sync/:hookId controllers.LobbyXhrC.syncWithHook(hookId: String)
GET /lobby/sync controllers.LobbyXhrC.syncWithoutHook()
# Lobby Private API
POST /internal/lobby/join/:gameId/:color controllers.LobbyApiC.join(gameId: String, color: String)
POST /api/lobby/join/:gameId/:color controllers.LobbyApiC.join(gameId: String, color: String)
# Useless, but play2 needs it
GET /assets/*file controllers.Assets.at(path="/public", file)

View File

@ -2,12 +2,44 @@ package lila.system
import model._
import memo._
import db.GameRepo
import lila.chess.{ Color, White, Black }
import db._
import scalaz.effects._
import scala.annotation.tailrec
import scala.math.max
final class LobbyXhr(
gameRepo: GameRepo,
versionMemo: VersionMemo,
aliveMemo: AliveMemo) {
hookRepo: HookRepo,
lobbyMemo: LobbyMemo,
duration: Int,
sleep: Int) {
def sync(
auth: Boolean,
lang: String,
version: Int): IO[Map[String, Any]] = for {
newVersion versionWait(version)
hooks if (auth) hookRepo.allOpen else hookRepo.allOpenCasual
} yield Map(
"state" -> newVersion,
"pool" -> {
if (hooks.nonEmpty) Map("hooks" -> renderHooks(hooks, None))
else Map("message" -> "No game available right now, create one!")
}
)
private def renderHooks(hooks: List[Hook], myHookId: Option[String]) = for {
hook hooks
} yield hook.render ++ {
if (myHookId == hook.ownerId) Map("action" -> "cancel", "id" -> myHookId)
else Map("action" -> "join", "id" -> hook.id)
}
private def versionWait(version: Int): IO[Int] = io {
@tailrec
def wait(loop: Int): Int = {
if (loop == 0 || lobbyMemo.version != version) lobbyMemo.version
else { Thread sleep sleep; wait(loop - 1) }
}
wait(max(1, duration / sleep))
}
}

View File

@ -23,7 +23,7 @@ final class Syncer(
fullId: Option[String]): IO[Map[String, Any]] = {
for {
color io { Color(colorString) err "Invalid color" }
_ io { versionWait(gameId, color, version) }
_ versionWait(gameId, color, version)
gameAndPlayer gameRepo.player(gameId, color)
(game, player) = gameAndPlayer
isPrivate = fullId some { game.isPlayerFullId(player, _) } none false
@ -60,7 +60,7 @@ final class Syncer(
"html" -> """<li class="%s">%s</li>""".format(author, escapeXml(message))
)
private def versionWait(gameId: String, color: Color, version: Int) {
private def versionWait(gameId: String, color: Color, version: Int) = io {
@tailrec
def wait(loop: Int): Unit = {
if (loop == 0 || versionMemo.get(gameId, color) != version) ()

View File

@ -21,9 +21,10 @@ final class SystemEnv(config: Config) {
aliveMemo = aliveMemo)
lazy val lobbyXhr = new LobbyXhr(
gameRepo = gameRepo,
versionMemo = versionMemo,
aliveMemo = aliveMemo)
hookRepo = hookRepo,
lobbyMemo = lobbyMemo,
duration = getMilliseconds("lobby.poll.duration"),
sleep = getMilliseconds("lobby.poll.sleep"))
lazy val lobbyApi = new LobbyApi(
gameRepo = gameRepo,
@ -55,6 +56,9 @@ final class SystemEnv(config: Config) {
lazy val userRepo = new UserRepo(
mongodb(config getString "mongo.collection.user"))
lazy val hookRepo = new HookRepo(
mongodb(config getString "mongo.collection.hook"))
lazy val mongodb = MongoConnection(
config getString "mongo.host",
config getInt "mongo.port"
@ -74,6 +78,8 @@ final class SystemEnv(config: Config) {
lazy val watcherMemo = new WatcherMemo(
timeout = getMilliseconds("memo.watcher.timeout"))
lazy val lobbyMemo = new LobbyMemo
def getMilliseconds(name: String): Int = (config getMilliseconds name).toInt
}

View File

@ -0,0 +1,27 @@
package lila.system
package db
import model.Hook
import com.novus.salat._
import com.novus.salat.dao._
import com.mongodb.casbah.MongoCollection
import com.mongodb.casbah.Imports._
import scalaz.effects._
class HookRepo(collection: MongoCollection)
extends SalatDAO[Hook, String](collection) {
def allOpen = hookList(DBObject(
"match" -> false
))
def allOpenCasual = hookList(DBObject(
"match" -> false,
"mode" -> 0
))
def hookList(query: MongoDBObject): IO[List[Hook]] = io {
find(query) sort DBObject("createdAt" -> 1) toList
}
}

View File

@ -0,0 +1,7 @@
package lila.system
package memo
final class LobbyMemo {
val version: Int = 0
}

View File

@ -0,0 +1,37 @@
package lila.system
package model
case class Hook(
id: String,
ownerId: String,
variant: Int,
hasClock: Boolean,
time: Option[Int],
increment: Option[Int],
mode: Int,
color: String,
username: String,
elo: Option[Int],
`match`: Boolean,
eloRange: Option[String],
engine: Boolean) {
def realVariant = Variant(variant) | Standard
def realMode = Mode(mode) | Casual
def eloMin = eloRange map (_ takeWhile ('-' !=))
def eloMax = eloRange map (_ dropWhile ('-' !=) tail)
def render = Map(
"username" -> username,
"elo" -> elo,
"variant" -> realVariant.toString,
"mode" -> realMode.toString,
"color" -> color,
"clock" -> (if (hasClock) time + " + " + increment else "Unlimited"),
"emin" -> eloMin,
"emax" -> eloMax
) +? (engine, "engine" -> true)
}

View File

@ -0,0 +1,16 @@
package lila.system
package model
sealed abstract class Mode(val id: Int)
case object Casual extends Mode(0)
case object Rated extends Mode(1)
object Mode {
val all = List(Casual, Rated)
val byId = all map { v (v.id, v) } toMap
def apply(id: Int): Option[Mode] = byId get id
}

View File

@ -21,6 +21,10 @@ package object system
def pp[A] = a ~ println
}
implicit def richerMap[A, B](m: Map[A, B]) = new {
def +?(bp: (Boolean, (A, B))): Map[A, B] = if (bp._1) m + bp._2 else m
}
def parseIntOption(str: String): Option[Int] = try {
Some(java.lang.Integer.parseInt(str))
}

View File

@ -5,18 +5,18 @@ import model._
import scalaz.effects._
import scalaz.{ Success, Failure }
class ServerTest extends SystemTest {
class AppXhrTest extends SystemTest {
val env = SystemEnv()
val repo = env.gameRepo
val server = env.server
val xhr = env.appXhr
def insert(dbGame: DbGame = newDbGameWithRandomIds()): IO[DbGame] = for {
_ repo insert dbGame
} yield dbGame
def move(game: DbGame, m: String = "d2 d4"): IO[Valid[Unit]] =
server.playMove(game fullIdOf White, m)
xhr.playMove(game fullIdOf White, m)
def updated(
game: DbGame = newDbGameWithRandomIds,