allow disabling moretime in user settings - closes #5357

pull/5360/head
Thibault Duplessis 2019-07-31 11:47:16 +02:00
parent f0538e22f6
commit 8308a64713
15 changed files with 147 additions and 61 deletions

View File

@ -157,6 +157,12 @@ trait SetupHelper { self: I18nHelper =>
(Pref.Takeback.CASUAL, trans.inCasualGamesOnly.txt())
)
def translatedMoretimeChoices(implicit ctx: Context) = List(
(Pref.Moretime.NEVER, trans.never.txt()),
(Pref.Moretime.ALWAYS, trans.always.txt()),
(Pref.Moretime.CASUAL, trans.inCasualGamesOnly.txt())
)
def translatedAutoQueenChoices(implicit ctx: Context) = List(
(Pref.AutoQueen.NEVER, trans.never.txt()),
(Pref.AutoQueen.PREMOVE, trans.whenPremoving.txt()),

View File

@ -111,6 +111,10 @@ object pref {
trans.takebacksWithOpponentApproval(),
radios(form("behavior.takeback"), translatedTakebackChoices)
),
setting(
trans.giveMoreTime(),
radios(form("behavior.moretime"), translatedMoretimeChoices)
),
setting(
trans.promoteToQueenAutomatically(),
radios(form("behavior.autoQueen"), translatedAutoQueenChoices)

View File

@ -31,6 +31,7 @@ object DataForm {
"moveEvent" -> optional(number.verifying(Set(0, 1, 2) contains _)),
"premove" -> booleanNumber,
"takeback" -> checkedNumber(Pref.Takeback.choices),
"moretime" -> checkedNumber(Pref.Moretime.choices),
"autoQueen" -> checkedNumber(Pref.AutoQueen.choices),
"autoThreefold" -> checkedNumber(Pref.AutoThreefold.choices),
"submitMove" -> checkedNumber(Pref.SubmitMove.choices),
@ -65,6 +66,7 @@ object DataForm {
moveEvent: Option[Int],
premove: Int,
takeback: Int,
moretime: Int,
autoQueen: Int,
autoThreefold: Int,
submitMove: Int,
@ -90,6 +92,7 @@ object DataForm {
autoQueen = behavior.autoQueen,
autoThreefold = behavior.autoThreefold,
takeback = behavior.takeback,
moretime = behavior.moretime,
clockTenths = clockTenths,
clockBar = clockBar == 1,
clockSound = clockSound == 1,
@ -135,6 +138,7 @@ object DataForm {
moveEvent = pref.moveEvent.some,
premove = if (pref.premove) 1 else 0,
takeback = pref.takeback,
moretime = pref.moretime,
autoQueen = pref.autoQueen,
autoThreefold = pref.autoThreefold,
submitMove = pref.submitMove,

View File

@ -19,6 +19,7 @@ object JsonView {
"autoQueen" -> p.autoQueen,
"autoThreefold" -> p.autoThreefold,
"takeback" -> p.takeback,
"moretime" -> p.moretime,
"clockTenths" -> p.clockTenths,
"clockBar" -> p.clockBar,
"clockSound" -> p.clockSound,

View File

@ -15,6 +15,7 @@ case class Pref(
autoQueen: Int,
autoThreefold: Int,
takeback: Int,
moretime: Int,
clockTenths: Int,
clockBar: Boolean,
clockSound: Boolean,
@ -236,6 +237,18 @@ object Pref {
)
}
object Moretime {
val NEVER = 1
val CASUAL = 2
val ALWAYS = 3
val choices = Seq(
NEVER -> "Never",
ALWAYS -> "Always",
CASUAL -> "In casual games only"
)
}
object Animation {
val NONE = 0
val FAST = 1
@ -364,6 +377,7 @@ object Pref {
autoQueen = AutoQueen.PREMOVE,
autoThreefold = AutoThreefold.TIME,
takeback = Takeback.ALWAYS,
moretime = Moretime.ALWAYS,
clockBar = true,
clockSound = true,
premove = true,

View File

@ -42,6 +42,7 @@ final class PrefApi(
autoQueen = r.getD("autoQueen", Pref.default.autoQueen),
autoThreefold = r.getD("autoThreefold", Pref.default.autoThreefold),
takeback = r.getD("takeback", Pref.default.takeback),
moretime = r.getD("moretime", Pref.default.moretime),
clockTenths = r.getD("clockTenths", Pref.default.clockTenths),
clockBar = r.getD("clockBar", Pref.default.clockBar),
clockSound = r.getD("clockSound", Pref.default.clockSound),
@ -84,6 +85,7 @@ final class PrefApi(
"autoQueen" -> o.autoQueen,
"autoThreefold" -> o.autoThreefold,
"takeback" -> o.takeback,
"moretime" -> o.moretime,
"clockTenths" -> o.clockTenths,
"clockBar" -> o.clockBar,
"clockSound" -> o.clockSound,

View File

@ -8,10 +8,10 @@ import scala.concurrent.duration._
import actorApi.{ GetSocketStatus, SocketStatus }
import lila.game.{ Game, GameRepo, Pov }
import lila.hub.actorApi.{ DeployPost, Announce }
import lila.hub.actorApi.map.Tell
import lila.hub.actorApi.round.{ Abort, Resign, FishnetPlay }
import lila.hub.actorApi.socket.HasUserId
import lila.hub.actorApi.{ DeployPost, Announce }
final class Env(
config: Config,
@ -63,13 +63,13 @@ final class Env(
private lazy val roundDependencies = Round.Dependencies(
messenger = messenger,
takebacker = takebacker,
moretimer = moretimer,
finisher = finisher,
rematcher = rematcher,
player = player,
drawer = drawer,
forecastApi = forecastApi,
socketMap = socketMap,
moretimeDuration = MoretimeDuration
socketMap = socketMap
)
val roundMap = new lila.hub.DuctMap[Round](
mkDuct = id => {
@ -216,7 +216,8 @@ final class Env(
noteApi = noteApi,
userJsonView = userJsonView,
getSocketStatus = getSocketStatus,
canTakeback = takebacker.isAllowedByPrefs,
canTakeback = takebacker.isAllowedIn,
canMoretime = moretimer.isAllowedIn,
divider = divider,
evalCache = evalCache,
baseAnimationDuration = AnimationDuration,
@ -234,12 +235,17 @@ final class Env(
private val corresAlarm = new CorresAlarm(system, db(CollectionAlarm), socketMap)
lazy val takebacker = new Takebacker(
private lazy val takebacker = new Takebacker(
messenger = messenger,
uciMemo = uciMemo,
prefApi = prefApi,
bus = bus
)
private lazy val moretimer = new Moretimer(
messenger = messenger,
prefApi = prefApi,
defaultDuration = MoretimeDuration
)
val tvBroadcast = system.actorOf(Props(classOf[TvBroadcast]))

View File

@ -21,6 +21,7 @@ final class JsonView(
userJsonView: lila.user.JsonView,
getSocketStatus: Game.ID => Fu[SocketStatus],
canTakeback: Game => Fu[Boolean],
canMoretime: Game => Fu[Boolean],
divider: lila.game.Divider,
evalCache: lila.evalCache.EvalCacheApi,
baseAnimationDuration: Duration,
@ -58,8 +59,9 @@ final class JsonView(
): Fu[JsObject] =
getSocketStatus(pov.gameId) zip
(pov.opponent.userId ?? UserRepo.byId) zip
canTakeback(pov.game) map {
case ((socket, opponentUser), takebackable) =>
canTakeback(pov.game) zip
canMoretime(pov.game) map {
case socket ~ opponentUser ~ takebackable ~ moretimeable =>
import pov._
Json.obj(
"game" -> gameJson(game, initialFen),
@ -112,6 +114,7 @@ final class JsonView(
).add("clock" -> game.clock.map(clockJson))
.add("correspondence" -> game.correspondenceClock)
.add("takebackable" -> takebackable)
.add("moretimeable" -> moretimeable)
.add("crazyhouse" -> pov.game.board.crazyData)
.add("possibleMoves" -> possibleMoves(pov, apiVersion))
.add("possibleDrops" -> possibleDrops(pov))

View File

@ -0,0 +1,59 @@
package lila.round
import chess.Color
import scala.concurrent.duration.FiniteDuration
import lila.game.{ GameRepo, Game, UciMemo, Pov, Rewind, Event, Progress }
import lila.pref.{ Pref, PrefApi }
private final class Moretimer(
messenger: Messenger,
prefApi: PrefApi,
defaultDuration: FiniteDuration
) {
// pov of the player giving more time
def apply(pov: Pov): Fu[Option[Progress]] = IfAllowed(pov.game) {
(pov.game moretimeable !pov.color) ?? {
if (pov.game.hasClock) give(pov.game, List(!pov.color), defaultDuration).some
else pov.game.correspondenceClock map { clock =>
messenger.system(pov.game, (_.untranslated(s"${!pov.color} gets more time")))
val p = pov.game.correspondenceGiveTime
p.game.correspondenceClock.map(Event.CorrespondenceClock.apply).fold(p)(p + _)
}
}
}
def isAllowedIn(game: Game): Fu[Boolean] =
if (game.isMandatory) fuFalse
else isAllowedByPrefs(game)
private[round] def give(game: Game, colors: List[Color], duration: FiniteDuration): Progress =
game.clock.fold(Progress(game)) { clock =>
val centis = duration.toCentis
val newClock = colors.foldLeft(clock) {
case (c, color) => c.giveTime(color, centis)
}
colors.foreach { c =>
messenger.system(game, (_.untranslated(s"$c + ${duration.toSeconds} seconds")))
}
(game withClock newClock) ++ colors.map { Event.ClockInc(_, centis) }
}
private def isAllowedByPrefs(game: Game): Fu[Boolean] =
game.userIds.map {
prefApi.getPref(_, (p: Pref) => p.moretime)
}.sequenceFu map {
_.forall { p =>
p == Pref.Takeback.ALWAYS || (p == Pref.Takeback.CASUAL && game.casual)
}
}
private def IfAllowed[A](game: Game)(f: => A): Fu[A] =
if (!game.playable) fufail(ClientError("[moretimer] game is over " + game.id))
else if (game.isMandatory) fufail(ClientError("[moretimer] game disallows it " + game.id))
else isAllowedByPrefs(game) flatMap {
case true => fuccess(f)
case _ => fufail(ClientError("[moretimer] disallowed by preferences " + game.id))
}
}

View File

@ -162,15 +162,10 @@ private[round] final class Round(
}
case Moretime(playerRef) => handle(playerRef) { pov =>
(pov.game moretimeable !pov.color) ?? {
val progress =
if (pov.game.hasClock) giveMoretime(pov.game, List(!pov.color), moretimeDuration)
else pov.game.correspondenceClock.fold(Progress(pov.game)) { clock =>
messenger.system(pov.game, (_.untranslated(s"${!pov.color} gets more time")))
val p = pov.game.correspondenceGiveTime
p.game.correspondenceClock.map(Event.CorrespondenceClock.apply).fold(p)(p + _)
}
proxy save progress inject progress.events
moretimer(pov) flatMap {
_ ?? { progress =>
proxy save progress inject progress.events
}
}
}
@ -187,7 +182,7 @@ private[round] final class Round(
game.playable ?? {
val freeTime = 20.seconds
messenger.system(game, (_.untranslated("Lichess has been updated! Sorry for the inconvenience.")))
val progress = giveMoretime(game, Color.all, freeTime)
val progress = moretimer.give(game, Color.all, freeTime)
proxy save progress inject progress.events
}
}
@ -206,20 +201,6 @@ private[round] final class Round(
}
}
private[this] def giveMoretime(game: Game, colors: List[Color], duration: FiniteDuration): Progress =
game.clock.fold(Progress(game)) { clock =>
val centis = duration.toCentis
val newClock = colors.foldLeft(clock) {
case (c, color) => c.giveTime(color, centis)
}
colors.foreach { c =>
messenger.system(game, (_.untranslated(
"%s + %d seconds".format(c, duration.toSeconds)
)))
}
(game withClock newClock) ++ colors.map { Event.ClockInc(_, centis) }
}
private[this] def recordLag(pov: Pov) =
if ((pov.game.playedTurns & 30) == 10) {
// Triggers every 32 moves starting on ply 10.
@ -290,13 +271,13 @@ object Round {
private[round] case class Dependencies(
messenger: Messenger,
takebacker: Takebacker,
moretimer: Moretimer,
finisher: Finisher,
rematcher: Rematcher,
player: Player,
drawer: Drawer,
forecastApi: ForecastApi,
socketMap: SocketMap,
moretimeDuration: FiniteDuration
socketMap: SocketMap
)
private[round] case class TakebackSituation(nbDeclined: Int, lastDeclined: Option[DateTime]) {

View File

@ -3,7 +3,7 @@ package lila.round
import lila.game.{ GameRepo, Game, UciMemo, Pov, Rewind, Event, Progress }
import lila.pref.{ Pref, PrefApi }
private[round] final class Takebacker(
private final class Takebacker(
messenger: Messenger,
uciMemo: UciMemo,
prefApi: PrefApi,
@ -45,10 +45,14 @@ private[round] final class Takebacker(
case _ => fufail(ClientError("[takebacker] invalid no " + pov))
}
def isAllowedByPrefs(game: Game): Fu[Boolean] =
def isAllowedIn(game: Game): Fu[Boolean] =
if (game.isMandatory) fuFalse
else isAllowedByPrefs(game)
private def isAllowedByPrefs(game: Game): Fu[Boolean] =
if (game.hasAi) fuTrue
else game.userIds.map { userId =>
prefApi.getPref(userId, (p: Pref) => p.takeback)
else game.userIds.map {
prefApi.getPref(_, (p: Pref) => p.takeback)
}.sequenceFu map {
_.forall { p =>
p == Pref.Takeback.ALWAYS || (p == Pref.Takeback.CASUAL && game.casual)
@ -65,6 +69,7 @@ private[round] final class Takebacker(
private def IfAllowed[A](game: Game)(f: => Fu[A]): Fu[A] =
if (!game.playable) fufail(ClientError("[takebacker] game is over " + game.id))
else if (game.isMandatory) fufail(ClientError("[takebacker] game disallows it " + game.id))
else isAllowedByPrefs(game) flatMap {
case true => f
case _ => fufail(ClientError("[takebacker] disallowed by preferences " + game.id))

View File

@ -30,6 +30,7 @@ export interface AnalyseData {
orientation: Color;
spectator?: boolean; // for compat with GameData, for game functions
takebackable: boolean;
moretimeable: boolean;
analysis?: Analysis;
userAnalysis: boolean;
forecast?: ForecastData;

View File

@ -46,8 +46,6 @@ export function abortable(data: GameData): boolean {
export function takebackable(data: GameData): boolean {
return playable(data) &&
data.takebackable &&
!data.tournament &&
!data.simul &&
bothPlayersHavePlayed(data) &&
!data.player.proposingTakeback &&
!data.opponent.proposingTakeback;
@ -73,7 +71,7 @@ export function berserkableBy(data: GameData): boolean {
}
export function moretimeable(data: GameData): boolean {
return isPlayerPlaying(data) && !mandatory(data) && (
return isPlayerPlaying(data) && data.moretimeable && (
!!data.clock ||
(!!data.correspondence &&
data.correspondence[data.opponent.color] < (data.correspondence.increment - 3600)

View File

@ -6,6 +6,7 @@ export interface GameData {
tournament?: Tournament;
simul?: Simul;
takebackable: boolean;
moretimeable: boolean;
clock?: Clock;
correspondence?: CorrespondenceClock;
}

View File

@ -45,11 +45,13 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
});
};
const d = ctrl.data;
const handlers: Handlers = {
takebackOffers(o) {
ctrl.setLoading(false);
ctrl.data.player.proposingTakeback = o[ctrl.data.player.color];
const fromOp = ctrl.data.opponent.proposingTakeback = o[ctrl.data.opponent.color];
d.player.proposingTakeback = o[d.player.color];
const fromOp = d.opponent.proposingTakeback = o[d.opponent.color];
if (fromOp) notify(ctrl.trans('yourOpponentProposesATakeback'));
ctrl.redraw();
},
@ -65,35 +67,34 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
},
cclock(o) {
if (ctrl.corresClock) {
ctrl.data.correspondence.white = o.white;
ctrl.data.correspondence.black = o.black;
d.correspondence.white = o.white;
d.correspondence.black = o.black;
ctrl.corresClock.update(o.white, o.black);
ctrl.redraw();
}
},
crowd(o) {
game.setOnGame(ctrl.data, 'white', o['white']);
game.setOnGame(ctrl.data, 'black', o['black']);
game.setOnGame(d, 'white', o['white']);
game.setOnGame(d, 'black', o['black']);
ctrl.redraw();
},
// end: function(winner) { } // use endData instead
endData(o: ApiEnd) {
ctrl.endWithData(o);
},
rematchOffer(by: Color) {
ctrl.data.player.offeringRematch = by === ctrl.data.player.color;
const fromOp = ctrl.data.opponent.offeringRematch = by === ctrl.data.opponent.color;
d.player.offeringRematch = by === d.player.color;
const fromOp = d.opponent.offeringRematch = by === d.opponent.color;
if (fromOp) notify(ctrl.trans('yourOpponentWantsToPlayANewGameWithYou'));
ctrl.redraw();
},
rematchTaken(nextId: string) {
ctrl.data.game.rematch = nextId;
if (!ctrl.data.player.spectator) ctrl.setLoading(true);
d.game.rematch = nextId;
if (!d.player.spectator) ctrl.setLoading(true);
else ctrl.redraw();
},
drawOffer(by) {
ctrl.data.player.offeringDraw = by === ctrl.data.player.color;
const fromOp = ctrl.data.opponent.offeringDraw = by === ctrl.data.opponent.color;
d.player.offeringDraw = by === d.player.color;
const fromOp = d.opponent.offeringDraw = by === d.opponent.color;
if (fromOp) notify(ctrl.trans('yourOpponentOffersADraw'));
ctrl.redraw();
},
@ -101,22 +102,22 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
ctrl.setBerserk(color);
},
gone(isGone) {
if (!ctrl.data.opponent.ai) {
game.setIsGone(ctrl.data, ctrl.data.opponent.color, isGone);
if (!d.opponent.ai) {
game.setIsGone(d, d.opponent.color, isGone);
ctrl.redraw();
}
},
checkCount(e) {
ctrl.data.player.checks = ctrl.data.player.color == 'white' ? e.white : e.black;
ctrl.data.opponent.checks = ctrl.data.opponent.color == 'white' ? e.white : e.black;
d.player.checks = d.player.color == 'white' ? e.white : e.black;
d.opponent.checks = d.opponent.color == 'white' ? e.white : e.black;
ctrl.redraw();
},
simulPlayerMove(gameId: string) {
if (
ctrl.opts.userId &&
ctrl.data.simul &&
ctrl.opts.userId == ctrl.data.simul.hostId &&
gameId !== ctrl.data.game.id &&
d.simul &&
ctrl.opts.userId == d.simul.hostId &&
gameId !== d.game.id &&
ctrl.moveOn.get() &&
ctrl.chessground.state.turnColor !== ctrl.chessground.state.movable.color) {
ctrl.setRedirecting();
@ -140,7 +141,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket {
send,
handlers,
moreTime: throttle(300, () => send('moretime')),
outoftime: throttle(500, () => send('flag', ctrl.data.game.player)),
outoftime: throttle(500, () => send('flag', d.game.player)),
berserk: throttle(200, () => send('berserk', null, { ackable: true })),
sendLoading(typ: string, data?: any) {
ctrl.setLoading(true);