fully type ui/round

typesafeRound
Thibault Duplessis 2017-07-08 23:09:41 +02:00
parent 85538b2003
commit 93c9be0989
20 changed files with 339 additions and 262 deletions

View File

@ -83,9 +83,8 @@ object JsonView {
"games" -> o.nb,
"rating" -> o.glicko.rating.toInt,
"rd" -> o.glicko.deviation.toInt,
"prov" -> o.glicko.provisional,
"prog" -> o.progress
)
).add("prov" -> o.glicko.provisional)
}
private val standardPerfKeys: Set[Perf.Key] = PerfType.standard.map(_.key)(scala.collection.breakOut)

View File

@ -71,10 +71,10 @@ export function replayable(data: GameData): boolean {
(status.aborted(data) && bothPlayersHavePlayed(data));
}
export function getPlayer(data: GameData, color: Color): Player;
export function getPlayer(data: GameData, color: Color | undefined): Player;
export function getPlayer(data: GameData, color?: Color): Player | null {
if (data.player.color == color) return data.player;
if (data.opponent.color == color) return data.opponent;
if (data.player.color === color) return data.player;
if (data.opponent.color === color) return data.opponent;
return null;
}

View File

@ -26,6 +26,7 @@ export interface Game {
boosted?: boolean;
rematch?: string;
rated?: boolean;
perf: string;
}
export interface Status {
@ -42,7 +43,7 @@ export type StatusId = number;
export interface Player {
id: string;
name: string;
user: User;
user?: PlayerUser;
spectator?: boolean;
color: Color;
proposingTakeback?: boolean;
@ -55,6 +56,10 @@ export interface Player {
hold?: Hold;
ratingDiff?: number;
checks?: number;
rating?: number;
provisional?: string;
engine?: boolean;
berserk?: boolean;
}
export interface TournamentRanks {
@ -63,8 +68,11 @@ export interface TournamentRanks {
}
export interface Tournament {
id: string;
berserkable: boolean;
ranks?: TournamentRanks;
running?: boolean;
nbSecondsForFirstMove?: number;
}
export interface Simul {
@ -79,9 +87,22 @@ export interface Clock {
export type Source = 'import' | 'lobby' | 'pool';
export interface User {
export interface PlayerUser {
online: boolean;
username: string;
patron?: boolean;
title?: string;
perfs: {
[key: string]: Perf;
}
}
export interface Perf {
games: number;
rating: number;
rd: number;
prog: number;
prov?: boolean;
}
export interface Ctrl {

View File

@ -19,6 +19,7 @@ export interface ClockData {
emerg: Seconds;
showTenths: 0 | 1 | 2;
showBar: boolean;
moretime: number;
}
interface Times {

View File

@ -5,8 +5,7 @@ import { game } from 'game';
import RoundController from '../ctrl';
import { ClockController, ClockData, Millis } from './clockCtrl';
import { Player } from 'game';
type Position = 'top' | 'bottom';
import { Position } from '../interfaces';
export function renderClock(ctrl: RoundController, player: Player, position: Position) {
const clock = ctrl.clock!,

View File

@ -6,6 +6,7 @@ export interface CorresClockData {
white: Seconds;
black: Seconds;
emerg: Seconds;
showBar: boolean;
}
export interface CorresClockController {

View File

@ -1,21 +1,24 @@
import { h } from 'snabbdom'
import { Millis } from '../clock/clockCtrl';
import { Position } from '../interfaces';
import { CorresClockController } from './corresClockCtrl';
function prefixInteger(num, length) {
function prefixInteger(num: number, length: number): string {
return (num / Math.pow(10, length)).toFixed(length).substr(2);
}
function bold(x) {
function bold(x: string) {
return '<b>' + x + '</b>';
}
function formatClockTime(trans, time) {
var date = new Date(time);
var minutes = prefixInteger(date.getUTCMinutes(), 2);
var seconds = prefixInteger(date.getSeconds(), 2);
var hours, str = '';
function formatClockTime(trans: Trans, time: Millis) {
const date = new Date(time),
minutes = prefixInteger(date.getUTCMinutes(), 2),
seconds = prefixInteger(date.getSeconds(), 2);
let hours: number, str = '';
if (time >= 86400 * 1000) {
// days : hours
var days = date.getUTCDate() - 1;
const days = date.getUTCDate() - 1;
hours = date.getUTCHours();
str += (days === 1 ? trans('oneDay') : trans('nbDays', days)) + ' ';
if (hours !== 0) str += trans('nbHours', hours);
@ -30,7 +33,7 @@ function formatClockTime(trans, time) {
return str;
}
export default function(ctrl, trans, color, position, runningColor) {
export default function(ctrl: CorresClockController, trans: Trans, color: Color, position: Position, runningColor: Color) {
const millis = ctrl.millisOf(color);
const update = (el: HTMLElement) => {
el.innerHTML = formatClockTime(trans, millis);

View File

@ -2,21 +2,22 @@ import { game } from 'game';
import { dragNewPiece } from 'chessground/drag';
import RoundController from '../ctrl';
import * as cg from 'chessground/types';
import { RoundData } from '../interfaces';
export function drag(ctrl: RoundController, e: cg.MouchEvent) {
export function drag(ctrl: RoundController, e: cg.MouchEvent): void {
if (e.button !== undefined && e.button !== 0) return; // only touch or left click
if (ctrl.replaying() || !game.isPlayerPlaying(ctrl.data)) return;
const el = e.target as HTMLElement,
role = el.getAttribute('data-role'),
color = el.getAttribute('data-color'),
number = el.getAttribute('data-nb');
role = el.getAttribute('data-role') as cg.Role,
color = el.getAttribute('data-color') as cg.Color,
number = el.getAttribute('data-nb');
if (!role || !color || number === '0') return;
e.stopPropagation();
e.preventDefault();
dragNewPiece(ctrl.chessground.state, { color, role }, e);
}
export function valid(data, role, key) {
export function valid(data: RoundData, role: cg.Role, key: cg.Key): boolean {
if (!game.isPlayerTurn(data)) return false;

View File

@ -70,7 +70,7 @@ import {
constructor(opts: RoundOpts, redraw: Redraw) {
const d = round.merge({}, opts.data).data;
const d = round.merge({} as RoundData, opts.data).data;
this.opts = opts;
this.data = d;
@ -427,7 +427,7 @@ import {
d = merged.data;
this.data = d;
this.clearVmJust();
if (this.clock) this.clock.update(d.clock.white, d.clock.black);
if (this.clock) this.clock.update(d.clock!.white, d.clock!.black);
if (this.corresClock) this.corresClock.update(d.correspondence.white, d.correspondence.black);
if (!this.replaying()) ground.reload(this);
this.setTitle();
@ -529,7 +529,7 @@ import {
if (this.resignConfirm) {
if (v) this.socket.sendLoading('resign');
else this.resignConfirm = false;
} else if (v !== false) {
} else if (v) {
if (this.data.pref.confirmResign) this.resignConfirm = true;
else this.socket.sendLoading('resign');
}

View File

@ -1,5 +1,6 @@
import { VNode } from 'snabbdom/vnode';
import { GameData, Status } from 'game';
import { ClockData } from './clock/clockCtrl';
import { CorresClockData } from './corresClock/corresClockCtrl';
import * as cg from 'chessground/types';
@ -28,6 +29,7 @@ export interface SocketDrop {
}
export interface RoundData extends GameData {
clock?: ClockData;
pref: Pref;
steps: Step[];
possibleMoves?: { [key: string]: string };
@ -40,6 +42,12 @@ export interface RoundData extends GameData {
round: string;
},
blind?: boolean;
tv?: Tv;
}
export interface Tv {
channel: string;
flip: boolean;
}
interface CrazyData {
@ -144,3 +152,5 @@ export interface MoveMetadata {
justDropped?: cg.Role;
justCaptured?: cg.Piece;
}
export type Position = 'top' | 'bottom';

View File

@ -1,17 +1,24 @@
import { h } from 'snabbdom'
import { Api as ChessgroundApi } from 'chessground/api';
import * as cg from 'chessground/types';
import { Step, Untyped, Redraw } from './interfaces';
import RoundController from './ctrl';
import { Step, Redraw } from './interfaces';
export interface KeyboardMove extends Untyped {
export interface KeyboardMove {
update(step: Step): void;
registerHandler(h): void
hasFocus(): boolean;
setFocus(v: boolean): void;
san(orig: cg.Key, dest: cg.Key): void;
select(key: cg.Key): void;
hasSelected(): cg.Key | undefined;
usedSan: boolean;
}
export function ctrl(cg: ChessgroundApi, step: Step, redraw: Redraw) {
export function ctrl(cg: ChessgroundApi, step: Step, redraw: Redraw): KeyboardMove {
let focus = false;
let handler;
let preHandlerBuffer = step.fen;
const select = function(key: cg.Key) {
const select = function(key: cg.Key): void {
if (cg.state.selected === key) cg.cancelMove();
else cg.selectSquare(key, true);
};
@ -42,7 +49,7 @@ export function ctrl(cg: ChessgroundApi, step: Step, redraw: Redraw) {
};
};
export function render(ctrl: RoundController) {
export function render(ctrl: KeyboardMove) {
return h('div.keyboard-move', [
h('input', {
attrs: {

View File

@ -1,26 +1,34 @@
import { h } from 'snabbdom'
import * as ground from './ground';
import * as cg from 'chessground/types';
import { DrawShape } from 'chessground/draw';
import xhr = require('./xhr');
import { key2pos } from 'chessground/util';
import { bind } from './util';
import { h } from 'snabbdom'
import RoundController from './ctrl';
let promoting: any | undefined;
interface Promoting {
move: [cg.Key, cg.Key];
pre: boolean;
meta: cg.MoveMetadata
}
let promoting: Promoting | undefined;
let prePromotionRole: cg.Role | undefined;
function sendPromotion(ctrl, orig, dest, role, meta) {
function sendPromotion(ctrl: RoundController, orig: cg.Key, dest: cg.Key, role: cg.Role, meta: cg.MoveMetadata): boolean {
ground.promote(ctrl.chessground, dest, role);
ctrl.sendMove(orig, dest, role, meta);
return true;
}
export function start(ctrl, orig, dest, meta) {
var d = ctrl.data;
var piece = ctrl.chessground.state.pieces[dest];
var premovePiece = ctrl.chessground.state.pieces[orig];
export function start(ctrl: RoundController, orig: cg.Key, dest: cg.Key, meta: cg.MoveMetadata): boolean {
const d = ctrl.data,
piece = ctrl.chessground.state.pieces[dest],
premovePiece = ctrl.chessground.state.pieces[orig];
if (((piece && piece.role === 'pawn') || (premovePiece && premovePiece.role === 'pawn')) && (
(dest[1] == 8 && d.player.color === 'white') ||
(dest[1] == 1 && d.player.color === 'black'))) {
(dest[1] === '8' && d.player.color === 'white') ||
(dest[1] === '1' && d.player.color === 'black'))) {
if (prePromotionRole && meta.premove) return sendPromotion(ctrl, orig, dest, prePromotionRole, meta);
if (!meta.ctrlKey && !promoting && (d.pref.autoQueen === 3 || (d.pref.autoQueen === 2 && premovePiece))) {
if (premovePiece) setPrePromotion(ctrl, dest, 'queen');
@ -38,7 +46,7 @@ export function start(ctrl, orig, dest, meta) {
return false;
}
function setPrePromotion(ctrl, dest, role) {
function setPrePromotion(ctrl: RoundController, dest: cg.Key, role: cg.Role): void {
prePromotionRole = role;
ctrl.chessground.setAutoShapes([{
orig: dest,
@ -46,11 +54,12 @@ function setPrePromotion(ctrl, dest, role) {
color: ctrl.data.player.color,
role,
opacity: 0.8
}
}]);
},
brush: ''
} as DrawShape]);
}
export function cancelPrePromotion(ctrl) {
export function cancelPrePromotion(ctrl: RoundController) {
if (prePromotionRole) {
ctrl.chessground.setAutoShapes([]);
prePromotionRole = undefined;
@ -58,7 +67,7 @@ export function cancelPrePromotion(ctrl) {
}
}
function finish(ctrl, role) {
function finish(ctrl: RoundController, role: cg.Role) {
if (promoting) {
const info = promoting;
promoting = undefined;
@ -67,14 +76,14 @@ function finish(ctrl, role) {
}
}
export function cancel(ctrl) {
export function cancel(ctrl: RoundController) {
cancelPrePromotion(ctrl);
ctrl.chessground.cancelPremove();
if (promoting) xhr.reload(ctrl).then(ctrl.reload);
promoting = undefined;
}
function renderPromotion(ctrl, dest, pieces, color, orientation) {
function renderPromotion(ctrl: RoundController, dest: cg.Key, roles: cg.Role[], color: Color, orientation: Color) {
var left = (8 - key2pos(dest)[0]) * 12.5;
if (orientation === 'white') left = 87.5 - left;
var vertical = color === orientation ? 'top' : 'bottom';
@ -90,7 +99,7 @@ function renderPromotion(ctrl, dest, pieces, color, orientation) {
});
}
}
}, pieces.map((serverRole, i) => {
}, roles.map((serverRole, i) => {
var top = (color === orientation ? i : 7 - i) * 12.5;
return h('square', {
attrs: {style: 'top: ' + top + '%;left: ' + left + '%'},
@ -104,12 +113,13 @@ function renderPromotion(ctrl, dest, pieces, color, orientation) {
}));
};
export function view(ctrl) {
if (!promoting) return;
var pieces = ['queen', 'knight', 'rook', 'bishop'];
if (ctrl.data.game.variant.key === 'antichess') pieces.push('king');
const roles: cg.Role[] = ['queen', 'knight', 'rook', 'bishop'];
return renderPromotion(ctrl, promoting.move[1], pieces,
export function view(ctrl: RoundController) {
if (!promoting) return;
return renderPromotion(ctrl, promoting.move[1],
ctrl.data.game.variant.key === 'antichess' ? roles.concat('king') : roles,
ctrl.data.player.color,
ctrl.chessground.state.orientation);
};

View File

@ -1,16 +1,21 @@
export function firstPly(d) {
import { RoundData, Step } from './interfaces';
export function firstPly(d: RoundData): number {
return d.steps[0].ply;
}
export function lastPly(d) {
export function lastPly(d: RoundData): number {
return d.steps[d.steps.length - 1].ply;
}
export function plyStep(d, ply) {
export function plyStep(d: RoundData, ply): Step {
return d.steps[ply - firstPly(d)];
}
export function merge(old, cfg) {
export function merge(old: RoundData, cfg: RoundData): {
data: RoundData;
changes: any;
} {
var data = cfg;
if (data.clock) {
@ -24,7 +29,7 @@ export function merge(old, cfg) {
if (['horde', 'crazyhouse'].indexOf(data.game.variant.key) !== -1)
data.pref.showCaptured = false;
var changes: any = {};
const changes: any = {};
if (old.opponent) {
if (!old.opponent.offeringDraw && cfg.opponent.offeringDraw)
changes.drawOffer = true;

View File

@ -17,9 +17,10 @@ const F = [
};
});
var tickerTimer;
let tickerTimer: number | undefined;
function resetTicker() {
tickerTimer = clearTimeout(tickerTimer);
if (tickerTimer) clearTimeout(tickerTimer);
tickerTimer = undefined;
F[0]();
}

View File

@ -1,8 +1,6 @@
import * as cg from 'chessground/types'
import { h } from 'snabbdom'
type Redraw = () => void
import * as cg from 'chessground/types'
import { Redraw } from './interfaces';
const pieceScores = {
pawn: 1,
@ -37,27 +35,27 @@ export function bind(eventName: string, f: (e: Event) => void, redraw: Redraw |
}
export function parsePossibleMoves(possibleMoves) {
if (!possibleMoves) return {};
for (var k in possibleMoves) {
for (let k in possibleMoves) {
if (typeof possibleMoves[k] === 'object') break;
possibleMoves[k] = possibleMoves[k].match(/.{2}/g);
}
return possibleMoves;
};
// {white: {pawn: 3 queen: 1}, black: {bishop: 2}}
export function getMaterialDiff(pieces) {
var counts = {
export function getMaterialDiff(pieces: cg.Pieces): cg.MaterialDiff {
let counts = {
king: 0,
queen: 0,
rook: 0,
bishop: 0,
knight: 0,
pawn: 0
}, p, role, c;
for (var k in pieces) {
}, p, role, c, k;
for (k in pieces) {
p = pieces[k];
counts[p.role] += (p.color === 'white' ? 1 : -1);
}
var diff = {
const diff = {
white: {},
black: {}
};
@ -68,9 +66,9 @@ export function getMaterialDiff(pieces) {
}
return diff;
};
export function getScore(pieces) {
var score = 0;
for (var k in pieces) {
export function getScore(pieces: cg.Pieces): number {
let score = 0, k;
for (k in pieces) {
score += pieceScores[pieces[k].role] * (pieces[k].color === 'white' ? 1 : -1);
}
return score;

View File

@ -1,32 +1,34 @@
import * as util from '../util';
import { game, status, router } from 'game';
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import * as util from '../util';
import { game, status, router } from 'game';
import { RoundData, MaybeVNodes } from '../interfaces';
import { ClockData } from '../clock/clockCtrl';
import RoundController from '../ctrl';
function analysisBoardOrientation(data) {
function analysisBoardOrientation(data: RoundData) {
return data.game.variant.key === 'racingKings' ? 'white' : data.player.color;
}
function poolUrl(clock) {
function poolUrl(clock: ClockData) {
return '/#pool/' + (clock.initial / 60) + '+' + clock.increment;
}
function analysisButton(ctrl) {
var d = ctrl.data;
var url = router.game(d, analysisBoardOrientation(d)) + '#' + ctrl.vm.ply;
function analysisButton(ctrl: RoundController): VNode | null {
const d = ctrl.data,
url = router.game(d, analysisBoardOrientation(d)) + '#' + ctrl.ply;
return game.replayable(d) ? h('a.button', {
attrs: { href: url },
hook: util.bind('click', () => {
hook: util.bind('click', _ => {
// force page load in case the URL is the same
if (location.pathname === url.split('#')[0]) location.reload();
})
}, ctrl.trans.noarg('analysis')) : null;
}
function rematchButtons(ctrl): Array<VNode | null> {
var d = ctrl.data;
var me = d.player.offeringRematch, them = d.opponent.offeringRematch;
function rematchButtons(ctrl): MaybeVNodes {
const d = ctrl.data,
me = d.player.offeringRematch, them = d.opponent.offeringRematch;
return [
them ? h('a.rematch-decline', {
attrs: {
@ -62,9 +64,16 @@ function rematchButtons(ctrl): Array<VNode | null> {
];
}
export function standard(ctrl, condition, icon, hint, socketMsg, onclick): VNode {
export function standard(
ctrl: RoundController,
condition: ((d: RoundData) => boolean) | undefined,
icon: string,
hint: string,
socketMsg: string,
onclick?: () => void
): VNode {
// disabled if condition callback is provided and is falsy
var enabled = function() {
const enabled = function() {
return !condition || condition(ctrl.data);
};
return h('button.fbt.hint--bottom.' + socketMsg, {
@ -72,18 +81,15 @@ export function standard(ctrl, condition, icon, hint, socketMsg, onclick): VNode
disabled: !enabled(),
'data-hint': ctrl.trans.noarg(hint)
},
hook: {
insert: vnode => {
(vnode.elm as HTMLElement).addEventListener('click', () => {
if (enabled()) onclick ? onclick() : ctrl.socket.sendLoading(socketMsg, null);
});
}
}
hook: util.bind('click', _ => {
if (enabled()) onclick ? onclick() : ctrl.socket.sendLoading(socketMsg);
})
}, [
h('span', { attrs: util.dataIcon(icon) })
]);
};
export function forceResign(ctrl) {
}
export function forceResign(ctrl: RoundController) {
return ctrl.forceResignable() ? h('div.suggestion', [
h('p', ctrl.trans.noarg('opponentLeftChoices')),
h('a.button', {
@ -93,8 +99,9 @@ export function forceResign(ctrl) {
hook: util.bind('click', () => ctrl.socket.sendLoading('draw-force'))
}, ctrl.trans.noarg('forceDraw'))
]) : null;
};
export function resignConfirm(ctrl): VNode {
}
export function resignConfirm(ctrl: RoundController): VNode {
return h('div.resign_confirm', [
h('button.fbt.no.hint--bottom', {
attrs: { 'data-hint': ctrl.trans.noarg('cancel') },
@ -105,21 +112,24 @@ export function resignConfirm(ctrl): VNode {
hook: util.bind('click', () => ctrl.resign(true))
}, [h('span', { attrs: util.dataIcon('b') })])
]);
};
export function threefoldClaimDraw(ctrl) {
}
export function threefoldClaimDraw(ctrl: RoundController) {
return ctrl.data.game.threefold ? h('div.suggestion', [
h('p', ctrl.trans('threefoldRepetition')),
h('a.button', {
hook: util.bind('click', () => ctrl.socket.sendLoading('draw-claim'))
}, ctrl.trans.noarg('claimADraw'))
]) : null;
};
export function cancelDrawOffer(ctrl) {
}
export function cancelDrawOffer(ctrl: RoundController) {
return ctrl.data.player.offeringDraw ? h('div.pending', [
h('p', ctrl.trans.noarg('drawOfferSent'))
]) : null;
};
export function answerOpponentDrawOffer(ctrl) {
}
export function answerOpponentDrawOffer(ctrl: RoundController) {
return ctrl.data.opponent.offeringDraw ? h('div.negotiation.draw', [
h('p', ctrl.trans.noarg('yourOpponentOffersADraw')),
h('a.accept', {
@ -137,16 +147,18 @@ export function answerOpponentDrawOffer(ctrl) {
hook: util.bind('click', () => ctrl.socket.sendLoading('draw-no'))
})
]) : null;
};
export function cancelTakebackProposition(ctrl) {
}
export function cancelTakebackProposition(ctrl: RoundController) {
return ctrl.data.player.proposingTakeback ? h('div.pending', [
h('p', ctrl.trans.noarg('takebackPropositionSent')),
h('a.button', {
hook: util.bind('click', () => ctrl.socket.sendLoading('takeback-no'))
}, ctrl.trans.noarg('cancel'))
]) : null;
};
export function answerOpponentTakebackProposition(ctrl) {
}
export function answerOpponentTakebackProposition(ctrl: RoundController) {
return ctrl.data.opponent.proposingTakeback ? h('div.negotiation.takeback', [
h('p', ctrl.trans.noarg('yourOpponentProposesATakeback')),
h('a.accept', {
@ -164,9 +176,10 @@ export function answerOpponentTakebackProposition(ctrl) {
hook: util.bind('click', () => ctrl.socket.sendLoading('takeback-no'))
})
]) : null;
};
export function submitMove(ctrl): VNode | undefined {
return (ctrl.vm.moveToSubmit || ctrl.vm.dropToSubmit) ? h('div.negotiation.move-confirm', [
}
export function submitMove(ctrl: RoundController): VNode | undefined {
return (ctrl.moveToSubmit || ctrl.dropToSubmit) ? h('div.negotiation.move-confirm', [
h('p', ctrl.trans.noarg('moveConfirmation')),
h('a.accept', {
attrs: {
@ -183,9 +196,10 @@ export function submitMove(ctrl): VNode | undefined {
hook: util.bind('click', () => ctrl.submitMove(false))
})
]) : undefined;
};
export function backToTournament(ctrl): VNode | undefined {
var d = ctrl.data;
}
export function backToTournament(ctrl: RoundController): VNode | undefined {
const d = ctrl.data;
return (d.tournament && d.tournament.running) ? h('div.follow_up', [
h('a.text.fbt.strong.glowed', {
attrs: {
@ -204,22 +218,24 @@ export function backToTournament(ctrl): VNode | undefined {
]),
analysisButton(ctrl)
]) : undefined;
};
export function moretime(ctrl) {
}
export function moretime(ctrl: RoundController) {
return game.moretimeable(ctrl.data) ? h('a.moretime.hint--bottom-left', {
attrs: { 'data-hint': ctrl.trans('giveNbSeconds', ctrl.data.clock.moretime) },
attrs: { 'data-hint': ctrl.trans('giveNbSeconds', ctrl.data.clock!.moretime) },
hook: util.bind('click', ctrl.socket.moreTime)
}, [
h('span', { attrs: util.dataIcon('O')})
]) : null;
};
export function followUp(ctrl): VNode {
var d = ctrl.data;
var rematchable = !d.game.rematch && (status.finished(d) || status.aborted(d)) && !d.tournament && !d.simul && !d.game.boosted && (d.opponent.onGame || (!d.clock && d.player.user && d.opponent.user));
var newable = (status.finished(d) || status.aborted(d)) && (
}
export function followUp(ctrl: RoundController): VNode {
const d = ctrl.data,
rematchable = !d.game.rematch && (status.finished(d) || status.aborted(d)) && !d.tournament && !d.simul && !d.game.boosted && (d.opponent.onGame || (!d.clock && d.player.user && d.opponent.user)),
newable = (status.finished(d) || status.aborted(d)) && (
d.game.source === 'lobby' ||
d.game.source === 'pool');
const rematchZone = ctrl.vm.challengeRematched ? [h('div.suggestion.text', {
d.game.source === 'pool'),
rematchZone = ctrl.challengeRematched ? [h('div.suggestion.text', {
attrs: util.dataIcon('j')
}, ctrl.trans.noarg('rematchOfferSent')
)] : (rematchable || d.game.rematch ? rematchButtons(ctrl) : []);
@ -229,18 +245,19 @@ export function followUp(ctrl): VNode {
attrs: {href: '/tournament/' + d.tournament.id}
}, ctrl.trans.noarg('viewTournament')) : null,
newable ? h('a.button', {
attrs: {href: d.game.source === 'pool' ? poolUrl(d.clock) : '/?hook_like=' + d.game.id },
attrs: {href: d.game.source === 'pool' ? poolUrl(d.clock!) : '/?hook_like=' + d.game.id },
}, ctrl.trans.noarg('newOpponent')) : null,
analysisButton(ctrl)
]);
};
export function watcherFollowUp(ctrl): VNode {
var d = ctrl.data;
}
export function watcherFollowUp(ctrl: RoundController): VNode {
const d = ctrl.data;
return h('div.follow_up', [
d.game.rematch ? h('a.button.text', {
attrs: {
'data-icon': 'v',
href: router.game(d.game.rematch, d.opponent.color)
href: `${d.game.rematch}/${d.opponent.color}`
}
}, ctrl.trans.noarg('viewRematch')) : null,
d.tournament ? h('a.button', {
@ -248,4 +265,4 @@ export function watcherFollowUp(ctrl): VNode {
}, ctrl.trans.noarg('viewTournament')) : null,
analysisButton(ctrl)
]);
};
}

View File

@ -1,34 +1,35 @@
import { game } from 'game';
import round = require('../round');
import table = require('./table');
import promotion = require('../promotion');
import ground = require('../ground');
import { read as fenRead } from 'chessground/fen';
import util = require('../util');
import blind = require('../blind');
import keyboard = require('../keyboard');
import crazyView from '../crazy/crazyView';
import { render as keyboardMove } from '../keyboardMove';
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { game } from 'game';
import { plyStep } from '../round';
import renderTable from './table';
import * as promotion from '../promotion';
import { render as renderGround } from '../ground';
import { read as fenRead } from 'chessground/fen';
import * as util from '../util';
import * as blind from '../blind';
import * as keyboard from '../keyboard';
import crazyView from '../crazy/crazyView';
import { render as keyboardMove } from '../keyboardMove';
import RoundController from '../ctrl';
import * as cg from 'chessground/types';
function renderMaterial(material, checks, score) {
function renderMaterial(material: cg.MaterialDiff, checks?: number, score?: number) {
var children: VNode[] = [];
if (score || score === 0)
children.push(h('score', score > 0 ? '+' + score : score));
for (var role in material) {
children.push(h('score', (score > 0 ? '+' : '') + score));
for (let role in material) {
const content: VNode[] = [];
for (var i = 0; i < material[role]; i++) content.push(h('mono-piece.' + role));
for (let i = 0; i < material[role]; i++) content.push(h('mono-piece.' + role));
children.push(h('tomb', content));
}
for (var i = 0; i < checks; i++) {
if (checks) for (let i = 0; i < checks; i++) {
children.push(h('tomb', h('mono-piece.king')));
}
return h('div.cemetery', children);
}
function wheel(ctrl, e) {
function wheel(ctrl: RoundController, e: WheelEvent): boolean {
if (game.isPlayerPlaying(ctrl.data)) return true;
e.preventDefault();
if (e.deltaY > 0) keyboard.next(ctrl);
@ -37,38 +38,38 @@ function wheel(ctrl, e) {
return false;
}
function visualBoard(ctrl) {
function visualBoard(ctrl: RoundController) {
return h('div.lichess_board_wrap', [
h('div.lichess_board.' + ctrl.data.game.variant.key + (ctrl.data.pref.blindfold ? '.blindfold' : ''), {
hook: util.bind('wheel', e => wheel(ctrl, e))
}, [ground.render(ctrl)]),
hook: util.bind('wheel', (e: WheelEvent) => wheel(ctrl, e))
}, [renderGround(ctrl)]),
promotion.view(ctrl)
]);
}
function blindBoard(ctrl) {
function blindBoard(ctrl: RoundController) {
return h('div.lichess_board_blind', [
h('div.textual', {
hook: {
insert: vnode => blind.init(vnode.elm, ctrl)
insert: vnode => blind.init(vnode.elm as HTMLElement, ctrl)
}
}, [ ground.render(ctrl) ])
}, [ renderGround(ctrl) ])
]);
}
var emptyMaterialDiff = {
white: [],
black: []
const emptyMaterialDiff: cg.MaterialDiff = {
white: {},
black: {}
};
export function main(ctrl: any): VNode {
export function main(ctrl: RoundController): VNode {
const d = ctrl.data,
cgState = ctrl.chessground && ctrl.chessground.state,
topColor = d[ctrl.vm.flip ? 'player' : 'opponent'].color,
bottomColor = d[ctrl.vm.flip ? 'opponent' : 'player'].color;
topColor = d[ctrl.flip ? 'player' : 'opponent'].color,
bottomColor = d[ctrl.flip ? 'opponent' : 'player'].color;
let material, score;
if (d.pref.showCaptured) {
var pieces = cgState ? cgState.pieces : fenRead(round.plyStep(ctrl.data, ctrl.vm.ply).fen);
var pieces = cgState ? cgState.pieces : fenRead(plyStep(ctrl.data, ctrl.ply).fen);
material = util.getMaterialDiff(pieces);
score = util.getScore(pieces) * (bottomColor === 'white' ? 1 : -1);
} else material = emptyMaterialDiff;
@ -81,7 +82,7 @@ export function main(ctrl: any): VNode {
d.blind ? blindBoard(ctrl) : visualBoard(ctrl),
h('div.lichess_ground', [
crazyView(ctrl, topColor, 'top') || renderMaterial(material[topColor], d.player.checks, undefined),
table.render(ctrl),
renderTable(ctrl),
crazyView(ctrl, bottomColor, 'bottom') || renderMaterial(material[bottomColor], d.opponent.checks, score)
])
]),

View File

@ -1,10 +1,11 @@
import { h } from 'snabbdom'
import { VNode, VNodeData } from 'snabbdom/vnode'
import * as round from '../round';
import { dropThrottle } from 'common';
import { game, status, router, view as gameView } from 'game';
import * as util from '../util';
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import RoundController from '../ctrl';
import { Step, MaybeVNodes, RoundData } from '../interfaces';
function emptyMove() {
return h('move.empty', '...');
@ -15,30 +16,30 @@ function nullMove() {
const scrollThrottle = dropThrottle(100);
function autoScroll(el, ctrl) {
function autoScroll(el: HTMLElement, ctrl: RoundController) {
scrollThrottle(function() {
if (ctrl.data.steps.length < 7) return;
let st;
if (ctrl.vm.ply < 3) st = 0;
else if (ctrl.vm.ply >= round.lastPly(ctrl.data) - 1) st = 9999;
let st: number | undefined = undefined;
if (ctrl.ply < 3) st = 0;
else if (ctrl.ply >= round.lastPly(ctrl.data) - 1) st = 9999;
else {
const plyEl = el.querySelector('.active');
const plyEl = el.querySelector('.active') as HTMLElement | undefined;
if (plyEl) st = plyEl.offsetTop - el.offsetHeight / 2 + plyEl.offsetHeight / 2;
}
if (st !== undefined) el.scrollTop = st;
});
}
function renderMove(step, curPly, orEmpty) {
function renderMove(step: Step, curPly: number, orEmpty?: boolean) {
if (!step) return orEmpty ? emptyMove() : nullMove();
var san = step.san[0] === 'P' ? step.san.slice(1) : step.san.replace('x', 'х');
const san = step.san[0] === 'P' ? step.san.slice(1) : step.san.replace('x', 'х');
return h('move', {
class: { active: step.ply === curPly }
}, san);
}
function renderResult(ctrl) {
var result;
function renderResult(ctrl: RoundController) {
let result;
if (status.finished(ctrl.data)) switch (ctrl.data.game.winner) {
case 'white':
result = '1-0';
@ -50,14 +51,14 @@ function renderResult(ctrl) {
result = '½-½';
}
if (result || status.aborted(ctrl.data)) {
var winner = game.getPlayer(ctrl.data, ctrl.data.game.winner);
const winner = game.getPlayer(ctrl.data, ctrl.data.game.winner);
return h('div.result_wrap', [
h('p.result', result),
h('p.status', {
hook: {
insert: _ => {
if (ctrl.vm.autoScroll) ctrl.vm.autoScroll();
else setTimeout(() => { ctrl.vm.autoScroll() }, 200);
if (ctrl.autoScroll) ctrl.autoScroll();
else setTimeout(() => ctrl.autoScroll(), 200);
}
}
}, [
@ -69,7 +70,7 @@ function renderResult(ctrl) {
return;
}
function renderMoves(ctrl) {
function renderMoves(ctrl: RoundController): MaybeVNodes {
const steps = ctrl.data.steps,
firstPly = round.firstPly(ctrl.data),
lastPly = round.lastPly(ctrl.data);
@ -81,10 +82,10 @@ function renderMoves(ctrl) {
pairs.push([null, steps[1]]);
startAt = 2;
}
for (var i = startAt; i < steps.length; i += 2) pairs.push([steps[i], steps[i + 1]]);
for (let i = startAt; i < steps.length; i += 2) pairs.push([steps[i], steps[i + 1]]);
const els: Array<VNode | undefined> = [], curPly = ctrl.vm.ply;
for (var i = 0; i < pairs.length; i++) {
const els: MaybeVNodes = [], curPly = ctrl.ply;
for (let i = 0; i < pairs.length; i++) {
els.push(h('index', i + 1 + ''));
els.push(renderMove(pairs[i][0], curPly, true));
els.push(renderMove(pairs[i][1], curPly, false));
@ -94,27 +95,27 @@ function renderMoves(ctrl) {
return els;
}
function analyseButton(ctrl) {
var showInfo = ctrl.forecastInfo();
var data: any = {
function analyseButton(ctrl: RoundController) {
const showInfo = ctrl.forecastInfo();
const data: VNodeData = {
class: {
'hint--top': !showInfo,
'hint--bottom': showInfo,
'glowed': showInfo,
'text': ctrl.data.forecastCount
'text': !!ctrl.data.forecastCount
},
attrs: {
'data-hint': ctrl.trans.noarg('analysis'),
href: router.game(ctrl.data, ctrl.data.player.color) + '/analysis#' + ctrl.vm.ply
href: router.game(ctrl.data, ctrl.data.player.color) + '/analysis#' + ctrl.ply
}
};
if (showInfo) data.hook = {
insert: vnode => {
insert(vnode) {
setTimeout(() => {
$(vnode.elm).powerTip({
$(vnode.elm as HTMLElement).powerTip({
closeDelay: 200,
placement: 'n'
}).data('powertipjq', $(vnode.elm).siblings('.forecast-info').clone().removeClass('none')).powerTip('show');
}).data('powertipjq', $(vnode.elm as HTMLElement).siblings('.forecast-info').clone().removeClass('none')).powerTip('show');
}, 1000);
}
};
@ -122,9 +123,9 @@ function analyseButton(ctrl) {
h('a.fbt.analysis', data, [
h('span', {
attrs: util.dataIcon('A'),
class: {text: ctrl.data.forecastCount}
class: {text: !!ctrl.data.forecastCount}
}),
ctrl.data.forecastCount
'' + ctrl.data.forecastCount
]),
showInfo ? h('div.forecast-info.info.none', [
h('strong.title.text', { attrs: util.dataIcon('') }, 'Speed up your game!'),
@ -133,10 +134,10 @@ function analyseButton(ctrl) {
];
}
function renderButtons(ctrl) {
var d = ctrl.data;
var firstPly = round.firstPly(d);
var lastPly = round.lastPly(d);
function renderButtons(ctrl: RoundController) {
const d = ctrl.data,
firstPly = round.firstPly(d),
lastPly = round.lastPly(d);
return h('div.buttons', {
hook: util.bind('mousedown', e => {
const target = e.target as HTMLElement;
@ -147,13 +148,13 @@ function renderButtons(ctrl) {
if (action === 'flip') {
if (d.tv) location.href = '/tv/' + d.tv.channel + (d.tv.flip ? '' : '?flip=1');
else if (d.player.spectator) location.href = router.game(d, d.opponent.color);
else ctrl.flip();
else ctrl.flipNow();
}
}
}, ctrl.redraw)
}, [
h('button.fbt.flip.hint--top', {
class: { active: ctrl.vm.flip },
class: { active: ctrl.flip },
attrs: {
'data-hint': ctrl.trans('flipBoard'),
'data-act': 'flip'
@ -163,11 +164,11 @@ function renderButtons(ctrl) {
]),
h('nav', [
['W', firstPly],
['Y', ctrl.vm.ply - 1],
['X', ctrl.vm.ply + 1],
['Y', ctrl.ply - 1],
['X', ctrl.ply + 1],
['V', lastPly]
].map((b, i) => {
const enabled = ctrl.vm.ply !== b[1] && b[1] >= firstPly && b[1] <= lastPly;
const enabled = ctrl.ply !== b[1] && b[1] >= firstPly && b[1] <= lastPly;
return h('button.fbt', {
class: { glowed: i === 3 && ctrl.isLate() },
attrs: {
@ -181,7 +182,7 @@ function renderButtons(ctrl) {
]);
}
function racingKingsInit(d) {
function racingKingsInit(d: RoundData) {
if (d.game.variant.key === 'racingKings' && d.game.turns === 0 && !d.player.spectator) {
const yourTurn = d.player.color === 'white' ? [h('br'), h('strong', "it's your turn!")] : [];
return h('div.message', {
@ -194,12 +195,12 @@ function racingKingsInit(d) {
return;
}
export function render(ctrl: any): VNode {
export default function(ctrl: RoundController): VNode {
return h('div.replay', [
renderButtons(ctrl),
racingKingsInit(ctrl.data) || (ctrl.replayEnabledByPref() ? h('div.moves', {
hook: {
insert: vnode => {
insert(vnode) {
(vnode.elm as HTMLElement).addEventListener('mousedown', e => {
let node = e.target as HTMLElement, offset = -2;
if (node.tagName !== 'MOVE') return;
@ -212,11 +213,11 @@ export function render(ctrl: any): VNode {
}
}
});
ctrl.vm.autoScroll = () => { autoScroll(vnode.elm, ctrl); };
ctrl.vm.autoScroll();
window.addEventListener('load', ctrl.vm.autoScroll);
ctrl.autoScroll = () => autoScroll(vnode.elm as HTMLElement, ctrl); ;
ctrl.autoScroll();
window.addEventListener('load', ctrl.autoScroll);
}
}
}, renderMoves(ctrl)) : renderResult(ctrl))
]);
};
}

View File

@ -1,27 +1,27 @@
import { game, status } from 'game';
import clockView = require('../clock/view');
import corresClockView from '../corresClock/view';
import replay = require('./replay');
import renderUser = require('./user');
import button = require('./button');
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { MaybeVNodes } from '../interfaces';
import { Position, MaybeVNodes } from '../interfaces';
import { game, status, Player } from 'game';
import { renderClock } from '../clock/clockView';
import renderCorresClock from '../corresClock/corresClockView';
import renderReplay from './replay';
import * as renderUser from './user';
import * as button from './button';
import RoundController from '../ctrl';
function playerAt(ctrl, position) {
return ctrl.vm.flip ^ ((position === 'top') as any) ? ctrl.data.opponent : ctrl.data.player;
function playerAt(ctrl: RoundController, position: Position) {
return (ctrl.flip as any) ^ ((position === 'top') as any) ? ctrl.data.opponent : ctrl.data.player;
}
function topPlayer(ctrl) {
function topPlayer(ctrl: RoundController) {
return playerAt(ctrl, 'top');
}
function bottomPlayer(ctrl) {
function bottomPlayer(ctrl: RoundController) {
return playerAt(ctrl, 'bottom');
}
function renderPlayer(ctrl, player) {
function renderPlayer(ctrl: RoundController, player: Player) {
return player.ai ? h('div.username.user_link.online', [
h('i.line'),
h('name', renderUser.aiName(ctrl, player))
@ -29,48 +29,48 @@ function renderPlayer(ctrl, player) {
renderUser.userHtml(ctrl, player);
}
function isLoading(ctrl) {
return ctrl.vm.loading || ctrl.vm.redirecting;
function isLoading(ctrl: RoundController): boolean {
return ctrl.loading || ctrl.redirecting;
}
function loader() { return h('span.ddloader'); }
function renderTableWith(ctrl, buttons: MaybeVNodes) {
function renderTableWith(ctrl: RoundController, buttons: MaybeVNodes) {
return [
replay.render(ctrl),
renderReplay(ctrl),
h('div.control.buttons', buttons),
renderPlayer(ctrl, bottomPlayer(ctrl))
];
}
function renderTableEnd(ctrl) {
function renderTableEnd(ctrl: RoundController) {
return renderTableWith(ctrl, [
isLoading(ctrl) ? loader() : (button.backToTournament(ctrl) || button.followUp(ctrl))
]);
}
function renderTableWatch(ctrl) {
function renderTableWatch(ctrl: RoundController) {
return renderTableWith(ctrl, [
isLoading(ctrl) ? loader() : button.watcherFollowUp(ctrl)
]);
}
function tournamentStartWarning(ctrl) {
function tournamentStartWarning(ctrl: RoundController) {
return h('div.suggestion', [
h('div.text', { attrs: {'data-icon': 'j'} },
ctrl.trans('youHaveNbSecondsToMakeYourFirstMove', ctrl.data.tournament.nbSecondsForFirstMove))
ctrl.trans('youHaveNbSecondsToMakeYourFirstMove', ctrl.data.tournament!.nbSecondsForFirstMove))
]);
}
function renderTablePlay(ctrl) {
function renderTablePlay(ctrl: RoundController) {
const d = ctrl.data;
const loading = isLoading(ctrl);
let submit = button.submitMove(ctrl);
let icons = (loading || submit) ? [] : [
game.abortable(d) ? button.standard(ctrl, null, 'L', 'abortGame', 'abort', null) :
game.abortable(d) ? button.standard(ctrl, undefined, 'L', 'abortGame', 'abort') :
button.standard(ctrl, game.takebackable, 'i', 'proposeATakeback', 'takeback-yes', ctrl.takebackYes),
button.standard(ctrl, ctrl.canOfferDraw, '2', 'offerDraw', 'draw-yes', ctrl.offerDraw),
ctrl.vm.resignConfirm ? button.resignConfirm(ctrl) : button.standard(ctrl, game.resignable, 'b', 'resign', 'resign-confirm', ctrl.resign)
ctrl.resignConfirm ? button.resignConfirm(ctrl) : button.standard(ctrl, game.resignable, 'b', 'resign', 'resign-confirm', () => ctrl.resign(true))
];
let buttons: MaybeVNodes = loading ? [loader()] : (submit ? [submit] : [
button.forceResign(ctrl),
@ -82,14 +82,14 @@ function renderTablePlay(ctrl) {
(d.tournament && game.nbMoves(d, d.player.color) === 0) ? tournamentStartWarning(ctrl) : null
]);
return [
replay.render(ctrl),
renderReplay(ctrl),
h('div.control.icons', icons),
h('div.control.buttons', buttons),
renderPlayer(ctrl, bottomPlayer(ctrl))
];
}
function whosTurn(ctrl, color) {
function whosTurn(ctrl: RoundController, color: Color) {
var d = ctrl.data;
if (status.finished(d) || status.aborted(d)) return;
return h('div.whos_turn',
@ -101,18 +101,18 @@ function whosTurn(ctrl, color) {
);
}
function anyClock(ctrl, position) {
function anyClock(ctrl: RoundController, position: Position) {
var player = playerAt(ctrl, position);
if (ctrl.clock) return clockView.renderClock(ctrl, player, position);
if (ctrl.clock) return renderClock(ctrl, player, position);
else if (ctrl.data.correspondence && ctrl.data.game.turns > 1)
return corresClockView(
ctrl.corresClock, ctrl.trans, player.color, position, ctrl.data.game.player
return renderCorresClock(
ctrl.corresClock!, ctrl.trans, player.color, position, ctrl.data.game.player
);
else return whosTurn(ctrl, player.color);
}
export function render(ctrl: any): VNode {
const contents: Array<VNode | string> = [
export default function(ctrl: RoundController): VNode {
const contents: MaybeVNodes = [
renderPlayer(ctrl, topPlayer(ctrl)),
h('div.table_inner',
ctrl.data.player.spectator ? renderTableWatch(ctrl) : (

View File

@ -1,25 +1,26 @@
import { game } from 'game';
import { h } from 'snabbdom'
import { VNode } from 'snabbdom/vnode'
import { game, Player } from 'game';
import RoundController from '../ctrl';
function ratingDiff(player) {
function ratingDiff(player: Player): VNode | undefined {
if (!player.ratingDiff) return;
if (player.ratingDiff === 0) return h('span.rp.null', '±0');
if (player.ratingDiff > 0) return h('span.rp.up', '+' + player.ratingDiff);
if (player.ratingDiff < 0) return h('span.rp.down', player.ratingDiff);
return;
if (player.ratingDiff < 0) return h('span.rp.down', '' + player.ratingDiff);
}
export function aiName(ctrl, player) {
export function aiName(ctrl: RoundController, player: Player) {
return ctrl.trans('aiNameLevelAiLevel', 'Stockfish', player.ai);
}
export function userHtml(ctrl, player) {
var d = ctrl.data;
var user = player.user;
var perf = user ? user.perfs[d.game.perf] : null;
var rating = player.rating ? player.rating : (perf && perf.rating);
export function userHtml(ctrl: RoundController, player: Player) {
const d = ctrl.data,
user = player.user,
perf = user ? user.perfs[d.game.perf] : null,
rating = player.rating ? player.rating : (perf && perf.rating);
if (user) {
var connecting = !player.onGame && ctrl.vm.firstSeconds && user.online;
const connecting = !player.onGame && ctrl.firstSeconds && user.online;
return h('div.username.user_link.' + player.color, {
class: {
online: player.onGame,
@ -39,7 +40,7 @@ export function userHtml(ctrl, player) {
href: '/@/' + user.username,
target: game.isPlayerPlaying(d) ? '_blank' : '_self'
}
}, user.title ? [h('span.title', user.title), ' ', user.username] : user.username),
}, user.title ? [h('span.title', user.title), ' ', user.username] : [user.username]),
rating ? h('rating', rating + (player.provisional ? '?' : '')) : null,
ratingDiff(player),
player.engine ? h('span', {
@ -50,7 +51,7 @@ export function userHtml(ctrl, player) {
}) : null
]);
}
var connecting = !player.onGame && ctrl.vm.firstSeconds;
const connecting = !player.onGame && ctrl.firstSeconds;
return h('div.username.user_link', {
class: {
online: player.onGame,
@ -65,14 +66,15 @@ export function userHtml(ctrl, player) {
}),
h('name', player.name || 'Anonymous')
]);
};
export function userTxt(ctrl, player) {
}
export function userTxt(ctrl: RoundController, player: Player) {
if (player.user) {
var perf = player.user.perfs[ctrl.data.game.perf];
var name = (player.user.title ? player.user.title + ' ' : '') + player.user.username;
var rating = player.rating ? player.rating : (perf ? perf.rating : null);
rating = rating ? ' (' + rating + (player.provisional ? '?' : '') + ')' : '';
return name + rating;
const perf = player.user.perfs[ctrl.data.game.perf],
name = (player.user.title ? player.user.title + ' ' : '') + player.user.username,
rating = player.rating ? player.rating : (perf ? perf.rating : null),
showRating = rating ? ' (' + rating + (player.provisional ? '?' : '') + ')' : '';
return name + showRating;
} else if (player.ai) return aiName(ctrl, player)
else return 'Anonymous';
};
}