puzzle racer WIP
parent
58e005bc76
commit
4037ba6cd4
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ export class Clock {
|
|||
initialMillis = config.clock.initial * 1000;
|
||||
|
||||
start = () => {
|
||||
this.startAt = getNow();
|
||||
if (!this.startAt) this.startAt = getNow();
|
||||
};
|
||||
|
||||
millis = (): number =>
|
||||
|
|
|
@ -33,7 +33,6 @@ export interface Puzzle {
|
|||
}
|
||||
|
||||
export interface Run {
|
||||
startAt?: number;
|
||||
endAt?: number;
|
||||
moves: number;
|
||||
errors: number;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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))]);
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.racer__race {
|
||||
@extend %box-neat;
|
||||
background: $c-bg-box;
|
||||
padding: 2vh 2vw;
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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')])
|
||||
);
|
||||
|
|
|
@ -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];
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue