Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 52 additions & 16 deletions modules/study/src/main/StudyMultiBoard.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package lila.study

import BSONHandlers.given
import chess.{ ByColor, Color, Outcome }
import chess.{ ByColor, Centis, Color, Outcome, Ply }
import chess.format.pgn.Tags
import chess.format.{ Fen, Uci }
import com.github.blemale.scaffeine.AsyncLoadingCache
Expand Down Expand Up @@ -67,13 +67,42 @@ final class StudyMultiBoard(
"lang" -> "js",
"args" -> $arr("$root", "$tags"),
"body" -> """function(root, tags) {
|tags = tags.filter(t => t.startsWith('White') || t.startsWith('Black') || t.startsWith('Result'));
|const node = tags.length ? Object.keys(root).reduce(([path, node], i) => (root[i].p > node.p && i.startsWith(path)) ? [i, root[i]] : [path, node], ['', root['_']])[1] : root['_'];
|return {node:{fen:node.f,uci:node.u},tags} }""".stripMargin
tags = tags.filter(t => t.startsWith('White') || t.startsWith('Black') || t.startsWith('Result'));
const [node, clockTicking] = tags.length ?
Object.keys(root).reduce(
([node, clockTicking, path, pathTicking], i) => {
if (root[i].p > node.p && i.startsWith(path)) {
clockTicking = node;
pathTicking = path;
node = root[i];
path = i;
} else if (clockTicking && root[i].p > clockTicking.p && i.startsWith(pathTicking)) {
clockTicking = root[i];
pathTicking = i;
}
return [node, clockTicking, path, pathTicking]
},
[root['_'], undefined, '', undefined]
).slice(0, 2) : [root['_'], undefined];
const [whiteClock, blackClock] = clockTicking ? node.f.includes(" b") ? [node.l, clockTicking.l] : [clockTicking.l, node.l] : [undefined, undefined]

return {
node: {
fen: node.f,
uci: node.u,
},
tags,
clocks: {
black: blackClock,
white: whiteClock,
}
}
}""".stripMargin
)
),
"orientation" -> "$setup.orientation",
"name" -> true
"name" -> true,
"lastMoveAt" -> "$relay.lastMoveAt"
)
)
)
Expand All @@ -84,18 +113,23 @@ final class StudyMultiBoard(
doc <- r
id <- doc.getAsOpt[StudyChapterId]("_id")
name <- doc.getAsOpt[StudyChapterName]("name")
comp <- doc.getAsOpt[Bdoc]("comp")
node <- comp.getAsOpt[Bdoc]("node")
fen <- node.getAsOpt[Fen.Epd]("fen")
lastMove = node.getAsOpt[Uci]("uci")
tags = comp.getAsOpt[Tags]("tags")
lastMoveAt = doc.getAsOpt[Instant]("lastMoveAt")
comp <- doc.getAsOpt[Bdoc]("comp")
node <- comp.getAsOpt[Bdoc]("node")
fen <- node.getAsOpt[Fen.Epd]("fen")
clocks <- comp.getAsOpt[Bdoc]("clocks")
lastMove = node.getAsOpt[Uci]("uci")
tags = comp.getAsOpt[Tags]("tags")
blackClock = clocks.getAsOpt[Centis]("black")
whiteClock = clocks.getAsOpt[Centis]("white")
yield ChapterPreview(
id = id,
name = name,
players = tags flatMap ChapterPreview.players,
players = tags flatMap ChapterPreview.players(blackClock = blackClock, whiteClock = whiteClock),
orientation = doc.getAsOpt[Color]("orientation") | Color.White,
fen = fen,
lastMove = lastMove,
lastMoveAt = lastMoveAt,
playing = lastMove.isDefined && tags.flatMap(_(_.Result)).has("*"),
outcome = tags.flatMap(_.outcome)
)
Expand All @@ -108,13 +142,14 @@ final class StudyMultiBoard(
.obj("name" -> p.name)
.add("title" -> p.title)
.add("rating" -> p.rating)
.add("clock" -> p.clock)
}

given Writes[ChapterPreview.Players] = Writes[ChapterPreview.Players] { players =>
Json.obj("white" -> players.white, "black" -> players.black)
}

given Writes[Outcome] = writeAs(_.toString)
given Writes[Outcome] = writeAs(_.toString.replace("1/2", "½"))

given Writes[ChapterPreview] = Json.writes

Expand All @@ -127,21 +162,22 @@ object StudyMultiBoard:
orientation: Color,
fen: Fen.Epd,
lastMove: Option[Uci],
lastMoveAt: Option[Instant],
playing: Boolean,
outcome: Option[Outcome]
)

object ChapterPreview:

case class Player(name: String, title: Option[String], rating: Option[Int])
case class Player(name: String, title: Option[String], rating: Option[Int], clock: Option[Centis])

type Players = ByColor[Player]

def players(tags: Tags): Option[Players] =
def players(blackClock: Option[Centis], whiteClock: Option[Centis])(tags: Tags): Option[Players] =
for
wName <- tags(_.White)
bName <- tags(_.Black)
yield ByColor(
white = Player(wName, tags(_.WhiteTitle), tags(_.WhiteElo).flatMap(_.toIntOption)),
black = Player(bName, tags(_.BlackTitle), tags(_.BlackElo).flatMap(_.toIntOption))
white = Player(wName, tags(_.WhiteTitle), tags(_.WhiteElo).flatMap(_.toIntOption), whiteClock),
black = Player(bName, tags(_.BlackTitle), tags(_.BlackElo).flatMap(_.toIntOption), blackClock)
)
5 changes: 3 additions & 2 deletions ui/analyse/src/explorer/explorerCtrl.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Prop, prop, defined } from 'common';
import { storedBooleanProp } from 'common/storage';
import { fenColor } from 'common/mini-game';
import debounce from 'common/debounce';
import { sync, Sync } from 'common/sync';
import { opposite } from 'chessground/util';
import * as xhr from './explorerXhr';
import { winnerOf, colorOf } from './explorerUtil';
import { winnerOf } from './explorerUtil';
import * as gameUtil from 'game';
import AnalyseCtrl from '../ctrl';
import { Hovering, ExplorerData, ExplorerDb, OpeningData, SimpleTablebaseHit, ExplorerOpts } from './interfaces';
Expand Down Expand Up @@ -217,7 +218,7 @@ export default class ExplorerCtrl {
return {
fen,
best: move && move.uci,
winner: res.checkmate ? opposite(colorOf(fen)) : res.stalemate ? undefined : winnerOf(fen, move!),
winner: res.checkmate ? opposite(fenColor(fen)) : res.stalemate ? undefined : winnerOf(fen, move!),
} as SimpleTablebaseHit;
};
}
7 changes: 2 additions & 5 deletions ui/analyse/src/explorer/explorerUtil.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { TablebaseMoveStats } from './interfaces';
import { opposite } from 'chessops/util';
import { fenColor } from 'common/mini-game';
import { VNode } from 'snabbdom';
import AnalyseCtrl from '../ctrl';

export function colorOf(fen: Fen): Color {
return fen.split(' ')[1] === 'w' ? 'white' : 'black';
}

export function winnerOf(fen: Fen, move: TablebaseMoveStats): Color | undefined {
const stm = colorOf(fen);
const stm = fenColor(fen);
if (move.checkmate || move.variant_loss || (move.dtz && move.dtz < 0)) return stm;
if (move.variant_win || (move.dtz && move.dtz > 0)) return opposite(stm);
return undefined;
Expand Down
6 changes: 4 additions & 2 deletions ui/analyse/src/study/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ export interface StudyChapterMeta {
id: string;
name: string;
ongoing?: boolean;
res?: string;
res?: '1-0' | '0-1' | '½-½' | '*';
}

export interface StudyChapterConfig extends StudyChapterMeta {
Expand Down Expand Up @@ -234,14 +234,16 @@ export interface ChapterPreview {
orientation: Color;
fen: string;
lastMove?: string;
lastMoveAt?: number;
playing: boolean;
outcome?: '1-0' | '0-1' | '1/2-1/2';
outcome?: '1-0' | '0-1' | '½-½';
}

export interface ChapterPreviewPlayer {
name: string;
title?: string;
rating?: number;
clock?: number;
}

export type Orientation = 'black' | 'white' | 'auto';
Expand Down
69 changes: 58 additions & 11 deletions ui/analyse/src/study/multiBoard.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import debounce from 'common/debounce';
import { renderClock, fenColor } from 'common/mini-game';
import { bind, MaybeVNodes } from 'common/snabbdom';
import { spinnerVdom as spinner } from 'common/spinner';
import { h, VNode } from 'snabbdom';
import { multiBoard as xhrLoad } from './studyXhr';
import { opposite } from 'chessground/util';
import { StudyCtrl, ChapterPreview, ChapterPreviewPlayer, Position } from './interfaces';
import { opposite as CgOpposite } from 'chessground/util';
import { opposite as oppositeColor } from 'chessops/util';
import { StudyCtrl, ChapterPreview, ChapterPreviewPlayer, Position, StudyChapterMeta } from './interfaces';

export class MultiBoardCtrl {
loading = false;
Expand All @@ -19,10 +21,28 @@ export class MultiBoardCtrl {
if (cp?.playing) {
cp.fen = node.fen;
cp.lastMove = node.uci;
const playerWhoMoved = cp.players && cp.players[oppositeColor(fenColor(cp.fen))];
playerWhoMoved && (playerWhoMoved.clock = node.clock);
// at this point `(cp: ChapterPreview).lastMoveAt` becomes outdated but should be ok since not in use anymore
// to mitigate bad usage, setting it as `undefined`
cp.lastMoveAt = undefined;
this.redraw();
}
};

addResult = (metas: StudyChapterMeta[]) => {
let changed = false;
for (const meta of metas) {
const cp = this.pager && this.pager.currentPageResults.find(cp => cp.id == meta.id);
if (cp?.playing) {
const oldOutcome = cp.outcome;
cp.outcome = meta.res !== '*' ? meta.res : undefined;
changed = changed || cp.outcome !== oldOutcome;
}
}
if (changed) this.redraw();
};

reload = (onInsert?: boolean) => {
if (this.pager && !onInsert) {
this.loading = true;
Expand Down Expand Up @@ -153,16 +173,29 @@ const makePreview = (study: StudyCtrl) => (preview: ChapterPreview) =>
},
postpatch(old, vnode) {
if (old.data!.fen !== preview.fen) {
lichess.miniGame.update(vnode.elm as HTMLElement, {
lm: preview.lastMove!,
fen: preview.fen,
});
if (preview.outcome) {
lichess.miniGame.finish(
vnode.elm as HTMLElement,
preview.outcome === '1-0' ? 'white' : preview.outcome === '0-1' ? 'black' : undefined
);
} else {
lichess.miniGame.update(vnode.elm as HTMLElement, {
lm: preview.lastMove!,
fen: preview.fen,
wc: computeTimeLeft(preview, 'white'),
bc: computeTimeLeft(preview, 'black'),
});
}
}
vnode.data!.fen = preview.fen;
},
},
},
[boardPlayer(preview, opposite(preview.orientation)), h('span.cg-wrap'), boardPlayer(preview, preview.orientation)]
[
boardPlayer(preview, CgOpposite(preview.orientation)),
h('span.cg-wrap'),
boardPlayer(preview, preview.orientation),
]
);

const userName = (u: ChapterPreviewPlayer) => (u.title ? [h('span.utitle', u.title), ' ' + u.name] : [u.name]);
Expand All @@ -179,11 +212,25 @@ function renderPlayer(player: ChapterPreviewPlayer | undefined): VNode | undefin
);
}

const computeTimeLeft = (preview: ChapterPreview, color: Color): number | undefined => {
const player = preview.players && preview.players[color];
if (player && player.clock) {
if (preview.lastMoveAt && fenColor(preview.fen) == color) {
const spent = (Date.now() - preview.lastMoveAt) / 1000;
return Math.max(0, player.clock / 100 - spent);
} else {
return player.clock / 100;
}
} else {
return;
}
};

const boardPlayer = (preview: ChapterPreview, color: Color) => {
const player = preview.players && preview.players[color];
const result = preview.outcome?.split('-')[color === 'white' ? 0 : 1];
return h('span.mini-game__player', [
h('span.mini-game__user', [renderPlayer(player)]),
result && h('span.mini-game__result', result.replace('1/2', '½')),
]);
const resultNode = result && h('span.mini-game__result', result);
const timeleft = computeTimeLeft(preview, color);
const clock = timeleft && renderClock(color, timeleft);
return h('span.mini-game__player', [h('span.mini-game__user', [renderPlayer(player)]), resultNode ?? clock]);
};
1 change: 1 addition & 0 deletions ui/analyse/src/study/studyCtrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ export default function (
},
chapters(d) {
chapters.list(d);
if (vm.toolTab() == 'multiBoard' || (relay && relay.tourShow.active)) multiBoard.addResult(d);
if (!currentChapter()) {
vm.chapterId = d[0].id;
if (!vm.mode.sticky) xhrReload();
Expand Down
11 changes: 11 additions & 0 deletions ui/common/src/mini-game.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { h } from 'snabbdom';

export const fenColor = (fen: string) => (fen.includes(' w') ? 'white' : 'black');

export const renderClock = (color: Color, time: number) =>
h(`span.mini-game__clock.mini-game__clock--${color}`, {
attrs: {
'data-time': time,
'data-managed': 1,
},
});
9 changes: 1 addition & 8 deletions ui/simul/src/view/pairings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { h } from 'snabbdom';
import { onInsert } from 'common/snabbdom';
import { renderClock } from 'common/mini-game';
import SimulCtrl from '../ctrl';
import { Pairing } from '../interfaces';
import { opposite } from 'chessground/util';
Expand All @@ -8,14 +9,6 @@ export default function (ctrl: SimulCtrl) {
return h('div.game-list.now-playing.box__pad', ctrl.data.pairings.map(miniPairing(ctrl)));
}

const renderClock = (color: Color, time: number) =>
h(`span.mini-game__clock.mini-game__clock--${color}`, {
attrs: {
'data-time': time,
'data-managed': 1,
},
});

const miniPairing = (ctrl: SimulCtrl) => (pairing: Pairing) => {
const game = pairing.game,
player = pairing.player;
Expand Down
13 changes: 6 additions & 7 deletions ui/site/src/component/mini-game.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { uciToMove } from 'chessground/util';
import { fenColor } from 'common/mini-game';
import * as domData from 'common/data';
import clockWidget from './clock-widget';
import StrongSocket from './socket';

const fenColor = (fen: string) => (fen.indexOf(' b') > 0 ? 'black' : 'white');

export const init = (node: HTMLElement) => {
if (!window.Chessground) setTimeout(() => init(node), 200);
else {
Expand Down Expand Up @@ -55,21 +54,21 @@ export const update = (node: HTMLElement, data: MiniGameUpdateData) => {
lastMove: uciToMove(lm),
});
const turnColor = fenColor(data.fen);
const renderClock = (time: number | undefined, color: Color) => {
const updateClock = (time: number | undefined, color: Color) => {
if (!isNaN(time!))
clockWidget($el[0]?.querySelector('.mini-game__clock--' + color) as HTMLElement, {
time: time!,
pause: color != turnColor || !clockIsRunning(data.fen, color),
});
};
renderClock(data.wc, 'white');
renderClock(data.bc, 'black');
updateClock(data.wc, 'white');
updateClock(data.bc, 'black');
};

export const finish = (node: HTMLElement, win?: string) =>
export const finish = (node: HTMLElement, win?: 'black' | 'white') =>
['white', 'black'].forEach(color => {
const $clock = $(node).find('.mini-game__clock--' + color);
// don't interfere with snabbdom clocks
if (!$clock.data('managed'))
$clock.replaceWith(`<span class="mini-game__result">${win ? (win == color[0] ? 1 : 0) : '½'}</span>`);
$clock.replaceWith(`<span class="mini-game__result">${win ? (win === color[0] ? 1 : 0) : '½'}</span>`);
});
Loading