Merge remote-tracking branch 'origin/timesealClock'

* origin/timesealClock: (21 commits)
  Fix outOfTime messages from spectators
  fix double reporting of player lag
  update scalachess
  Update Player.scala
  Update socket.js
  Minor syntax update
  apply lag grace on move
  update scalachess
  Always use performance.now
  Don't send 'd' key if empty
  fix timeseal bugs
  Fix compile error
  Update scalachess
  Don't use grace for self-outoftime reports
  Update socket.js
  Update scalachess
  Switch to base 36 for time
  Encode move time with radix
  Update scalachess
  Use client move time for lag computations
  ...
This commit is contained in:
Thibault Duplessis 2017-06-05 17:14:05 +02:00
commit 6adf49db68
13 changed files with 117 additions and 105 deletions

@ -1 +1 @@
Subproject commit e913f3179c8c81f89c5c7e7a0bf977a929f53c48
Subproject commit f0329923c7018ddfb7a9d9ad1fc2f7f043f6ec8c

View file

@ -80,11 +80,24 @@ object BinaryFormat {
}
case class clock(start: Timestamp) {
def legacyElapsed(clock: Clock, color: Color) =
clock.limit - clock.players(color).remaining
def computeRemaining(config: Clock.Config, legacyElapsed: Centis) =
config.limit - legacyElapsed
// TODO: new binary clock format
// - clock history
// - berserk bits
// - "real" elapsed
// - lag stats
def write(clock: Clock): ByteArray = {
Array(writeClockLimit(clock.limitSeconds), clock.incrementSeconds.toByte) ++
writeSignedInt24(clock.whiteTime.centis) ++
writeSignedInt24(clock.blackTime.centis) ++
clock.timerOption.fold(Array.empty[Byte])(writeTimer)
writeSignedInt24(legacyElapsed(clock, White).centis) ++
writeSignedInt24(legacyElapsed(clock, Black).centis) ++
clock.timer.fold(Array.empty[Byte])(writeTimer)
}
def read(ba: ByteArray, whiteBerserk: Boolean, blackBerserk: Boolean): Color => Clock = color => {
@ -101,27 +114,23 @@ object BinaryFormat {
ia match {
case Array(b1, b2, b3, b4, b5, b6, b7, b8, _*) => {
val config = Clock.Config(readClockLimit(b1), b2)
val whiteTime = Centis(readSignedInt24(b3, b4, b5))
val blackTime = Centis(readSignedInt24(b6, b7, b8))
timer.fold[Clock](
PausedClock(
config = config,
color = color,
whiteTime = whiteTime,
blackTime = blackTime,
whiteBerserk = whiteBerserk,
blackBerserk = blackBerserk
)
)(t =>
RunningClock(
val legacyWhite = Centis(readSignedInt24(b3, b4, b5))
val legacyBlack = Centis(readSignedInt24(b6, b7, b8))
Clock(
config = config,
color = color,
players = Color.Map(
ClockPlayer(
config = config,
color = color,
whiteTime = whiteTime,
blackTime = blackTime,
whiteBerserk = whiteBerserk,
blackBerserk = blackBerserk,
timer = t
))
berserk = whiteBerserk
).setRemaining(computeRemaining(config, legacyWhite)),
ClockPlayer(
config = config,
berserk = blackBerserk
).setRemaining(computeRemaining(config, legacyBlack))
),
timer = timer
)
}
case _ => sys error s"BinaryFormat.clock.read invalid bytes: ${ba.showBytes}"
}

View file

@ -6,7 +6,7 @@ import chess.Color.{ White, Black }
import chess.format.{ Uci, FEN }
import chess.opening.{ FullOpening, FullOpeningDB }
import chess.variant.{ Variant, Crazyhouse }
import chess.{ History => ChessHistory, CheckCount, Castles, Board, MoveOrDrop, Pos, Game => ChessGame, Clock, Status, Color, Mode, PositionHash, UnmovedRooks, Centis }
import chess.{ MoveMetrics, History => ChessHistory, CheckCount, Castles, Board, MoveOrDrop, Pos, Game => ChessGame, Clock, Status, Color, Mode, PositionHash, UnmovedRooks, Centis }
import org.joda.time.DateTime
import lila.common.Sequence
@ -189,7 +189,7 @@ case class Game(
game: ChessGame,
moveOrDrop: MoveOrDrop,
blur: Boolean = false,
lag: Option[Centis] = None
moveMetrics: MoveMetrics = MoveMetrics()
): Progress = {
val (history, situation) = (game.board.history, game.situation)
@ -446,12 +446,12 @@ case class Game(
def drawn = finished && winner.isEmpty
def outoftime(playerLag: Color => Centis): Boolean =
outoftimeClock(playerLag) || outoftimeCorrespondence
def outoftime(withGrace: Boolean): Boolean =
if (isCorrespondence) outoftimeCorrespondence else outoftimeClock(withGrace)
private def outoftimeClock(playerLag: Color => Centis): Boolean = clock ?? { c =>
private def outoftimeClock(withGrace: Boolean): Boolean = clock ?? { c =>
started && playable && (bothPlayersHaveMoved || isSimul) && {
(!c.isRunning && !c.isInit) || c.outoftimeWithGrace(turnColor, playerLag(turnColor))
(!c.isRunning && !c.isInit) || c.outOfTime(turnColor, withGrace)
}
}

View file

@ -72,7 +72,7 @@ class BinaryClockTest extends Specification {
isomorphism(c3) must_== c3
val c4 = clock.start
isomorphism(c4).timerOption.get.value must beCloseTo(c4.timer.value, 10)
isomorphism(c4).timer.get.value must beCloseTo(c4.timer.get.value, 10)
Clock(120, 60) |> { c =>
isomorphism(c) must_== c

View file

@ -9,7 +9,7 @@ import scala.concurrent.duration.Duration
import scala.concurrent.Promise
import chess.format.Uci
import chess.{ Centis, Pos }
import chess.Pos
import Forecast.Step
import lila.game.{ Pov, Game }
import lila.hub.actorApi.map.Tell
@ -55,7 +55,6 @@ final class ForecastApi(coll: Coll, roundMap: akka.actor.ActorSelection) {
playerId = pov.playerId,
uci = uci,
blur = true,
lag = Centis(0),
promise = promise.some
))
saveSteps(pov, steps) >> promise.future

View file

@ -1,7 +1,7 @@
package lila.round
import chess.format.{ Forsyth, FEN, Uci }
import chess.{ Centis, Status, Color, MoveOrDrop }
import chess.{ MoveMetrics, Centis, Status, Color, MoveOrDrop }
import actorApi.round.{ HumanPlay, DrawNo, TakebackNo, ForecastPlay }
import akka.actor.ActorRef
@ -19,7 +19,7 @@ private[round] final class Player(
def human(play: HumanPlay, round: ActorRef)(pov: Pov)(implicit proxy: GameProxy): Fu[Events] = play match {
case p @ HumanPlay(playerId, uci, blur, lag, promiseOption) => pov match {
case Pov(game, color) if game playableBy color =>
p.trace.segmentSync("applyUci", "logic")(applyUci(game, uci, blur, lag + humanLag)).prefixFailuresWith(s"$pov ")
p.trace.segmentSync("applyUci", "logic")(applyUci(game, uci, blur, lag)).prefixFailuresWith(s"$pov ")
.fold(errs => fufail(ClientError(errs.shows)), fuccess).flatMap {
case (progress, moveOrDrop) =>
p.trace.segment("save", "db")(proxy save progress) >>- {
@ -49,8 +49,8 @@ private[round] final class Player(
def fishnet(game: Game, uci: Uci, currentFen: FEN, round: ActorRef)(implicit proxy: GameProxy): Fu[Events] =
if (game.playable && game.player.isAi) {
if (currentFen == FEN(Forsyth >> game.toChess))
if (game.outoftime(_ => fishnetLag)) finisher.outOfTime(game)
else applyUci(game, uci, blur = false, lag = fishnetLag)
if (game.outoftime(withGrace = true)) finisher.outOfTime(game)
else applyUci(game, uci, blur = false, metrics = fishnetLag)
.fold(errs => fufail(ClientError(errs.shows)), fuccess).flatMap {
case (progress, moveOrDrop) =>
proxy.save(progress) >>-
@ -70,21 +70,18 @@ private[round] final class Player(
else fuccess(round ! actorApi.round.ResignAi)
}
private val clientLag = Centis(3)
private val serverLag = Centis(1)
private val humanLag = clientLag + serverLag
private val fishnetLag = Centis(1) + serverLag
private val fishnetLag = MoveMetrics()
private def applyUci(game: Game, uci: Uci, blur: Boolean, lag: Centis) = (uci match {
case Uci.Move(orig, dest, prom) => game.toChess.apply(orig, dest, prom, lag) map {
private def applyUci(game: Game, uci: Uci, blur: Boolean, metrics: MoveMetrics) = (uci match {
case Uci.Move(orig, dest, prom) => game.toChess.apply(orig, dest, prom, metrics) map {
case (ncg, move) => ncg -> (Left(move): MoveOrDrop)
}
case Uci.Drop(role, pos) => game.toChess.drop(role, pos, lag) map {
case Uci.Drop(role, pos) => game.toChess.drop(role, pos, metrics) map {
case (ncg, drop) => ncg -> (Right(drop): MoveOrDrop)
}
}).map {
case (newChessGame, moveOrDrop) =>
game.update(newChessGame, moveOrDrop, blur, lag.some) -> moveOrDrop
game.update(newChessGame, moveOrDrop, blur, metrics) -> moveOrDrop
}
private def notifyMove(moveOrDrop: MoveOrDrop, game: Game) {

View file

@ -2,7 +2,7 @@ package lila.round
import chess.format.Forsyth
import chess.variant._
import chess.{ Clock, Game => ChessGame, Board, Color => ChessColor, Castles }
import chess.{ Game => ChessGame, Board, Color => ChessColor, Castles, Clock }
import ChessColor.{ White, Black }
import lila.game.{ GameRepo, Game, Event, Progress, Pov, Source, AnonCookie, PerfPicker }

View file

@ -41,16 +41,6 @@ private[round] final class Round(
implicit val proxy = new GameProxy(gameId)
object lags { // player lag in centis
var white = Centis(0)
var black = Centis(0)
def get(c: Color) = c.fold(white, black)
def set(c: Color, v: Centis) {
if (c.white) white = v
else black = v
}
}
var takebackSituation = Round.TakebackSituation()
def process = {
@ -62,9 +52,8 @@ private[round] final class Round(
case p: HumanPlay =>
p.trace.finishFirstSegment()
handleHumanPlay(p) { pov =>
if (pov.game outoftime lags.get) finisher.outOfTime(pov.game)
if (pov.game.outoftime(withGrace = true)) finisher.outOfTime(pov.game)
else {
lags.set(pov.color, p.lag)
reportNetworkLag(pov)
player.human(p, self)(pov)
}
@ -120,8 +109,12 @@ private[round] final class Round(
}
}
case Outoftime => handle { game =>
game.outoftime(lags.get) ?? finisher.outOfTime(game)
case Outoftime(Some(playerId)) => handle(playerId) { pov =>
pov.game.outoftime(withGrace = !pov.isMyTurn) ?? finisher.outOfTime(pov.game)
}
case Outoftime(None) => handle { game =>
game.outoftime(withGrace = true) ?? finisher.outOfTime(game)
}
// exceptionally we don't block nor publish events
@ -195,7 +188,7 @@ private[round] final class Round(
case ForecastPlay(lastMove) => handle { game =>
forecastApi.nextMove(game, lastMove) map { mOpt =>
mOpt foreach { move =>
self ! HumanPlay(game.player.id, move, false, Centis(0))
self ! HumanPlay(game.player.id, move, false)
}
Nil
}
@ -228,8 +221,12 @@ private[round] final class Round(
}
private def reportNetworkLag(pov: Pov) =
if (pov.game.turns == 20 || pov.game.turns == 21) List(lags.white, lags.black).foreach { lag =>
if (lag.centis > 0) lila.mon.round.move.networkLag(lag.centis * 10l)
if (pov.game.turns == 20) {
for {
clock <- pov.game.clock
color <- Color.all
lag <- clock.lag(color)
} lila.mon.round.move.networkLag(lag.millis)
}
private def handle[A](op: Game => Fu[Events]): Funit =

View file

@ -2,11 +2,12 @@ package lila.round
import scala.concurrent.duration._
import scala.concurrent.Promise
import scala.util.Try
import akka.actor._
import akka.pattern.ask
import chess.format.Uci
import chess.Centis
import chess.{ Centis, MoveMetrics }
import play.api.libs.json.{ JsObject, Json }
import actorApi._, round._
@ -48,7 +49,7 @@ private[round] final class SocketHandler(
member.playerIdOption.fold[Handler.Controller](({
case ("p", o) => ping(o)
case ("talk", o) => o str "d" foreach { messenger.watcher(gameId, member, _) }
case ("outoftime", _) => send(Outoftime)
case ("outoftime", _) => send(Outoftime(None))
}: Handler.Controller) orElse evalCacheHandler(member, me) orElse lila.chat.Socket.in(
chatId = s"$gameId/w",
member = member,
@ -87,7 +88,7 @@ private[round] final class SocketHandler(
case ("draw-force", _) => send(DrawForce(playerId))
case ("abort", _) => send(Abort(playerId))
case ("moretime", _) => send(Moretime(playerId))
case ("outoftime", _) => send(Outoftime)
case ("outoftime", _) => send(Outoftime(playerId.some))
case ("bye2", _) => socket ! Bye(ref.color)
case ("talk", o) => o str "d" foreach { messenger.owner(gameId, member, _) }
case ("hold", o) => for {
@ -183,8 +184,10 @@ private[round] final class SocketHandler(
blur = d int "b" contains 1
} yield (drop, blur, parseLag(d))
private def parseLag(d: JsObject) =
Centis.ofMillis(d.int("l") orElse d.int("lag") getOrElse 0)
private def parseLag(d: JsObject) = MoveMetrics(
d.int("l") orElse d.int("lag") map Centis.ofMillis,
d.str("s") flatMap { v => Try(Centis(Integer.parseInt(v, 36))).toOption }
)
private val ackEvent = Json.obj("t" -> "ack")
}

View file

@ -46,7 +46,7 @@ private[round] final class Titivate(
if (game.finished || game.isPgnImport || game.playedThenAborted)
GameRepo unsetCheckAt game
else if (game.outoftime(_ => chess.Clock.maxGrace)) fuccess {
else if (game.outoftime(withGrace = true)) fuccess {
roundMap ! Tell(game.id, Outoftime)
}

View file

@ -4,7 +4,7 @@ package actorApi
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.Promise
import chess.{ Centis, Color }
import chess.{ MoveMetrics, Color }
import chess.format.Uci
import lila.common.IpAddress
@ -102,7 +102,7 @@ package round {
playerId: String,
uci: Uci,
blur: Boolean,
lag: Centis,
moveMetrics: MoveMetrics = MoveMetrics(),
promise: Option[Promise[Unit]] = None
) {
@ -129,7 +129,7 @@ package round {
case class TakebackYes(playerId: String)
case class TakebackNo(playerId: String)
case class Moretime(playerId: String)
case object Outoftime
case class Outoftime(playerId: Option[String])
case object Abandon
case class ForecastPlay(lastMove: chess.Move)
case class Cheat(color: Color)

View file

@ -44,7 +44,8 @@ module.exports = function(opts, redraw) {
justDropped: null,
justCaptured: null,
preDrop: null,
lastDrawOfferAtPly: null
lastDrawOfferAtPly: null,
lastMoveMillis: null
};
this.vm.goneBerserk[this.data.player.color] = opts.data.player.berserk;
this.vm.goneBerserk[this.data.opponent.color] = opts.data.opponent.berserk;
@ -163,6 +164,22 @@ module.exports = function(opts, redraw) {
this.setTitle = () => title.set(this);
this.actualSendMove = function(type, action, meta) {
meta = meta === undefined ? {} : meta
var socketOpts = {
ackable: true,
// withLag: !!this.clock
}
var startTime = this.vm.lastMoveMillis;
if (startTime !== null) socketOpts.millis = performance.now() - startTime;
this.socket.send(type, action, socketOpts);
this.vm.justDropped = meta.justDropped;
this.vm.justCaptured = meta.justCaptured;
this.vm.preDrop = null;
redraw();
}
this.sendMove = function(orig, dest, prom, meta) {
var move = {
u: orig + dest
@ -172,13 +189,9 @@ module.exports = function(opts, redraw) {
this.resign(false);
if (this.data.pref.submitMove && !meta.premove) {
this.vm.moveToSubmit = move;
} else this.socket.send('move', move, {
ackable: true,
withLag: !!this.clock
});
this.vm.justDropped = null;
this.vm.justCaptured = meta.captured;
redraw();
redraw();
} else this.actualSendMove('move', move, {justCaptured: meta.captured});
}.bind(this);
this.sendNewPiece = function(role, key, isPredrop) {
@ -190,14 +203,10 @@ module.exports = function(opts, redraw) {
this.resign(false);
if (this.data.pref.submitMove && !isPredrop) {
this.vm.dropToSubmit = drop;
} else this.socket.send('drop', drop, {
ackable: true,
withLag: !!this.clock
});
this.vm.preDrop = null;
this.vm.justDropped = role;
this.vm.justCaptured = null;
redraw();
redraw();
} else {
this.actualSendMove('drop', drop, {justDropped: role});
}
}.bind(this);
var showYourMoveNotification = function() {
@ -224,6 +233,8 @@ module.exports = function(opts, redraw) {
this.apiMove = function(o) {
var d = this.data,
playing = game.isPlayerPlaying(d);
if (playing) this.vm.lastMoveMillis = performance.now();
d.game.turns = o.ply;
d.game.player = o.ply % 2 === 0 ? 'white' : 'black';
var playedColor = o.ply % 2 === 0 ? 'black' : 'white';
@ -481,16 +492,11 @@ module.exports = function(opts, redraw) {
this.submitMove = function(v) {
if (v && (this.vm.moveToSubmit || this.vm.dropToSubmit)) {
if (this.vm.moveToSubmit)
this.socket.send('move', this.vm.moveToSubmit, {
ackable: true,
withLag: !!this.clock
});
else if (this.vm.dropToSubmit)
this.socket.send('drop', this.vm.dropToSubmit, {
ackable: true,
withLag: !!this.clock
});
if (this.vm.moveToSubmit) {
this.actualSendMove('move', this.vm.moveToSubmit);
} else {
this.actualSendMove('drop', this.vm.dropToSubmit);
}
lichess.sound.confirmation();
} else this.jump(this.vm.ply);
this.cancelMove();

View file

@ -65,14 +65,15 @@ lichess.StrongSocket = function(url, version, settings) {
};
var send = function(t, d, o, noRetry) {
d = d == null ? {} : d,
o = o || {};
if (o.withLag) d.l = Math.round(averageLag);
o = o || {};
var msg = { t: t };
if (d !== undefined) {
if (o.withLag) d.l = Math.round(averageLag);
if ('millis' in o) d.s = Math.floor(o.millis * 0.1).toString(36);
msg.d = d;
}
if (o.ackable) ackable.register(t, d);
var message = JSON.stringify({
t: t,
d: d
});
var message = JSON.stringify(msg);
debug("send " + message);
try {
ws.send(message);