puzzle racer WIP

puzzle-racer-road-translate
Thibault Duplessis 2021-03-04 18:01:53 +01:00
parent 58e005bc76
commit 4037ba6cd4
29 changed files with 236 additions and 206 deletions

View File

@ -6,6 +6,7 @@ import views._
import lila.api.Context
import lila.app._
import lila.common.HTTPRequest
import lila.racer.RacerPlayer
import lila.racer.RacerRace
import lila.socket.Socket
@ -19,38 +20,44 @@ final class Racer(env: Env)(implicit mat: akka.stream.Materializer) extends Lila
}
def create =
Open { implicit ctx =>
NoBot {
WithSessionId { sid =>
env.racer.api.create(sid, ctx.me) map { race =>
Redirect(routes.Racer.show(race.id.value))
}
}
WithPlayerId { implicit ctx => playerId =>
env.racer.api.create(playerId) map { race =>
Redirect(routes.Racer.show(race.id.value))
}
}
def show(id: String) =
Open { implicit ctx =>
NoBot {
WithSessionId { sid =>
env.racer.api.get(RacerRace.Id(id)) match {
case None => Redirect(routes.Racer.home).fuccess
case Some(race) =>
Ok(
html.racer.show(
race,
env.racer.json.raceJson(race, ctx.me.toRight(sid)),
env.storm.json.pref(ctx.pref.copy(coords = lila.pref.Pref.Coords.NONE))
)
).fuccess
}
}
WithPlayerId { implicit ctx => playerId =>
env.racer.api.get(RacerRace.Id(id)) match {
case None => Redirect(routes.Racer.home).fuccess
case Some(race) =>
Ok(
html.racer.show(
race,
env.racer.json.raceJson(race, playerId),
env.storm.json.pref(ctx.pref.copy(coords = lila.pref.Pref.Coords.NONE))
)
).fuccess
}
}
private def WithSessionId(f: String => Fu[Result])(implicit ctx: Context): Fu[Result] =
HTTPRequest sid ctx.req match {
case None => Redirect(routes.Racer.home).fuccess
case Some(sid) => f(sid)
def join(id: String) =
WithPlayerId { implicit ctx => playerId =>
Redirect {
env.racer.api.join(RacerRace.Id(id), playerId) match {
case None => routes.Racer.home
case Some(race) => routes.Racer.show(race.id.value)
}
}.fuccess
}
private def WithPlayerId(f: Context => RacerPlayer.Id => Fu[Result]): Action[Unit] =
Open { implicit ctx =>
NoBot {
HTTPRequest sid ctx.req map { env.racer.api.playerId(_, ctx.me) } match {
case None => Redirect(routes.Racer.home).fuccess
case Some(id) => f(ctx)(id)
}
}
}
}

View File

@ -119,6 +119,7 @@ GET /api/storm/dashboard/:username controllers.Storm.apiDashboardOf(usernam
GET /racer controllers.Racer.home
POST /racer controllers.Racer.create
GET /racer/:id controllers.Racer.show(id: String)
POST /racer/:id controllers.Racer.join(id: String)
# User Analysis
GET /analysis/help controllers.UserAnalysis.help

View File

@ -24,16 +24,28 @@ final class RacerApi(colls: RacerColls, selector: StormSelector, cacheApi: Cache
def get(id: Id): Option[RacerRace] = store getIfPresent id
def create(sessionId: String, user: Option[User]): Fu[RacerRace] =
def playerId(sessionId: String, user: Option[User]) = user match {
case Some(u) => RacerPlayer.Id.User(u.id)
case None => RacerPlayer.Id.Anon(sessionId)
}
def create(player: RacerPlayer.Id): Fu[RacerRace] =
selector.apply map { puzzles =>
val race = RacerRace.make(
owner = user match {
case Some(u) => RacerPlayer.Id.User(u.id)
case None => RacerPlayer.Id.Anon(sessionId)
},
owner = player,
puzzles = puzzles.grouped(2).flatMap(_.headOption).toList
)
store.put(race.id, race)
race
}
def join(id: RacerRace.Id, player: RacerPlayer.Id): Option[RacerRace] =
get(id) map { race =>
race.join(player) match {
case Some(joined) =>
store.put(joined.id, joined)
joined
case None => race
}
}
}

View File

@ -16,20 +16,17 @@ final class RacerJson(stormJson: StormJson, sign: StormSign, lightUserSync: Ligh
import StormJson._
def raceJson(race: RacerRace, me: Either[String, User]) =
def raceJson(race: RacerRace, playerId: RacerPlayer.Id) =
Json.obj(
"id" -> race.id.value,
"owner" -> {
(race.owner, me) match {
case (RacerPlayer.Id.User(id), Right(u)) => id == u.id
case (RacerPlayer.Id.Anon(a), Left(b)) => a == b
case _ => false
}
},
"race" -> Json.obj(
"id" -> race.id.value,
"isPlayer" -> race.has(playerId),
"isOwner" -> (race.owner == playerId)
),
"puzzles" -> race.puzzles,
"players" -> race.players.zipWithIndex.map { case (player, index) =>
Json
.obj("index" -> index)
.obj("index" -> (index + 1), "score" -> player.score)
.add("user" -> player.userId.flatMap(lightUserSync))
}
)

View File

@ -3,7 +3,7 @@ package lila.racer
import org.joda.time.DateTime
import lila.user.User
case class RacerPlayer(id: RacerPlayer.Id, createdAt: DateTime) {
case class RacerPlayer(id: RacerPlayer.Id, createdAt: DateTime, score: Int) {
import RacerPlayer.Id
@ -19,4 +19,6 @@ object RacerPlayer {
case class User(userId: lila.user.User.ID) extends Id
case class Anon(sessionId: String) extends Id
}
def make(id: Id) = RacerPlayer(id = id, score = 0, createdAt = DateTime.now)
}

View File

@ -17,20 +17,25 @@ case class RacerRace(
) {
def id = _id
def has(id: RacerPlayer.Id) = players.exists(_.id == id)
def join(id: RacerPlayer.Id): Option[RacerRace] =
!has(id) && players.sizeIs <= RacerRace.maxPlayers option
copy(players = players :+ RacerPlayer.make(id))
}
object RacerRace {
val maxPlayers = 10
case class Id(value: String) extends AnyVal with StringValue
def make(owner: RacerPlayer.Id, puzzles: List[StormPuzzle]) = RacerRace(
_id = Id(lila.common.ThreadLocalRandom nextString 8),
owner = owner,
players = List(
RacerPlayer(
id = owner,
createdAt = DateTime.now
)
RacerPlayer.make(owner)
),
puzzles = puzzles,
createdAt = DateTime.now,

View File

@ -11,11 +11,7 @@ export interface Contact {
user: User;
lastMsg: LastMsg;
}
export interface User {
id: string;
name: string;
title?: string;
patron: boolean;
export interface User extends LightUser {
online: boolean;
}
export interface Me extends User {

View File

@ -15,11 +15,10 @@ export function userIcon(user: User, cls: string): VNode {
);
}
export function userName(user: User): Array<string | VNode> {
return user.title
export const userName = (user: User): Array<string | VNode> =>
user.title
? [h('span.utitle', user.title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, user.title), ' ', user.name]
: [user.name];
}
export function bind(eventName: string, f: (e: Event) => void) {
return {

View File

@ -13,11 +13,11 @@
margin: 0;
}
.storm--mod-bonus-slow & {
.puz-mod-bonus-slow & {
color: $c-good;
}
.storm--mod-malus-slow & {
.puz-mod-malus-slow & {
color: $c-bad;
}
}

View File

@ -27,7 +27,7 @@
transition: color 0.1s;
.storm--mod-move & {
.puz-mod-move & {
color: $c-brag;
}
}
@ -65,11 +65,11 @@
box-shadow: 0 0 15px $c-in-base;
transition: all 0.5s ease-in-out;
.storm--mod-bonus-slow & {
.puz-mod-bonus-slow & {
display: none;
}
.storm--mod-malus-slow & {
.puz-mod-malus-slow & {
transition-property: width;
background: $c-bad;
box-shadow: 0 0 10px $c-bad, 0 0 20px $c-bad;
@ -93,7 +93,7 @@
}
}
.storm--mod-bonus-slow & {
.puz-mod-bonus-slow & {
display: block;
animation: bar-full 0.9s ease-in-out;
}

View File

@ -0,0 +1,24 @@
@import 'font';
@import 'side';
@import 'combo';
@import 'clock';
#main-wrap {
--main-max-width: calc(100vh - #{$site-header-outer-height} - #{$col1-uniboard-controls});
@include breakpoint($mq-col2) {
--main-max-width: auto;
}
user-select: none;
}
.puz {
&-side {
grid-area: side;
}
&-board {
grid-area: board;
}
}

View File

@ -94,7 +94,7 @@ $mq-col2: $mq-col2-uniboard;
transition: text-shadow 0.1s;
}
.storm--mod-puzzle & {
.puz-mod-puzzle & {
text-shadow: 0 0 15px white;
}
}

View File

@ -6,7 +6,7 @@ export class Clock {
initialMillis = config.clock.initial * 1000;
start = () => {
this.startAt = getNow();
if (!this.startAt) this.startAt = getNow();
};
millis = (): number =>

View File

@ -33,7 +33,6 @@ export interface Puzzle {
}
export interface Run {
startAt?: number;
endAt?: number;
moves: number;
errors: number;

View File

@ -1,9 +1,10 @@
import { Run } from './interfaces';
import { Run, TimeMod } from './interfaces';
import { Config as CgConfig } from 'chessground/config';
import { uciToLastMove } from './util';
import { getNow, uciToLastMove } from './util';
import { opposite } from 'chessops';
import { makeFen, parseFen } from 'chessops/fen';
import { chessgroundDests } from 'chessops/compat';
import config from './config';
export const makeCgOpts = (run: Run): CgConfig => {
const cur = run.current;
@ -24,3 +25,27 @@ export const makeCgOpts = (run: Run): CgConfig => {
lastMove: uciToLastMove(cur.lastMove()),
};
};
export const onGoodMove = (run: Run): TimeMod | undefined => {
run.combo.inc();
run.modifier.moveAt = getNow();
const bonus = run.combo.bonus();
if (bonus) {
run.modifier.bonus = bonus;
run.clock.addSeconds(bonus.seconds);
return bonus;
}
return undefined;
};
export const onBadMove = (run: Run): void => {
run.errors++;
run.combo.reset();
run.clock.addSeconds(-config.clock.malus);
run.modifier.malus = {
seconds: config.clock.malus,
at: getNow(),
};
};
export const countWins = (run: Run): number => run.history.reduce((c, r) => c + (r.win ? 1 : 0), 0);

View File

@ -31,7 +31,7 @@ export default function renderClock(run: Run, onFlag: OnFlag): VNode {
}
function renderIn(run: Run, onFlag: OnFlag, el: HTMLElement) {
if (!run.startAt) return;
if (!run.clock.startAt) return;
const mods = run.modifier;
const now = getNow();
const millis = run.clock.millis();

View File

@ -2,6 +2,7 @@ import { h } from 'snabbdom';
import { VNode } from 'snabbdom/vnode';
import config from '../config';
import { Run } from '../interfaces';
import { countWins } from '../run';
import { getNow } from '../util';
export const playModifiers = (run: Run) => {
@ -9,10 +10,10 @@ export const playModifiers = (run: Run) => {
const malus = run.modifier.malus;
const bonus = run.modifier.bonus;
return {
'storm--mod-puzzle': run.current.startAt > now - 90,
'storm--mod-move': run.modifier.moveAt > now - 90,
'storm--mod-malus-slow': !!malus && malus.at > now - 950,
'storm--mod-bonus-slow': !!bonus && bonus.at > now - 950,
'puz-mod-puzzle': run.current.startAt > now - 90,
'puz-mod-move': run.modifier.moveAt > now - 90,
'puz-mod-malus-slow': !!malus && malus.at > now - 950,
'puz-mod-bonus-slow': !!bonus && bonus.at > now - 950,
};
};
@ -47,3 +48,6 @@ export const renderCombo = (run: Run): VNode => {
]),
]);
};
export const renderSolved = (run: Run): VNode =>
h('div.puz-side__top.puz-side__solved', [h('div.puz-side__solved__text', countWins(run))]);

View File

@ -0,0 +1,5 @@
.racer__race {
@extend %box-neat;
background: $c-bg-box;
padding: 2vh 2vw;
}

View File

@ -1,23 +1,20 @@
@import 'race';
.racer {
&--play {
display: grid;
&__race {
grid-area: race;
}
&__side {
grid-area: side;
}
display: grid;
grid-row-gap: $block-gap;
grid-column-gap: $block-gap;
grid-template-areas: 'race' 'board' 'side';
&__board {
grid-area: board;
}
grid-row-gap: $block-gap;
grid-column-gap: $block-gap;
grid-template-areas: 'board' 'side';
@include breakpoint($mq-col2) {
grid-template-columns: $col2-uniboard-width $col2-uniboard-table;
grid-template-rows: fit-content(0);
grid-template-areas: 'board side';
}
@include breakpoint($mq-col2) {
grid-template-columns: $col2-uniboard-width $col2-uniboard-table;
grid-template-rows: auto fit-content(0);
grid-template-areas:
'race race'
'board side';
}
}

View File

@ -3,9 +3,6 @@
@import '../../../common/css/component/board-resize';
@import '../../../common/css/component/bar-glider';
@import '../../../chess/css/promotion';
@import '../../../puz/css/font';
@import '../../../puz/css/side';
@import '../../../puz/css/combo';
@import '../../../puz/css/clock';
@import '../../../puz/css/puz';
@import '../racer';

View File

@ -3,11 +3,11 @@ import makePromotion from 'puz/promotion';
import sign from 'puz/sign';
import { Api as CgApi } from 'chessground/api';
import { getNow, loadSound } from 'puz/util';
import { makeCgOpts } from 'puz/run';
import { makeCgOpts, onBadMove, onGoodMove } from 'puz/run';
import { parseUci } from 'chessops/util';
import { prop, Prop } from 'common';
import { Role } from 'chessground/types';
import { RacerOpts, RacerData, RacerVm, RacerPrefs } from './interfaces';
import { RacerOpts, RacerData, RacerVm, RacerPrefs, Race } from './interfaces';
import { Promotion, Run } from 'puz/interfaces';
import { Combo } from 'puz/combo';
import CurrentPuzzle from 'puz/current';
@ -16,6 +16,7 @@ import { Clock } from 'puz/clock';
export default class StormCtrl {
private data: RacerData;
private redraw: () => void;
race: Race;
pref: RacerPrefs;
run: Run;
vm: RacerVm;
@ -25,6 +26,7 @@ export default class StormCtrl {
constructor(opts: RacerOpts, redraw: (data: RacerData) => void) {
this.data = opts.data;
this.race = this.data.race;
this.pref = opts.pref;
this.redraw = () => redraw(this.data);
this.trans = lichess.trans(opts.i18n);
@ -46,6 +48,8 @@ export default class StormCtrl {
if (this.data.key) setTimeout(() => sign(this.data.key!).then(this.vm.signed), 1000 * 40);
}
players = () => this.data.players;
end = (): void => {
this.run.history.reverse();
this.run.endAt = getNow();
@ -65,7 +69,7 @@ export default class StormCtrl {
};
playUserMove = (orig: Key, dest: Key, promotion?: Role): void => {
if (!this.run.moves) this.run.startAt = getNow();
this.run.clock.start();
this.run.moves++;
this.promotion.cancel();
const cur = this.run.current;
@ -76,14 +80,7 @@ export default class StormCtrl {
pos.play(move);
if (pos.isCheckmate() || uci == cur.expectedMove()) {
cur.moveIndex++;
this.run.combo.inc();
this.run.modifier.moveAt = getNow();
const bonus = this.run.combo.bonus();
if (bonus) {
this.run.modifier.bonus = bonus;
this.run.clock.addSeconds(bonus.seconds);
this.sound.bonus();
}
if (onGoodMove(this.run)) this.sound.bonus();
if (cur.isOver()) {
this.pushToHistory(true);
if (!this.incPuzzle()) this.end();
@ -95,13 +92,7 @@ export default class StormCtrl {
} else {
lichess.sound.play('error');
this.pushToHistory(false);
this.run.errors++;
this.run.combo.reset();
this.run.clock.addSeconds(-config.clock.malus);
this.run.modifier.malus = {
seconds: config.clock.malus,
at: getNow(),
};
onBadMove(this.run);
if (this.run.clock.flag()) this.end();
else if (!this.incPuzzle()) this.end();
}

View File

@ -12,19 +12,22 @@ export interface RacerPrefs extends PuzPrefs {}
export interface RacerData {
race: Race;
puzzles: Puzzle[];
players: Player[];
owner: boolean;
key?: string;
}
export interface Race {
id: string;
players: Player[];
isPlayer: boolean;
isOwner: boolean;
startRel?: number;
}
export interface Player {
index: number;
user?: LightUser;
score: number;
}
export interface RacerVm {

View File

@ -2,11 +2,12 @@ import { Chessground } from 'chessground';
import { makeConfig as makeCgConfig } from 'puz/view/chessground';
import renderClock from 'puz/view/clock';
import RacerCtrl from '../ctrl';
import { onInsert } from 'puz/util';
import { playModifiers, renderCombo } from 'puz/view/util';
import { playModifiers, renderCombo, renderSolved } from 'puz/view/util';
import { h } from 'snabbdom';
import { VNode } from 'snabbdom/vnode';
import { makeCgOpts } from 'puz/run';
import { renderRace } from './race';
import { Race } from '../interfaces';
export default function (ctrl: RacerCtrl): VNode {
return h(
@ -30,50 +31,37 @@ const chessground = (ctrl: RacerCtrl): VNode =>
});
const renderPlay = (ctrl: RacerCtrl): VNode[] => [
h('div.storm__board.main-board', [chessground(ctrl), ctrl.promotion.view()]),
h('div.storm__side', [
ctrl.run.startAt ? renderSolved(ctrl) : renderStart(ctrl),
renderClock(ctrl.run, ctrl.endNow),
h('div.storm__table', [renderControls(ctrl), renderCombo(ctrl.run)]),
renderRace(ctrl),
h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]),
h('div.puz-side', [
ctrl.run.clock.startAt ? renderSolved(ctrl.run) : renderStart(),
ctrl.race.isPlayer ? renderClock(ctrl.run, ctrl.endNow) : renderJoin(ctrl.race),
h('div.puz-side__table', [renderCombo(ctrl.run)]),
]),
];
const renderControls = (ctrl: RacerCtrl): VNode =>
h('div.storm__control', [
h('a.storm__control__reload.button.button-empty', {
attrs: {
href: '/storm',
'data-icon': 'B',
title: ctrl.trans('newRun'),
},
}),
h('a.storm__control__end.button.button-empty', {
attrs: {
'data-icon': 'b',
title: ctrl.trans('endRun'),
},
hook: onInsert(el => el.addEventListener('click', ctrl.endNow)),
}),
]);
const renderSolved = (ctrl: RacerCtrl): VNode =>
h('div.storm__top.storm__solved', [h('div.storm__solved__text', ctrl.countWins())]);
const renderStart = (ctrl: RacerCtrl) =>
const renderJoin = (race: Race) =>
h(
'div.storm__top.storm__start',
h('div.storm__start__text', [h('strong', 'Puzzle Storm'), h('span', ctrl.trans('moveToStart'))])
'form.puz-side__join',
{
attrs: {
action: `/racer/${race.id}`,
method: 'post',
},
},
h(
'button.button.button-fat',
{
attrs: {
type: 'submit',
},
},
'Join the race!'
)
);
const renderReload = (msg: string) =>
h('div.storm.storm--reload.box.box-pad', [
h('i', { attrs: { 'data-icon': '~' } }),
h('p', msg),
h(
'a.storm--dup__reload.button',
{
attrs: { href: '/storm' },
},
'Click to reload'
),
]);
const renderStart = () =>
h(
'div.puz-side__top.puz-side__start',
h('div.puz-side__start__text', [h('strong', 'Puzzle Racer'), h('span', 'Waiting to start')])
);

View File

@ -0,0 +1,17 @@
import { h } from 'snabbdom';
import { VNode } from 'snabbdom/vnode';
import RacerCtrl from '../ctrl';
import { Player } from '../interfaces';
export const renderRace = (ctrl: RacerCtrl) => h('div.racer__race', ctrl.players().map(renderPlayer));
const renderPlayer = (player: Player) =>
h('div.racer__race__player', [
h('div.racer__race__player__name', player.user ? userName(player.user) : ['Anon', ' ', player.index]),
h(`div.racer__race__player__car.car-${player.index}`, [player.score]),
]);
export const userName = (user: LightUser): Array<string | VNode> =>
user.title
? [h('span.utitle', user.title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, user.title), ' ', user.name]
: [user.name];

View File

@ -2,14 +2,6 @@
&--play {
display: grid;
&__side {
grid-area: side;
}
&__board {
grid-area: board;
}
grid-row-gap: $block-gap;
grid-column-gap: $block-gap;
grid-template-areas: 'board' 'side';

View File

@ -1,15 +1,5 @@
$mq-col2: $mq-col2-uniboard;
#main-wrap {
--main-max-width: calc(100vh - #{$site-header-outer-height} - #{$col1-uniboard-controls});
@include breakpoint($mq-col2) {
--main-max-width: auto;
}
user-select: none;
}
@import 'play';
@import 'end';
@import 'high';

View File

@ -3,9 +3,6 @@
@import '../../../common/css/component/board-resize';
@import '../../../common/css/component/bar-glider';
@import '../../../chess/css/promotion';
@import '../../../puz/css/font';
@import '../../../puz/css/side';
@import '../../../puz/css/combo';
@import '../../../puz/css/clock';
@import '../../../puz/css/puz';
@import '../storm';

View File

@ -4,7 +4,7 @@ import makePromotion from 'puz/promotion';
import sign from 'puz/sign';
import { Api as CgApi } from 'chessground/api';
import { getNow, loadSound } from 'puz/util';
import { makeCgOpts } from 'puz/run';
import { countWins, makeCgOpts, onBadMove, onGoodMove } from 'puz/run';
import { parseUci } from 'chessops/util';
import { prop, Prop } from 'common';
import { Role } from 'chessground/types';
@ -50,7 +50,7 @@ export default class StormCtrl {
setTimeout(this.hotkeys, 1000);
if (this.data.key) setTimeout(() => sign(this.data.key!).then(this.vm.signed), 1000 * 40);
setTimeout(() => {
if (!this.run.startAt) {
if (!this.run.clock.startAt) {
this.vm.lateStart = true;
this.redraw();
}
@ -80,7 +80,7 @@ export default class StormCtrl {
};
playUserMove = (orig: Key, dest: Key, promotion?: Role): void => {
if (!this.run.moves) this.run.startAt = getNow();
this.run.clock.start();
this.run.moves++;
this.promotion.cancel();
const cur = this.run.current;
@ -91,14 +91,7 @@ export default class StormCtrl {
pos.play(move);
if (pos.isCheckmate() || uci == cur.expectedMove()) {
cur.moveIndex++;
this.run.combo.inc();
this.run.modifier.moveAt = getNow();
const bonus = this.run.combo.bonus();
if (bonus) {
this.run.modifier.bonus = bonus;
this.run.clock.addSeconds(bonus.seconds);
this.sound.bonus();
}
if (onGoodMove(this.run)) this.sound.bonus();
if (cur.isOver()) {
this.pushToHistory(true);
if (!this.incPuzzle()) this.end();
@ -110,13 +103,7 @@ export default class StormCtrl {
} else {
lichess.sound.play('error');
this.pushToHistory(false);
this.run.errors++;
this.run.combo.reset();
this.run.clock.addSeconds(-config.clock.malus);
this.run.modifier.malus = {
seconds: config.clock.malus,
at: getNow(),
};
onBadMove(this.run);
if (this.run.clock.flag()) this.end();
else if (!this.incPuzzle()) this.end();
}
@ -146,8 +133,6 @@ export default class StormCtrl {
return false;
};
countWins = (): number => this.run.history.reduce((c, r) => c + (r.win ? 1 : 0), 0);
withGround = <A>(f: (cg: CgApi) => A): A | false => {
const g = this.ground();
return g && f(g);
@ -155,11 +140,11 @@ export default class StormCtrl {
runStats = (): StormRecap => ({
puzzles: this.run.history.length,
score: this.countWins(),
score: countWins(this.run),
moves: this.run.moves,
errors: this.run.errors,
combo: this.run.combo.best,
time: (this.run.endAt! - this.run.startAt!) / 1000,
time: (this.run.endAt! - this.run.clock.startAt!) / 1000,
highest: this.run.history.reduce((h, r) => (r.win && r.puzzle.rating > h ? r.puzzle.rating : h), 0),
signed: this.vm.signed(),
});
@ -179,7 +164,7 @@ export default class StormCtrl {
const dupTabMsg = lichess.storage.make('storm.tab');
dupTabMsg.fire(this.data.puzzles[0].id);
dupTabMsg.listen(ev => {
if (!this.run.startAt && ev.value == this.data.puzzles[0].id) {
if (!this.run.clock.startAt && ev.value == this.data.puzzles[0].id) {
this.vm.dupTab = true;
this.redraw();
}

View File

@ -4,7 +4,7 @@ import renderClock from 'puz/view/clock';
import renderEnd from './end';
import StormCtrl from '../ctrl';
import { onInsert } from 'puz/util';
import { playModifiers, renderCombo } from 'puz/view/util';
import { playModifiers, renderCombo, renderSolved } from 'puz/view/util';
import { h } from 'snabbdom';
import { VNode } from 'snabbdom/vnode';
import { makeCgOpts } from 'puz/run';
@ -35,9 +35,9 @@ const chessground = (ctrl: StormCtrl): VNode =>
});
const renderPlay = (ctrl: StormCtrl): VNode[] => [
h('div.storm__board.main-board', [chessground(ctrl), ctrl.promotion.view()]),
h('div.storm__side.puz-side', [
ctrl.run.startAt ? renderSolved(ctrl) : renderStart(ctrl),
h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]),
h('div.puz-side', [
ctrl.run.clock.startAt ? renderSolved(ctrl.run) : renderStart(ctrl),
renderClock(ctrl.run, ctrl.endNow),
h('div.puz-side__table', [renderControls(ctrl), renderCombo(ctrl.run)]),
]),
@ -61,9 +61,6 @@ const renderControls = (ctrl: StormCtrl): VNode =>
}),
]);
const renderSolved = (ctrl: StormCtrl): VNode =>
h('div.puz-side__top.puz-side__solved', [h('div.puz-side__solved__text', ctrl.countWins())]);
const renderStart = (ctrl: StormCtrl) =>
h(
'div.puz-side__top.puz-side__start',