Start implementing end game logic
This commit is contained in:
parent
f62fcbf8fb
commit
0da37614fb
|
@ -11,7 +11,7 @@ object AppApiC extends LilaController {
|
|||
private val api = env.appApi
|
||||
|
||||
def talk(gameId: String) = Action { implicit request ⇒
|
||||
ValidIOk[TalkData](talkForm)(talk ⇒ api.talk(gameId, talk._1, talk._2))
|
||||
FormValidIOk[TalkData](talkForm)(talk ⇒ api.talk(gameId, talk._1, talk._2))
|
||||
}
|
||||
|
||||
def updateVersion(gameId: String) = Action {
|
||||
|
@ -27,23 +27,19 @@ object AppApiC extends LilaController {
|
|||
}
|
||||
|
||||
def draw(gameId: String, color: String) = Action { implicit request ⇒
|
||||
ValidIOk[String](drawForm)(msgs ⇒ api.draw(gameId, color, msgs))
|
||||
FormValidIOk[String](drawForm)(msgs ⇒ api.draw(gameId, color, msgs))
|
||||
}
|
||||
|
||||
def drawAccept(gameId: String, color: String) = Action { implicit request ⇒
|
||||
ValidIOk[String](drawForm)(msgs ⇒ api.drawAccept(gameId, color, msgs))
|
||||
}
|
||||
|
||||
def end(gameId: String) = Action { implicit request ⇒
|
||||
ValidIOk[String](endForm)(msgs ⇒ api.end(gameId, msgs))
|
||||
FormValidIOk[String](drawForm)(msgs ⇒ api.drawAccept(gameId, color, msgs))
|
||||
}
|
||||
|
||||
def start(gameId: String) = Action { implicit request =>
|
||||
ValidIOk[EntryData](entryForm)(entryData ⇒ api.start(gameId, entryData))
|
||||
FormValidIOk[EntryData](entryForm)(entryData ⇒ api.start(gameId, entryData))
|
||||
}
|
||||
|
||||
def join(fullId: String) = Action { implicit request ⇒
|
||||
ValidIOk[JoinData](joinForm) { join ⇒
|
||||
FormValidIOk[JoinData](joinForm) { join ⇒
|
||||
api.join(fullId, join._1, join._2, join._3)
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +53,7 @@ object AppApiC extends LilaController {
|
|||
}
|
||||
|
||||
def rematchAccept(gameId: String, color: String, newGameId: String) = Action { implicit request ⇒
|
||||
ValidIOk[RematchData](rematchForm)(r ⇒
|
||||
FormValidIOk[RematchData](rematchForm)(r ⇒
|
||||
api.rematchAccept(gameId, newGameId, color, r._1, r._2, r._3))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,19 @@ object AppXhrC extends LilaController {
|
|||
})
|
||||
}
|
||||
|
||||
def ping() = Action { implicit request =>
|
||||
def abort(fullId: String) = Action {
|
||||
ValidIORedir(xhr abort fullId, fullId)
|
||||
}
|
||||
|
||||
def outoftime(fullId: String) = Action {
|
||||
ValidIORedir(xhr outoftime fullId, fullId)
|
||||
}
|
||||
|
||||
def resign(fullId: String) = Action {
|
||||
ValidIORedir(xhr resign fullId, fullId)
|
||||
}
|
||||
|
||||
def ping() = Action { implicit request ⇒
|
||||
JsonIOk(env.pinger.ping(
|
||||
username = get("username"),
|
||||
playerKey = get("player_key"),
|
||||
|
@ -35,7 +47,5 @@ object AppXhrC extends LilaController {
|
|||
))
|
||||
}
|
||||
|
||||
def nbPlayers() = Action {
|
||||
Ok(env.aliveMemo.count.toString)
|
||||
}
|
||||
def nbPlayers = Action { Ok(env.aliveMemo.count.toString) }
|
||||
}
|
||||
|
|
|
@ -24,7 +24,13 @@ trait LilaController extends Controller with ContentTypes with RequestGetter {
|
|||
_ ⇒ Ok("ok")
|
||||
)
|
||||
|
||||
def ValidIOk[A](form: Form[A])(op: A ⇒ IO[Unit])(implicit request: Request[_]) =
|
||||
def ValidIORedir(op: IO[Valid[Unit]], url: => String) =
|
||||
op.unsafePerformIO.fold(
|
||||
failures ⇒ BadRequest(failures.list mkString "\n"),
|
||||
_ ⇒ Redirect("/" + url)
|
||||
)
|
||||
|
||||
def FormValidIOk[A](form: Form[A])(op: A ⇒ IO[Unit])(implicit request: Request[_]) =
|
||||
form.bindFromRequest.fold(
|
||||
form ⇒ BadRequest(form.errors mkString "\n"),
|
||||
data ⇒ IOk(op(data))
|
||||
|
|
|
@ -12,7 +12,7 @@ object LobbyApiC extends LilaController {
|
|||
private val api = env.lobbyApi
|
||||
|
||||
def join(gameId: String, color: String) = Action { implicit request ⇒
|
||||
ValidIOk[EntryData](entryForm)(entry ⇒ api.join(gameId, color, entry))
|
||||
FormValidIOk[EntryData](entryForm)(entry ⇒ api.join(gameId, color, entry))
|
||||
}
|
||||
|
||||
def create(hookOwnerId: String) = Action {
|
||||
|
|
4
bc
4
bc
|
@ -1,5 +1,5 @@
|
|||
Game.lastMove (and no more in pieces notation)
|
||||
clock time in milliseconds (and no more floating seconds)
|
||||
request promotion is not wrapped in options anymore
|
||||
x request promotion is not wrapped in options anymore
|
||||
Game.castles (new field in forsyth format)
|
||||
clock.times => clock.white, clock.black
|
||||
blurs
|
||||
|
|
|
@ -95,17 +95,18 @@ case class Board(pieces: Map[Pos, Piece], history: History) {
|
|||
def count(p: Piece): Int = pieces.values count (_ == p)
|
||||
def count(c: Color): Int = pieces.values count (_.color == c)
|
||||
|
||||
def autoDraw: Boolean = {
|
||||
history.positionHashes.size > 100 || (Color.all map rolesOf forall { roles ⇒
|
||||
(roles filterNot (_ == King)) match {
|
||||
case roles if roles.size > 1 ⇒ false
|
||||
case List(Knight) ⇒ true
|
||||
case List(Bishop) ⇒ true
|
||||
case Nil ⇒ true
|
||||
case _ ⇒ false
|
||||
}
|
||||
})
|
||||
}
|
||||
def autoDraw: Boolean =
|
||||
history.positionHashes.size > 100 ||
|
||||
(Color.all forall { !hasEnoughMaterialToMate(_) })
|
||||
|
||||
def hasEnoughMaterialToMate(color: Color) =
|
||||
rolesOf(color) filterNot (_ == King) match {
|
||||
case roles if roles.size > 1 ⇒ true
|
||||
case List(Knight) ⇒ false
|
||||
case List(Bishop) ⇒ false
|
||||
case Nil ⇒ false
|
||||
case _ ⇒ true
|
||||
}
|
||||
|
||||
def positionHash = Hasher(actors.values map (_.hash) mkString).md5.toString
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ sealed trait Clock {
|
|||
|
||||
def time(c: Color) = if (c == White) whiteTime else blackTime
|
||||
|
||||
def isOutOfTime(c: Color) = remainingTime(c) == 0
|
||||
def outoftime(c: Color) = remainingTime(c) == 0
|
||||
|
||||
def remainingTime(c: Color) = max(0, limit - elapsedTime(c))
|
||||
|
||||
|
|
42
chess/src/main/scala/EloCalculator.scala
Normal file
42
chess/src/main/scala/EloCalculator.scala
Normal file
|
@ -0,0 +1,42 @@
|
|||
package lila.chess
|
||||
|
||||
import scala.math.round
|
||||
|
||||
final class EloCalculator {
|
||||
|
||||
// Player 1 wins
|
||||
val P1WIN = -1;
|
||||
|
||||
// No player wins
|
||||
val DRAW = 0;
|
||||
|
||||
// Player 2 wins
|
||||
val P2WIN = 1;
|
||||
|
||||
type User = {
|
||||
def elo: Int
|
||||
def nbRatedGames: Int
|
||||
}
|
||||
|
||||
def calculate(user1: User, user2: User, win: Option[Color]): (Int, Int) = {
|
||||
val winCode = win match {
|
||||
case None ⇒ DRAW
|
||||
case Some(White) ⇒ P1WIN
|
||||
case Some(Black) ⇒ P2WIN
|
||||
}
|
||||
(calculateUserElo(user1, user2.elo, -winCode),
|
||||
calculateUserElo(user2, user1.elo, winCode))
|
||||
}
|
||||
|
||||
private def calculateUserElo(user: User, opponentElo: Int, win: Int) = {
|
||||
val score = (1 + win) / 2f
|
||||
val expected = 1 / (1 + math.pow(10, (opponentElo - user.elo) / 400f))
|
||||
val kFactor = math.round(
|
||||
if (user.nbRatedGames > 20) 16
|
||||
else 50 - user.nbRatedGames * (34 / 20f)
|
||||
)
|
||||
val diff = 2 * kFactor * (score - expected)
|
||||
|
||||
round(user.elo + diff).toInt
|
||||
}
|
||||
}
|
71
chess/src/test/scala/EloTest.scala
Normal file
71
chess/src/test/scala/EloTest.scala
Normal file
|
@ -0,0 +1,71 @@
|
|||
package lila.chess
|
||||
|
||||
class EloTest extends ChessTest {
|
||||
|
||||
val calc = new EloCalculator
|
||||
import calc._
|
||||
|
||||
def user(e: Int, n: Int) = new {
|
||||
val elo = e
|
||||
val nbGames = n
|
||||
}
|
||||
|
||||
"calculate standard" should {
|
||||
"with equal elo" in {
|
||||
val (u1, u2) = (user(1400, 56), user(1400, 389))
|
||||
"p1 win" in {
|
||||
val (nu1, nu2) = calculate(u1, u2, -1)
|
||||
"new elos" in {
|
||||
(nu1, nu2) must_== (1416, 1384)
|
||||
}
|
||||
"conservation rule" in {
|
||||
nu1 - u1.elo + nu2 - u2.elo must_== 0
|
||||
}
|
||||
}
|
||||
"p1 loss" in {
|
||||
val (u1, u2) = (user(1400, 56), user(1400, 389))
|
||||
val (nu1, nu2) = calculate(u1, u2, 1)
|
||||
"new elos" in {
|
||||
(nu1, nu2) must_== (1384, 1416)
|
||||
}
|
||||
"conservation rule" in {
|
||||
nu1 - u1.elo + nu2 - u2.elo must_== 0
|
||||
}
|
||||
}
|
||||
"draw" in {
|
||||
val (u1, u2) = (user(1400, 56), user(1400, 389))
|
||||
val (nu1, nu2) = calculate(u1, u2, 0)
|
||||
"new elos" in {
|
||||
(nu1, nu2) must_== (1400, 1400)
|
||||
}
|
||||
"conservation rule" in {
|
||||
nu1 - u1.elo + nu2 - u2.elo must_== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
"loss" in {
|
||||
val (u1, u2) = (user(1613, 56), user(1388, 389))
|
||||
val (nu1, nu2) = calculate(u1, u2, -1)
|
||||
"new elos" in {
|
||||
(nu1, nu2) must_== (1620, 1381)
|
||||
}
|
||||
"conservation rule" in {
|
||||
nu1 - u1.elo + nu2 - u2.elo must_== 0
|
||||
}
|
||||
}
|
||||
}
|
||||
"provision" should {
|
||||
val (u1, u2) = (user(1613, 8), user(1388, 389))
|
||||
val (nu1, nu2) = calculate(u1, u2, -1)
|
||||
"new elos" in {
|
||||
(nu1, nu2) must_== (1628, 1381)
|
||||
}
|
||||
}
|
||||
"no provision" should {
|
||||
val (u1, u2) = (user(1313, 1256), user(1158, 124))
|
||||
val (nu1, nu2) = calculate(u1, u2, -1)
|
||||
"new elos" in {
|
||||
(nu1, nu2) must_== (1322, 1149)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ mongo {
|
|||
hook = hook
|
||||
entry = lobby_entry
|
||||
message = lobby_message
|
||||
history = user_history
|
||||
}
|
||||
}
|
||||
redis {
|
||||
|
@ -35,6 +36,7 @@ memo {
|
|||
watcher.timeout = 6 seconds
|
||||
username.timeout = 10 seconds
|
||||
hook.timeout = 6 seconds
|
||||
finisher_lock.timeout = 30 seconds
|
||||
}
|
||||
crafty {
|
||||
exec_path = "/usr/bin/crafty"
|
||||
|
|
|
@ -8,11 +8,13 @@ GET /sync/:gameId/:color/:version controllers.AppXhrC.syncPublic(gameId: S
|
|||
GET /sync/:gameId/:color/:version/:fullId controllers.AppXhrC.sync(gameId: String, color: String, version: Int, fullId: String)
|
||||
GET /how-many-players-now controllers.AppXhrC.nbPlayers
|
||||
POST /move/:fullId controllers.AppXhrC.move(fullId: String)
|
||||
GET /abort/:fullId controllers.AppXhrC.abort(fullId: String)
|
||||
GET /resign/:fullId controllers.AppXhrC.resign(fullId: String)
|
||||
POST /outoftime/:fullId controllers.AppXhrC.outoftime(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/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)
|
||||
|
|
|
@ -32,12 +32,6 @@ case class AppApi(
|
|||
_ ← save(g1, g2)
|
||||
} yield ()
|
||||
|
||||
def end(gameId: String, messages: String): IO[Unit] = for {
|
||||
g1 ← gameRepo game gameId
|
||||
g2 = g1 withEvents (EndEvent() :: decodeMessages(messages))
|
||||
_ ← save(g1, g2)
|
||||
} yield ()
|
||||
|
||||
def start(gameId: String, entryData: String): IO[Unit] = for {
|
||||
game ← gameRepo game gameId
|
||||
_ ← addEntry(game, entryData)
|
||||
|
|
|
@ -7,16 +7,19 @@ import lila.chess._
|
|||
import Pos.posAt
|
||||
import scalaz.effects._
|
||||
|
||||
final class AppXhr(
|
||||
case class AppXhr(
|
||||
gameRepo: GameRepo,
|
||||
ai: Ai,
|
||||
finisher: Finisher,
|
||||
versionMemo: VersionMemo,
|
||||
aliveMemo: AliveMemo) {
|
||||
aliveMemo: AliveMemo) extends IOTools {
|
||||
|
||||
type IOValid = IO[Valid[Unit]]
|
||||
|
||||
def playMove(
|
||||
fullId: String,
|
||||
moveString: String,
|
||||
promString: Option[String] = None): IO[Valid[Unit]] =
|
||||
promString: Option[String] = None): IOValid =
|
||||
(decodeMoveString(moveString) toValid "Wrong move").fold(
|
||||
e ⇒ io(failure(e)),
|
||||
move ⇒ play(fullId, move._1, move._2, promString)
|
||||
|
@ -26,7 +29,7 @@ final class AppXhr(
|
|||
fullId: String,
|
||||
fromString: String,
|
||||
toString: String,
|
||||
promString: Option[String] = None): IO[Valid[Unit]] =
|
||||
promString: Option[String] = None): IOValid =
|
||||
gameRepo player fullId flatMap {
|
||||
case (g1, player) ⇒ purePlay(g1, fromString, toString, promString).fold(
|
||||
e ⇒ io(failure(e)),
|
||||
|
@ -43,6 +46,22 @@ final class AppXhr(
|
|||
)
|
||||
}
|
||||
|
||||
def abort(fullId: String): IOValid = for {
|
||||
game ← gameRepo playerGame fullId
|
||||
res ← (finisher abort game).sequence
|
||||
} yield res
|
||||
|
||||
def resign(fullId: String): IOValid = for {
|
||||
gameAndPlayer ← gameRepo player fullId
|
||||
(game, player) = gameAndPlayer
|
||||
res ← (finisher.resign(game, player.color)).sequence
|
||||
} yield res
|
||||
|
||||
def outoftime(fullId: String): IOValid = for {
|
||||
game ← gameRepo playerGame fullId
|
||||
res ← (finisher outoftime game).sequence
|
||||
} yield res
|
||||
|
||||
private def purePlay(
|
||||
g1: DbGame,
|
||||
origString: String,
|
||||
|
|
75
system/src/main/scala/Finisher.scala
Normal file
75
system/src/main/scala/Finisher.scala
Normal file
|
@ -0,0 +1,75 @@
|
|||
package lila.system
|
||||
|
||||
import db.{ UserRepo, GameRepo, HistoryRepo }
|
||||
import model._
|
||||
import memo.{ VersionMemo, FinisherLock }
|
||||
import lila.chess.{ Color, White, Black, EloCalculator }
|
||||
|
||||
import scalaz.effects._
|
||||
|
||||
final class Finisher(
|
||||
historyRepo: HistoryRepo,
|
||||
userRepo: UserRepo,
|
||||
gameRepo: GameRepo,
|
||||
versionMemo: VersionMemo,
|
||||
eloCalculator: EloCalculator,
|
||||
finisherLock: FinisherLock) {
|
||||
|
||||
type ValidIO = Valid[IO[Unit]]
|
||||
|
||||
def abort(game: DbGame): ValidIO =
|
||||
if (game.abortable) finish(game, Aborted)
|
||||
else !!("game is not abortable")
|
||||
|
||||
def resign(game: DbGame, color: Color): ValidIO =
|
||||
if (game.resignable) finish(game, Resign, Some(!color))
|
||||
else !!("game is not resignable")
|
||||
|
||||
def outoftime(game: DbGame): ValidIO =
|
||||
game.outoftimePlayer some { player ⇒
|
||||
finish(game, Outoftime,
|
||||
Some(!player.color) filter game.toChess.board.hasEnoughMaterialToMate)
|
||||
} none !!("no outoftime applicable")
|
||||
|
||||
private def !!(msg: String) = failure(msg.wrapNel)
|
||||
|
||||
private def finish(
|
||||
game: DbGame,
|
||||
status: Status,
|
||||
winner: Option[Color] = None,
|
||||
message: Option[String] = None): ValidIO =
|
||||
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
|
||||
_ ← updateElo(g2)
|
||||
_ ← incNbGames(g2, White)
|
||||
_ ← incNbGames(g2, Black)
|
||||
} yield ())
|
||||
|
||||
private def incNbGames(game: DbGame, color: Color) =
|
||||
game.player(color).userId.fold(
|
||||
id ⇒ userRepo.incNbGames(id, game.rated),
|
||||
io()
|
||||
)
|
||||
|
||||
private def updateElo(game: DbGame): IO[Unit] =
|
||||
if (!game.finished || !game.rated || game.turns < 2) io()
|
||||
else {
|
||||
for {
|
||||
whiteUserId ← game.player(White).userId
|
||||
blackUserId ← game.player(Black).userId
|
||||
if whiteUserId != blackUserId
|
||||
} yield for {
|
||||
whiteUser ← userRepo user whiteUserId
|
||||
blackUser ← userRepo user blackUserId
|
||||
(whiteElo, blackElo) = eloCalculator.calculate(whiteUser, blackUser, game.winnerColor)
|
||||
_ ← userRepo.setElo(whiteUser.id, whiteElo)
|
||||
_ ← userRepo.setElo(blackUser.id, blackElo)
|
||||
_ ← historyRepo.addEntry(whiteUser.username, whiteElo, game.id)
|
||||
_ ← historyRepo.addEntry(blackUser.username, blackElo, game.id)
|
||||
} yield ()
|
||||
} | io()
|
||||
}
|
|
@ -3,6 +3,7 @@ package lila.system
|
|||
import com.mongodb.casbah.MongoConnection
|
||||
import com.typesafe.config._
|
||||
|
||||
import lila.chess.EloCalculator
|
||||
import db._
|
||||
import ai._
|
||||
import memo._
|
||||
|
@ -12,6 +13,7 @@ final class SystemEnv(config: Config) {
|
|||
lazy val appXhr = new AppXhr(
|
||||
gameRepo = gameRepo,
|
||||
ai = ai,
|
||||
finisher = finisher,
|
||||
versionMemo = versionMemo,
|
||||
aliveMemo = aliveMemo)
|
||||
|
||||
|
@ -63,6 +65,15 @@ final class SystemEnv(config: Config) {
|
|||
watcherMemo = watcherMemo,
|
||||
hookMemo = hookMemo)
|
||||
|
||||
lazy val finisher = new Finisher(
|
||||
historyRepo = historyRepo,
|
||||
userRepo = userRepo,
|
||||
gameRepo = gameRepo,
|
||||
versionMemo = versionMemo,
|
||||
eloCalculator = new EloCalculator,
|
||||
finisherLock = new FinisherLock(
|
||||
timeout = getMilliseconds("memo.finisher_lock.timeout")))
|
||||
|
||||
lazy val ai: Ai = craftyAi
|
||||
|
||||
lazy val craftyAi = new CraftyAi(
|
||||
|
@ -86,6 +97,10 @@ final class SystemEnv(config: Config) {
|
|||
collection = mongodb(config getString "mongo.collection.message"),
|
||||
max = config getInt "lobby.message.max")
|
||||
|
||||
lazy val historyRepo = new HistoryRepo(
|
||||
collection = mongodb(config getString "mongo.collection.history")
|
||||
)
|
||||
|
||||
lazy val mongodb = MongoConnection(
|
||||
config getString "mongo.host",
|
||||
config getInt "mongo.port"
|
||||
|
|
25
system/src/main/scala/db/HistoryRepo.scala
Normal file
25
system/src/main/scala/db/HistoryRepo.scala
Normal file
|
@ -0,0 +1,25 @@
|
|||
package lila.system
|
||||
package db
|
||||
|
||||
import com.mongodb.casbah.MongoCollection
|
||||
import com.mongodb.casbah.Imports._
|
||||
|
||||
import scalaz.effects._
|
||||
|
||||
final class HistoryRepo(collection: MongoCollection) {
|
||||
|
||||
val entryType = 2
|
||||
|
||||
def addEntry(username: String, elo: Int, gameId: String): IO[Unit] = io {
|
||||
val tsKey = (System.currentTimeMillis / 1000).toString
|
||||
collection.update(
|
||||
DBObject("_id" -> username),
|
||||
DBObject("$set" -> (("entries." + tsKey) -> DBObject(
|
||||
"t" -> entryType,
|
||||
"e" -> elo,
|
||||
"g" -> gameId
|
||||
))),
|
||||
false, false
|
||||
)
|
||||
}
|
||||
}
|
|
@ -12,6 +12,25 @@ import scalaz.effects._
|
|||
class UserRepo(collection: MongoCollection)
|
||||
extends SalatDAO[User, ObjectId](collection) {
|
||||
|
||||
def user(userId: String): IO[User] = user(new ObjectId(userId))
|
||||
|
||||
def user(userId: ObjectId): IO[User] = io {
|
||||
findOneByID(userId) err "No user found for id " + userId
|
||||
}
|
||||
|
||||
def setElo(userId: ObjectId, elo: Int): IO[Unit] = io {
|
||||
collection.update(
|
||||
DBObject("_id" -> userId),
|
||||
$set ("elo" -> elo))
|
||||
}
|
||||
|
||||
def incNbGames(userId: String, rated: Boolean): IO[Unit] = io {
|
||||
collection.update(
|
||||
DBObject("_id" -> new ObjectId(userId)),
|
||||
if (rated) $inc ("nbGames" -> 1, "nbRatedGames" -> 1)
|
||||
else $inc ("nbGames" -> 1))
|
||||
}
|
||||
|
||||
def updateOnlineUsernames(usernames: Iterable[String]): IO[Unit] = io {
|
||||
val names = usernames map (_.toLowerCase)
|
||||
collection.update(
|
||||
|
|
12
system/src/main/scala/memo/FinisherLock.scala
Normal file
12
system/src/main/scala/memo/FinisherLock.scala
Normal file
|
@ -0,0 +1,12 @@
|
|||
package lila.system
|
||||
package memo
|
||||
|
||||
import model.DbGame
|
||||
import scalaz.effects._
|
||||
|
||||
final class FinisherLock(timeout: Int) extends BooleanExpiryMemo(timeout) {
|
||||
|
||||
def isLocked(game: DbGame): Boolean = get(game.id)
|
||||
|
||||
def lock(game: DbGame): IO[Unit] = put(game.id)
|
||||
}
|
|
@ -19,7 +19,8 @@ case class DbGame(
|
|||
positionHashes: String = "",
|
||||
castles: String = "KQkq",
|
||||
isRated: Boolean = false,
|
||||
variant: Variant = Standard) {
|
||||
variant: Variant = Standard,
|
||||
winnerId: Option[String]) {
|
||||
|
||||
val players = List(whitePlayer, blackPlayer)
|
||||
|
||||
|
@ -163,6 +164,29 @@ case class DbGame(
|
|||
player(color).lastDrawOffer some (_ >= turns - 1) none false
|
||||
|
||||
def abortable = status == Started && turns < 2
|
||||
|
||||
def resignable = playable && !abortable
|
||||
|
||||
def finish(status: Status, winner: Option[Color], msg: Option[String]) = copy(
|
||||
status = status,
|
||||
winnerId = winner flatMap (player(_).userId),
|
||||
whitePlayer = whitePlayer finish (winner == White),
|
||||
blackPlayer = blackPlayer finish (winner == Black),
|
||||
positionHashes = ""
|
||||
) withEvents (EndEvent() :: msg.map(MessageEvent("system", _)).toList)
|
||||
|
||||
def rated = isRated
|
||||
|
||||
def finished = status >= Mate
|
||||
|
||||
def winnerColor: Option[Color] = players find (_.wins) map (_.color)
|
||||
|
||||
def outoftimePlayer: Option[DbPlayer] = for {
|
||||
c ← clock
|
||||
if this.playable
|
||||
if c outoftime player.color
|
||||
} yield player
|
||||
|
||||
}
|
||||
|
||||
object DbGame {
|
||||
|
|
|
@ -3,6 +3,8 @@ package model
|
|||
|
||||
import lila.chess._
|
||||
|
||||
import com.mongodb.DBRef
|
||||
|
||||
case class DbPlayer(
|
||||
id: String,
|
||||
color: Color,
|
||||
|
@ -12,7 +14,8 @@ case class DbPlayer(
|
|||
evts: String = "",
|
||||
elo: Option[Int],
|
||||
isOfferingDraw: Boolean,
|
||||
lastDrawOffer: Option[Int]) {
|
||||
lastDrawOffer: Option[Int],
|
||||
user: Option[DBRef]) {
|
||||
|
||||
def eventStack: EventStack = EventStack decode evts
|
||||
|
||||
|
@ -32,4 +35,12 @@ case class DbPlayer(
|
|||
} mkString " "
|
||||
|
||||
def isAi = aiLevel.isDefined
|
||||
|
||||
def userId: Option[String] = user map (_.getId.toString)
|
||||
|
||||
def wins = isWinner getOrElse false
|
||||
|
||||
def finish(winner: Boolean) = copy(
|
||||
isWinner = if (winner) Some(true) else None
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,8 @@ case class RawDbGame(
|
|||
positionHashes: String = "",
|
||||
castles: String = "KQkq",
|
||||
isRated: Boolean = false,
|
||||
variant: Int = 1) {
|
||||
variant: Int = 1,
|
||||
winnerId: Option[String] = None) {
|
||||
|
||||
def decode: Option[DbGame] = for {
|
||||
whitePlayer ← players find (_.color == "white") flatMap (_.decode)
|
||||
|
@ -44,7 +45,8 @@ case class RawDbGame(
|
|||
positionHashes = positionHashes,
|
||||
castles = castles,
|
||||
isRated = isRated,
|
||||
variant = trueVariant
|
||||
variant = trueVariant,
|
||||
winnerId = winnerId
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -65,7 +67,8 @@ object RawDbGame {
|
|||
positionHashes = positionHashes,
|
||||
castles = castles,
|
||||
isRated = isRated,
|
||||
variant = variant.id
|
||||
variant = variant.id,
|
||||
winnerId = winnerId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package model
|
|||
|
||||
import lila.chess._
|
||||
|
||||
import com.mongodb.DBRef
|
||||
|
||||
case class RawDbPlayer(
|
||||
id: String,
|
||||
color: String,
|
||||
|
@ -12,7 +14,8 @@ case class RawDbPlayer(
|
|||
evts: String = "",
|
||||
elo: Option[Int],
|
||||
isOfferingDraw: Option[Boolean],
|
||||
lastDrawOffer: Option[Int]) {
|
||||
lastDrawOffer: Option[Int],
|
||||
user: Option[DBRef]) {
|
||||
|
||||
def decode: Option[DbPlayer] = for {
|
||||
trueColor ← Color(color)
|
||||
|
@ -25,7 +28,8 @@ case class RawDbPlayer(
|
|||
evts = evts,
|
||||
elo = elo,
|
||||
isOfferingDraw = isOfferingDraw getOrElse false,
|
||||
lastDrawOffer = lastDrawOffer
|
||||
lastDrawOffer = lastDrawOffer,
|
||||
user = user
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -42,7 +46,8 @@ object RawDbPlayer {
|
|||
evts = evts,
|
||||
elo = elo,
|
||||
isOfferingDraw = if (isOfferingDraw) Some(true) else None,
|
||||
lastDrawOffer = lastDrawOffer
|
||||
lastDrawOffer = lastDrawOffer,
|
||||
user = user
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,5 +6,8 @@ import com.mongodb.casbah.Imports.ObjectId
|
|||
case class User(
|
||||
id: ObjectId,
|
||||
username: String,
|
||||
isOnline: Boolean) {
|
||||
isOnline: Boolean,
|
||||
elo: Int,
|
||||
nbGames: Int,
|
||||
nbRatedGames: Int) {
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue