progress on chessground based rounds

This commit is contained in:
Thibault Duplessis 2014-10-03 10:10:12 +02:00
parent b3232bc84f
commit 18e428c1e8
18 changed files with 308 additions and 69 deletions

View file

@ -56,11 +56,10 @@ object Round extends LilaController with TheftPrevention {
html = pov.game.started.fold(
PreventTheft(pov) {
(pov.game.tournamentId ?? TournamentRepo.byId) zip
Env.game.crosstableApi(pov.game) zip
(pov.game.playable ?? env.takebacker.isAllowedByPrefs(pov.game)) flatMap {
case ((tour, crosstable), takebackable) =>
Env.game.crosstableApi(pov.game) flatMap {
case (tour, crosstable) =>
env.jsonView.playerJson(pov, ctx.pref, Env.api.version, ctx.me) map { data =>
Ok(html.round.player(pov, data, tour = tour, cross = crosstable, takebackable = takebackable))
Ok(html.round.player(pov, data, tour = tour, cross = crosstable))
}
}
},

View file

@ -1,4 +1,4 @@
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.Tournament], cross: Option[lila.game.Crosstable], takebackable: Boolean)(implicit ctx: Context)
@(pov: Pov, data: play.api.libs.json.JsObject, tour: Option[lila.tournament.Tournament], cross: Option[lila.game.Crosstable])(implicit ctx: Context)
@import pov._
@ -10,7 +10,8 @@
*@
@jsAt(s"compiled/lichess.round.js")
@helper.javascriptRouter("roundRoutes")(
routes.javascript.Auth.signup
routes.javascript.Auth.signup,
routes.javascript.User.show
)(ctx.req)
@embedJs {
lichess = lichess || {};
@ -18,7 +19,29 @@ lichess.round = {
data: @Html(play.api.libs.json.Json.stringify(data)),
routes: roundRoutes.controllers,
i18n: @Html(J.stringify(i18nJsObject(
trans.premoveEnabledClickAnywhereToCancel)))
trans.premoveEnabledClickAnywhereToCancel,
trans.aiNameLevelAiLevel,
trans.waiting,
trans.yourTurn,
trans.abortGame,
trans.proposeATakeback,
trans.offerDraw,
trans.resign,
trans.theOtherPlayerHasLeftTheGameYouCanForceResignationOrWaitForHim,
trans.forceResignation,
trans.forceDraw,
trans.threefoldRepetition,
trans.claimADraw,
trans.drawOfferSent,
trans.cancel,
trans.yourOpponentOffersADraw,
trans.accept,
trans.decline,
trans.takebackPropositionSent,
trans.yourOpponentProposesATakeback,
trans.youHaveNbSecondsToMakeYourFirstMove,
trans.thisPlayerUsesChessComputerAssistance
)))
};
}
}

View file

@ -93,10 +93,6 @@ object Event {
override def owner = true
}
object Reload extends Empty {
def typ = "resync"
}
case class Promotion(role: PromotableRole, pos: Pos) extends Event {
def typ = "promotion"
def data = Json.obj(
@ -141,8 +137,12 @@ object Event {
case object ReloadTables extends Empty {
def typ = "reloadTable"
}
case object ReloadTablesOwner extends Empty {
def typ = "reloadTable"
case object Reload extends Empty {
def typ = "reload"
}
case object ReloadOwner extends Empty {
def typ = "reload"
override def owner = true
}

View file

@ -18,7 +18,7 @@ private[round] final class Drawer(messenger: Messenger, finisher: Finisher) {
case Pov(g, color) if (g playerCanOfferDraw color) => GameRepo save {
messenger.system(g, _.drawOfferSent)
Progress(g) map { g => g.updatePlayer(color, _ offerDraw g.turns) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case _ => fufail("[drawer] invalid yes " + pov)
}
@ -26,11 +26,11 @@ private[round] final class Drawer(messenger: Messenger, finisher: Finisher) {
case Pov(g, color) if pov.player.isOfferingDraw => GameRepo save {
messenger.system(g, _.drawOfferCanceled)
Progress(g) map { g => g.updatePlayer(color, _.removeDrawOffer) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case Pov(g, color) if pov.opponent.isOfferingDraw => GameRepo save {
messenger.system(g, _.drawOfferDeclined)
Progress(g) map { g => g.updatePlayer(!color, _.removeDrawOffer) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case _ => fufail("[drawer] invalid no " + pov)
}

View file

@ -159,6 +159,7 @@ final class Env(
chatApi = chatApi,
userJsonView = userJsonView,
getVersion = version,
canTakeback = takebacker.isAllowedByPrefs,
baseAnimationDuration = AnimationDuration)
{

View file

@ -17,6 +17,7 @@ final class JsonView(
chatApi: lila.chat.ChatApi,
userJsonView: lila.user.JsonView,
getVersion: String => Fu[Int],
canTakeback: Game => Fu[Boolean],
baseAnimationDuration: Duration) {
def playerJson(
@ -26,8 +27,9 @@ final class JsonView(
playerUser: Option[User]): Fu[JsObject] =
getVersion(pov.game.id) zip
(pov.opponent.userId ?? UserRepo.byId) zip
canTakeback(pov.game) zip
getChat(pov.game, playerUser) map {
case ((version, opponentUser), chat) =>
case (((version, opponentUser), takebackable), chat) =>
import pov._
Json.obj(
"game" -> Json.obj(
@ -66,7 +68,8 @@ final class JsonView(
"isProposingTakeback" -> opponent.isProposingTakeback.option(true)
).noNull,
"url" -> Json.obj(
"socket" -> s"/$fullId/socket/v$apiVersion"
"socket" -> s"/$fullId/socket/v$apiVersion",
"round" -> s"/$fullId"
),
"pref" -> Json.obj(
"animationDuration" -> animationDuration(pov, pref),
@ -90,7 +93,8 @@ final class JsonView(
},
"possibleMoves" -> possibleMoves(pov),
"tournamentId" -> game.tournamentId,
"poolId" -> game.poolId)
"poolId" -> game.poolId,
"takebackable" -> takebackable)
}
def watcherJson(pov: Pov, version: Int, tv: Boolean, pref: Pref) = {

View file

@ -31,11 +31,11 @@ private[round] final class Rematcher(
case Pov(game, color) if pov.player.isOfferingRematch => GameRepo save {
messenger.system(game, _.rematchOfferCanceled)
Progress(game) map { g => g.updatePlayer(color, _.removeRematchOffer) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case Pov(game, color) if pov.opponent.isOfferingRematch => GameRepo save {
messenger.system(game, _.rematchOfferDeclined)
Progress(game) map { g => g.updatePlayer(!color, _.removeRematchOffer) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case _ => ClientErrorException.future("[rematcher] invalid no " + pov)
}
@ -61,7 +61,7 @@ private[round] final class Rematcher(
private def rematchCreate(pov: Pov): Fu[Events] = GameRepo save {
messenger.system(pov.game, _.rematchOfferSent)
Progress(pov.game) map { g => g.updatePlayer(pov.color, _ offerRematch) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
private def returnGame(pov: Pov): Fu[Game] = for {
pieces pov.game.variant.standard.fold(

View file

@ -15,7 +15,7 @@ private[round] final class Takebacker(
case Pov(game, color) if (game playerCanProposeTakeback color) => GameRepo save {
messenger.system(game, _.takebackPropositionSent)
Progress(game) map { g => g.updatePlayer(color, _.proposeTakeback) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case _ => ClientErrorException.future("[takebacker] invalid yes " + pov)
}
}
@ -25,11 +25,11 @@ private[round] final class Takebacker(
case Pov(game, color) if pov.player.isProposingTakeback => GameRepo save {
messenger.system(game, _.takebackPropositionCanceled)
Progress(game) map { g => g.updatePlayer(color, _.removeTakebackProposition) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case Pov(game, color) if pov.opponent.isProposingTakeback => GameRepo save {
messenger.system(game, _.takebackPropositionDeclined)
Progress(game) map { g => g.updatePlayer(!color, _.removeTakebackProposition) }
} inject List(Event.ReloadTablesOwner)
} inject List(Event.ReloadOwner)
case _ => ClientErrorException.future("[takebacker] invalid no " + pov)
}
}

View file

@ -709,14 +709,13 @@ div.lichess_control .lichess_rematch.disabled {
}
#claim_draw_zone,
div.force_resign_zone,
div.proposed_takeback,
div.offered_draw {
div.negociation {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #ddd;
}
#claim_draw_zone .button,
div.offered_draw .button,
div.negociation .button,
div.force_resign_zone .button {
display: inline-block;
}
@ -742,7 +741,9 @@ div.lichess_current_player div.lichess_player {
position: absolute;
margin-left: -5px;
}
div.lichess_current_player div.lichess_player div.piece {
div.lichess_current_player div.lichess_player div.cg-piece {
width: 64px;
height: 64px;
top: 0;
left: 0;
}

View file

@ -126,8 +126,7 @@ body.dark #hooks_list .pool_buttons > a,
body.dark #hooks_list .pool_title,
body.dark div.training div.box,
body.dark div.force_resign_zone,
body.dark div.proposed_takeback,
body.dark div.offered_draw {
body.dark div.negotiation {
border-color: #3d3d3d;
}
body.dark #crosstable td.last {

View file

@ -30,7 +30,7 @@
"watchify": "^1.0.2"
},
"dependencies": {
"chessground": "git://github.com/ornicar/chessground.git",
"chessground": "^1.4.0",
"lodash-node": "^2.4.1",
"mithril": "^0.1.22"
}

View file

@ -1,4 +1,4 @@
module.exports = function(data) {
module.exports = function(data, onFlag) {
var lastUpdate;
@ -22,7 +22,8 @@ module.exports = function(data) {
this.tick = function(color) {
m.startComputation();
this.data[color] = lastUpdate[color] - (new Date() - lastUpdate.at) / 1000;
this.data[color] = Math.max(0, lastUpdate[color] - (new Date() - lastUpdate.at) / 1000);
if (this.data[color] === 0) onFlag();
m.endComputation();
}.bind(this);
}

View file

@ -1,11 +1,12 @@
var m = require('mithril');
var partial = require('lodash-node/modern/functions/partial');
var throttle = require('lodash-node/modern/functions/throttle');
var chessground = require('chessground');
var data = require('./data');
var round = require('./round');
var ground = require('./ground');
var socket = require('./socket');
var clockCtrl = require('./clock/ctrl');
var util = require('./util');
module.exports = function(cfg, router, i18n, socketSend) {
@ -24,36 +25,17 @@ module.exports = function(cfg, router, i18n, socketSend) {
});
}.bind(this);
this.chessground = new chessground.controller({
fen: cfg.game.fen,
orientation: this.data.player.color,
turnColor: this.data.game.player,
lastMove: util.str2move(this.data.game.lastMove),
highlight: {
lastMove: this.data.pref.highlight,
check: this.data.pref.highlight,
dragOver: true
},
movable: {
free: false,
color: round.isPlayerPlaying(this.data) ? this.data.player.color : null,
dests: round.parsePossibleMoves(this.data.possibleMoves),
showDests: this.data.pref.destination,
events: {
after: this.userMove
},
},
animation: {
enabled: true,
duration: this.data.pref.animationDuration
},
premovable: {
enabled: this.data.pref.enablePremove,
showDests: this.data.pref.destination
}
});
this.chessground = ground.make(this.data, cfg.game.fen, this.userMove);
this.clock = this.data.clock ? new clockCtrl(this.data.clock) : false;
this.reload = function(cfg) {
this.data = data(cfg);
ground.reload(this.chessground, this.data, cfg.game.fen);
}.bind(this);
this.clock = this.data.clock ? new clockCtrl(
this.data.clock,
throttle(partial(this.socket.send, 'outoftime'), 500)
) : false;
this.isClockRunning = function() {
return !this.data.game.finished && ((this.data.game.turns - this.data.game.startedAtTurn) > 1 || this.data.game.clockRunning);

53
ui/round/src/ground.js Normal file
View file

@ -0,0 +1,53 @@
var chessground = require('chessground');
var round = require('./round');
var util = require('./util');
function makeConfig(data, fen) {
return {
fen: fen,
orientation: data.player.color,
turnColor: data.game.player,
lastMove: util.str2move(data.game.lastMove),
highlight: {
lastMove: data.pref.highlight,
check: data.pref.highlight,
dragOver: true
},
movable: {
free: false,
color: round.isPlayerPlaying(data) ? data.player.color : null,
dests: round.parsePossibleMoves(data.possibleMoves),
showDests: data.pref.destination
},
animation: {
enabled: true,
duration: data.pref.animationDuration
},
premovable: {
enabled: data.pref.enablePremove,
showDests: data.pref.destination
}
};
}
function make(data, fen, userMove) {
var config = makeConfig(data, fen);
config.movable.events = {
after: userMove
};
return new chessground.controller(config);
}
function reload(ground, data, fen) {
ground.set(makeConfig(data, fen));
}
function end(ground) {
ground.stop();
}
module.exports = {
make: make,
reload: reload,
end: end
};

View file

@ -14,8 +14,42 @@ function parsePossibleMoves(possibleMoves) {
});
}
function playable(data) {
return data.game.started && !data.game.finished;
}
function mandatory(data) {
return data.tournamentId || data.poolId;
}
function abortable(data) {
return playable(data) && data.game.turns < 2 && !mandatory(data);
}
function takebackable(data) {
return playable(data) && data.takebackable && !data.tournamentId && data.game.turns > 1 && !data.player.isProposingTakeback && !data.opponent.isProposingTakeback;
}
function drawable(data) {
return playable(data) && data.game.turns >= 2 && !data.player.isOfferingDraw && !data.opponent.ai;
}
function resignable(data) {
return playable(data) && !abortable(data);
}
function getPlayer(data, color) {
return data.player.color == color ? data.player : data.opponent;
}
module.exports = {
isGamePlaying: isGamePlaying,
isPlayerPlaying: isPlayerPlaying,
playable: playable,
abortable: abortable,
takebackable: takebackable,
drawable: drawable,
resignable: resignable,
getPlayer: getPlayer,
parsePossibleMoves: parsePossibleMoves
};

View file

@ -1,6 +1,7 @@
var m = require('mithril');
var round = require('./round');
var ground = require('chessground');
var ground = require('./ground');
var xhr = require('./xhr');
module.exports = function(send, ctrl) {
@ -8,14 +9,14 @@ module.exports = function(send, ctrl) {
var handlers = {
possibleMoves: function(o) {
ctrl.chessground.reconfigure({
ctrl.chessground.set({
movable: {
dests: round.parsePossibleMoves(o)
}
});
},
state: function(o) {
ctrl.chessground.reconfigure({
ctrl.chessground.set({
turnColor: o.color
});
ctrl.data.game.player = o.color;
@ -42,7 +43,7 @@ module.exports = function(send, ctrl) {
ctrl.chessground.setPieces(pieces);
},
check: function(o) {
ctrl.chessground.reconfigure({
ctrl.chessground.set({
check: o
});
},
@ -58,11 +59,30 @@ module.exports = function(send, ctrl) {
$.redirect(o);
}, 400);
},
reload: function(o) {
xhr.reload(ctrl.data).then(ctrl.reload);
},
threefoldRepetition: function() {
// ???
},
clock: function(o) {
if (ctrl.clock) ctrl.clock.update(o.white, o.black);
},
crowd: function(o) {
m.startComputation();
['white', 'black'].forEach(function(c) {
round.getPlayer(ctrl.data, c).statused = true;
round.getPlayer(ctrl.data, c).connected = o[c];
});
ctrl.data.watchers = o.watchers;
m.endComputation();
},
end: function() {
m.startComputation();
ctrl.data.game.finished = true;
m.endComputation();
ground.end(ctrl.chessground);
xhr.reload(ctrl.data).then(ctrl.reload);
}
};

View file

@ -1,14 +1,112 @@
var map = require('lodash-node/modern/collections/map');
var chessground = require('chessground');
var round = require('./round');
var opposite = chessground.util.opposite;
var classSet = chessground.util.classSet;
var partial = chessground.util.partial;
var clockView = require('./clock/view');
var m = require('mithril');
function renderOpponent(ctrl) {
var op = ctrl.data.opponent;
return op.ai ? m('div.username.connected.statused',
ctrl.trans('aiNameLevelAiLevel', 'Stockfish', op.ai)
) : m('div', {
class: 'username ' + op.color + ' ' + classSet({
'statused': op.statused,
'connected': op.connected,
'offline': !op.connected
})
},
op.user ? [
m('a', {
class: 'user_link ulpt',
href: ctrl.router.User.show(op.user.username).url,
target: round.playable(ctrl.data) ? '_blank' : null,
'data-icon': 'r',
}, [
(op.user.title ? op.user.title + ' ' : '') + op.user.username,
op.engine ? m('span[data-icon=j]', {
title: ctrl.trans('thisPlayerUsesChessComputerAssistance')
}) : null
]),
m('span.status')
] : m('span.user_link', [
'Anonymous',
m('span.status')
])
);
}
function renderTableEnd(ctrl) {}
function renderButton(ctrl, condition, icon, hint, socketMsg) {
return condition(ctrl.data) ? m('button', {
class: 'button hint--bottom',
'data-hint': ctrl.trans(hint),
onclick: partial(ctrl.socket.send, socketMsg, null)
}, m('span[data-icon=' + icon + ']')) : null;
}
function renderTablePlay(ctrl) {
var d = ctrl.data;
return [
m('div.lichess_current_player',
m('div.lichess_player', [
m('div.cg-piece.king.' + d.game.player),
m('p', ctrl.trans(d.game.player == d.player.color ? 'yourTurn' : 'waiting'))
])
),
m('div.lichess_control.icons', [
renderButton(ctrl, round.abortable, 'L', 'abortGame', 'abort'),
renderButton(ctrl, round.takebackable, 'i', 'proposeATakeback', 'takeback-yes'),
renderButton(ctrl, round.drawable, '2', 'offerDraw', 'draw-yes'),
renderButton(ctrl, round.resignable, 'b', 'resign', 'resign')
]),
d.player.isOfferingDraw ? m('div.negociation', [
ctrl.trans('drawOfferSent') + ' ',
m('a', {
onclick: partial(ctrl.socket.send, 'draw-no', null)
}, ctrl.trans('cancel'))
]) : null,
d.opponent.isOfferingDraw ? m('div.negociation', [
ctrl.trans('yourOpponentOffersADraw'),
m('br'),
m('a.button[data-icon=E]', {
onclick: partial(ctrl.socket.send, 'draw-yes', null)
}, ctrl.trans('accept')),
m.trust('&nbsp;'),
m('a.button[data-icon=L]', {
onclick: partial(ctrl.socket.send, 'draw-no', null)
}, ctrl.trans('decline')),
]) : null,
d.player.isProposingTakeback ? m('div.negociation', [
ctrl.trans('takebackPropositionSent') + ' ',
m('a', {
onclick: partial(ctrl.socket.send, 'takeback-no', null)
}, ctrl.trans('cancel'))
]) : null,
d.opponent.isProposingTakeback ? m('div.negociation', [
ctrl.trans('yourOpponentProposesATakeback'),
m('br'),
m('a.button[data-icon=E]', {
onclick: partial(ctrl.socket.send, 'takeback-yes', null)
}, ctrl.trans('accept')),
m.trust('&nbsp;'),
m('a.button[data-icon=L]', {
onclick: partial(ctrl.socket.send, 'takeback-no', null)
}, ctrl.trans('decline')),
]) : null,
];
}
module.exports = function(ctrl) {
var clockRunningColor = ctrl.isClockRunning() ? ctrl.data.game.player : null;
return m('div', {
config: function(el, isUpdate, context) {
if (isUpdate) return;
$('body').trigger('lichess.content_loaded');
},
class: 'lichess_game clearfix not_spectator pov_' + ctrl.data.player.color
}, [
ctrl.data.blindMode ? m('div#lichess_board_blind') : null,
@ -24,7 +122,13 @@ module.exports = function(ctrl) {
'table_with_clock': ctrl.clock,
'finished': ctrl.data.game.finished
})
}), (ctrl.clock && !ctrl.data.blindMode) ? clockView(ctrl.clock, ctrl.data.player.color, "bottom", clockRunningColor) : null,
}, [
m('div.lichess_opponent', renderOpponent(ctrl)),
m('div.lichess_separator'),
m('div.table_inner',
round.playable(ctrl.data) ? renderTablePlay(ctrl) : renderTableEnd(ctrl)
)
]), (ctrl.clock && !ctrl.data.blindMode) ? clockView(ctrl.clock, ctrl.data.player.color, "bottom", clockRunningColor) : null,
])
)
]);

18
ui/round/src/xhr.js Normal file
View file

@ -0,0 +1,18 @@
var m = require('mithril');
var xhrConfig = function(xhr) {
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Accept', 'application/vnd.lichess.v1+json');
}
function reload(data) {
return m.request({
method: 'GET',
url: data.url.round,
config: xhrConfig
});
}
module.exports = {
reload: reload
};