puzzle racer WIP
parent
4037ba6cd4
commit
10094de7c0
|
@ -77,7 +77,7 @@ lazy val storm = module("storm",
|
|||
)
|
||||
|
||||
lazy val racer = module("racer",
|
||||
Seq(common, memo, hub, puzzle, storm, db, user, pref, tree),
|
||||
Seq(common, memo, hub, puzzle, storm, db, user, pref, tree, room),
|
||||
reactivemongo.bundle
|
||||
)
|
||||
|
||||
|
|
|
@ -18,10 +18,10 @@ final private class ChallengeSocket(
|
|||
def reload(challengeId: Challenge.ID): Unit =
|
||||
rooms.tell(challengeId, NotifyVersion("reload", JsNull))
|
||||
|
||||
lazy val rooms = makeRoomMap(send)
|
||||
|
||||
private lazy val send: String => Unit = remoteSocketApi.makeSender("chal-out").apply _
|
||||
|
||||
lazy val rooms = makeRoomMap(send)
|
||||
|
||||
private lazy val challengeHandler: Handler = { case Protocol.In.OwnerPings(ids) =>
|
||||
ids foreach api.ping
|
||||
}
|
||||
|
|
|
@ -20,9 +20,11 @@ final class Env(
|
|||
stormJson: StormJson,
|
||||
stormSign: StormSign,
|
||||
lightUserGetter: LightUser.GetterSync,
|
||||
remoteSocketApi: lila.socket.RemoteSocket,
|
||||
db: lila.db.Db
|
||||
)(implicit
|
||||
ec: scala.concurrent.ExecutionContext
|
||||
ec: scala.concurrent.ExecutionContext,
|
||||
mode: play.api.Mode
|
||||
) {
|
||||
|
||||
private lazy val colls = new RacerColls(puzzle = puzzleColls.puzzle)
|
||||
|
@ -30,6 +32,8 @@ final class Env(
|
|||
lazy val api = wire[RacerApi]
|
||||
|
||||
lazy val json = wire[RacerJson]
|
||||
|
||||
private val socket = wire[RacerSocket] // requires eager eval
|
||||
}
|
||||
|
||||
final private class RacerColls(val puzzle: AsyncColl)
|
||||
|
|
|
@ -43,9 +43,27 @@ final class RacerApi(colls: RacerColls, selector: StormSelector, cacheApi: Cache
|
|||
get(id) map { race =>
|
||||
race.join(player) match {
|
||||
case Some(joined) =>
|
||||
store.put(joined.id, joined)
|
||||
saveAndPublish(joined)
|
||||
joined
|
||||
case None => race
|
||||
}
|
||||
}
|
||||
|
||||
def registerPlayerMoves(id: RacerRace.Id, player: RacerPlayer.Id, moves: Int): Unit =
|
||||
get(id) foreach { prev =>
|
||||
val race = prev.registerMoves(player, moves)
|
||||
saveAndPublish(race)
|
||||
}
|
||||
|
||||
private def save(race: RacerRace): Unit =
|
||||
store.put(race.id, race)
|
||||
|
||||
private def saveAndPublish(race: RacerRace): Unit = {
|
||||
save(race)
|
||||
socket.foreach(_ publishState race)
|
||||
}
|
||||
|
||||
// work around circular dependency
|
||||
private var socket: Option[RacerSocket] = None
|
||||
private[racer] def registerSocket(s: RacerSocket) = { socket = s.some }
|
||||
}
|
||||
|
|
|
@ -24,10 +24,18 @@ final class RacerJson(stormJson: StormJson, sign: StormSign, lightUserSync: Ligh
|
|||
"isOwner" -> (race.owner == playerId)
|
||||
),
|
||||
"puzzles" -> race.puzzles,
|
||||
"players" -> race.players.zipWithIndex.map { case (player, index) =>
|
||||
Json
|
||||
.obj("index" -> (index + 1), "score" -> player.score)
|
||||
.add("user" -> player.userId.flatMap(lightUserSync))
|
||||
}
|
||||
"players" -> playersJson(race)
|
||||
)
|
||||
|
||||
def state(race: RacerRace) = Json.obj(
|
||||
"players" -> playersJson(race)
|
||||
)
|
||||
|
||||
private def playersJson(race: RacerRace) = JsArray {
|
||||
race.players.zipWithIndex.map { case (player, index) =>
|
||||
Json
|
||||
.obj("index" -> (index + 1), "moves" -> player.moves)
|
||||
.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, score: Int) {
|
||||
case class RacerPlayer(id: RacerPlayer.Id, createdAt: DateTime, moves: Int) {
|
||||
|
||||
import RacerPlayer.Id
|
||||
|
||||
|
@ -18,7 +18,10 @@ object RacerPlayer {
|
|||
object Id {
|
||||
case class User(userId: lila.user.User.ID) extends Id
|
||||
case class Anon(sessionId: String) extends Id
|
||||
def apply(str: String) =
|
||||
if (str startsWith "@") Anon(str drop 1)
|
||||
else User(str)
|
||||
}
|
||||
|
||||
def make(id: Id) = RacerPlayer(id = id, score = 0, createdAt = DateTime.now)
|
||||
def make(id: Id) = RacerPlayer(id = id, moves = 0, createdAt = DateTime.now)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package lila.racer
|
||||
|
||||
|
||||
import org.joda.time.DateTime
|
||||
|
||||
import lila.user.User
|
||||
|
@ -23,6 +24,14 @@ case class RacerRace(
|
|||
def join(id: RacerPlayer.Id): Option[RacerRace] =
|
||||
!has(id) && players.sizeIs <= RacerRace.maxPlayers option
|
||||
copy(players = players :+ RacerPlayer.make(id))
|
||||
|
||||
def registerMoves(playerId: RacerPlayer.Id, moves: Int): RacerRace =
|
||||
copy(
|
||||
players = players map {
|
||||
case p if p.id == playerId => p.copy(moves = moves)
|
||||
case p => p
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
object RacerRace {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package lila.racer
|
||||
|
||||
import lila.room.RoomSocket.{ Protocol => RP, _ }
|
||||
import lila.socket.RemoteSocket.{ Protocol => P, _ }
|
||||
import play.api.libs.json.{ JsObject, Json }
|
||||
|
||||
final private class RacerSocket(
|
||||
api: RacerApi,
|
||||
json: RacerJson,
|
||||
remoteSocketApi: lila.socket.RemoteSocket
|
||||
)(implicit
|
||||
ec: scala.concurrent.ExecutionContext,
|
||||
mode: play.api.Mode
|
||||
) {
|
||||
|
||||
import RacerSocket._
|
||||
|
||||
def publishState(race: RacerRace): Unit = send(
|
||||
Protocol.Out.publishState(race.id, json state race).pp
|
||||
)
|
||||
|
||||
private lazy val send: String => Unit = remoteSocketApi.makeSender("racer-out").apply _
|
||||
|
||||
lazy val rooms = makeRoomMap(send)
|
||||
|
||||
private lazy val racerHandler: Handler = { case Protocol.In.PlayerMoves(raceId, playerId, moves) =>
|
||||
api.registerPlayerMoves(raceId, playerId, moves)
|
||||
}
|
||||
|
||||
remoteSocketApi.subscribe("racer-in", Protocol.In.reader)(
|
||||
racerHandler orElse minRoomHandler(rooms, logger) orElse remoteSocketApi.baseHandler
|
||||
)
|
||||
|
||||
api registerSocket this
|
||||
}
|
||||
|
||||
object RacerSocket {
|
||||
|
||||
object Protocol {
|
||||
|
||||
object In {
|
||||
|
||||
case class PlayerMoves(race: RacerRace.Id, player: RacerPlayer.Id, moves: Int) extends P.In
|
||||
|
||||
val reader: P.In.Reader = raw =>
|
||||
raw.path match {
|
||||
case "racer/moves" =>
|
||||
raw.get(3) { case Array(raceId, playerId, moveStr) =>
|
||||
moveStr.toIntOption map { PlayerMoves(RacerRace.Id(raceId), RacerPlayer.Id(playerId), _) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Out {
|
||||
|
||||
def publishState(id: RacerRace.Id, data: JsObject) = s"racer/state $id ${Json stringify data}"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,3 +24,9 @@ export const loadSound = (file: string, volume?: number, delay?: number) => {
|
|||
setTimeout(() => lichess.sound.loadOggOrMp3(file, `${lichess.sound.baseUrl}/${file}`), delay || 1000);
|
||||
return () => lichess.sound.play(file, volume);
|
||||
};
|
||||
|
||||
export const sound = {
|
||||
move: (take: boolean) => lichess.sound.play(take ? 'capture' : 'move'),
|
||||
bonus: loadSound('other/ping', 0.8, 1000),
|
||||
end: loadSound('other/gewonnen', 0.5, 5000),
|
||||
};
|
||||
|
|
|
@ -2,12 +2,12 @@ import config from 'puz/config';
|
|||
import makePromotion from 'puz/promotion';
|
||||
import sign from 'puz/sign';
|
||||
import { Api as CgApi } from 'chessground/api';
|
||||
import { getNow, loadSound } from 'puz/util';
|
||||
import { getNow, sound } from 'puz/util';
|
||||
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, Race } from './interfaces';
|
||||
import { RacerOpts, RacerData, RacerVm, RacerPrefs, Race, ServerState } from './interfaces';
|
||||
import { Promotion, Run } from 'puz/interfaces';
|
||||
import { Combo } from 'puz/combo';
|
||||
import CurrentPuzzle from 'puz/current';
|
||||
|
@ -46,6 +46,11 @@ export default class StormCtrl {
|
|||
};
|
||||
this.promotion = makePromotion(this.withGround, () => makeCgOpts(this.run), this.redraw);
|
||||
if (this.data.key) setTimeout(() => sign(this.data.key!).then(this.vm.signed), 1000 * 40);
|
||||
lichess.socket = new lichess.StrongSocket(`/racer/${this.race.id}`, false);
|
||||
lichess.pubsub.on('socket.in.racerState', (state: ServerState) => {
|
||||
this.data.players = state.players;
|
||||
this.redraw();
|
||||
});
|
||||
}
|
||||
|
||||
players = () => this.data.players;
|
||||
|
@ -55,7 +60,7 @@ export default class StormCtrl {
|
|||
this.run.endAt = getNow();
|
||||
this.ground(false);
|
||||
this.redraw();
|
||||
this.sound.end();
|
||||
sound.end();
|
||||
this.redrawSlow();
|
||||
};
|
||||
|
||||
|
@ -72,23 +77,24 @@ export default class StormCtrl {
|
|||
this.run.clock.start();
|
||||
this.run.moves++;
|
||||
this.promotion.cancel();
|
||||
const cur = this.run.current;
|
||||
const puzzle = this.run.current;
|
||||
const uci = `${orig}${dest}${promotion ? (promotion == 'knight' ? 'n' : promotion[0]) : ''}`;
|
||||
const pos = cur.position();
|
||||
const pos = puzzle.position();
|
||||
const move = parseUci(uci)!;
|
||||
let captureSound = pos.board.occupied.has(move.to);
|
||||
pos.play(move);
|
||||
if (pos.isCheckmate() || uci == cur.expectedMove()) {
|
||||
cur.moveIndex++;
|
||||
if (onGoodMove(this.run)) this.sound.bonus();
|
||||
if (cur.isOver()) {
|
||||
if (pos.isCheckmate() || uci == puzzle.expectedMove()) {
|
||||
puzzle.moveIndex++;
|
||||
onGoodMove(this.run);
|
||||
lichess.pubsub.emit('socket.send', 'racerMoves', this.run.moves);
|
||||
if (puzzle.isOver()) {
|
||||
this.pushToHistory(true);
|
||||
if (!this.incPuzzle()) this.end();
|
||||
} else {
|
||||
cur.moveIndex++;
|
||||
captureSound = captureSound || pos.board.occupied.has(parseUci(cur.line[cur.moveIndex]!)!.to);
|
||||
puzzle.moveIndex++;
|
||||
captureSound = captureSound || pos.board.occupied.has(parseUci(puzzle.line[puzzle.moveIndex]!)!.to);
|
||||
}
|
||||
this.sound.move(captureSound);
|
||||
sound.move(captureSound);
|
||||
} else {
|
||||
lichess.sound.play('error');
|
||||
this.pushToHistory(false);
|
||||
|
@ -128,10 +134,4 @@ export default class StormCtrl {
|
|||
const g = this.ground();
|
||||
return g && f(g);
|
||||
};
|
||||
|
||||
private sound = {
|
||||
move: (take: boolean) => lichess.sound.play(take ? 'capture' : 'move'),
|
||||
bonus: loadSound('other/ping', 0.8, 1000),
|
||||
end: loadSound('other/gewonnen', 0.6, 5000),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -24,10 +24,14 @@ export interface Race {
|
|||
startRel?: number;
|
||||
}
|
||||
|
||||
export interface ServerState {
|
||||
players: Player[];
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
index: number;
|
||||
user?: LightUser;
|
||||
score: number;
|
||||
moves: number;
|
||||
}
|
||||
|
||||
export interface RacerVm {
|
||||
|
|
|
@ -8,7 +8,7 @@ export const renderRace = (ctrl: RacerCtrl) => h('div.racer__race', ctrl.players
|
|||
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]),
|
||||
h(`div.racer__race__player__car.car-${player.index}`, [player.moves]),
|
||||
]);
|
||||
|
||||
export const userName = (user: LightUser): Array<string | VNode> =>
|
||||
|
|
|
@ -3,7 +3,7 @@ import config from 'puz/config';
|
|||
import makePromotion from 'puz/promotion';
|
||||
import sign from 'puz/sign';
|
||||
import { Api as CgApi } from 'chessground/api';
|
||||
import { getNow, loadSound } from 'puz/util';
|
||||
import { getNow, sound } from 'puz/util';
|
||||
import { countWins, makeCgOpts, onBadMove, onGoodMove } from 'puz/run';
|
||||
import { parseUci } from 'chessops/util';
|
||||
import { prop, Prop } from 'common';
|
||||
|
@ -62,7 +62,7 @@ export default class StormCtrl {
|
|||
this.run.endAt = getNow();
|
||||
this.ground(false);
|
||||
this.redraw();
|
||||
this.sound.end();
|
||||
sound.end();
|
||||
xhr.record(this.runStats(), this.data.notAnExploit).then(res => {
|
||||
this.vm.response = res;
|
||||
this.redraw();
|
||||
|
@ -83,23 +83,23 @@ export default class StormCtrl {
|
|||
this.run.clock.start();
|
||||
this.run.moves++;
|
||||
this.promotion.cancel();
|
||||
const cur = this.run.current;
|
||||
const puzzle = this.run.current;
|
||||
const uci = `${orig}${dest}${promotion ? (promotion == 'knight' ? 'n' : promotion[0]) : ''}`;
|
||||
const pos = cur.position();
|
||||
const pos = puzzle.position();
|
||||
const move = parseUci(uci)!;
|
||||
let captureSound = pos.board.occupied.has(move.to);
|
||||
pos.play(move);
|
||||
if (pos.isCheckmate() || uci == cur.expectedMove()) {
|
||||
cur.moveIndex++;
|
||||
if (onGoodMove(this.run)) this.sound.bonus();
|
||||
if (cur.isOver()) {
|
||||
if (pos.isCheckmate() || uci == puzzle.expectedMove()) {
|
||||
puzzle.moveIndex++;
|
||||
onGoodMove(this.run);
|
||||
if (puzzle.isOver()) {
|
||||
this.pushToHistory(true);
|
||||
if (!this.incPuzzle()) this.end();
|
||||
} else {
|
||||
cur.moveIndex++;
|
||||
captureSound = captureSound || pos.board.occupied.has(parseUci(cur.line[cur.moveIndex]!)!.to);
|
||||
puzzle.moveIndex++;
|
||||
captureSound = captureSound || pos.board.occupied.has(parseUci(puzzle.line[puzzle.moveIndex]!)!.to);
|
||||
}
|
||||
this.sound.move(captureSound);
|
||||
sound.move(captureSound);
|
||||
} else {
|
||||
lichess.sound.play('error');
|
||||
this.pushToHistory(false);
|
||||
|
@ -154,12 +154,6 @@ export default class StormCtrl {
|
|||
this.redraw();
|
||||
};
|
||||
|
||||
private sound = {
|
||||
move: (take: boolean) => lichess.sound.play(take ? 'capture' : 'move'),
|
||||
bonus: loadSound('other/ping', 0.8, 1000),
|
||||
end: loadSound('other/gewonnen', 0.6, 5000),
|
||||
};
|
||||
|
||||
private checkDupTab = () => {
|
||||
const dupTabMsg = lichess.storage.make('storm.tab');
|
||||
dupTabMsg.fire(this.data.puzzles[0].id);
|
||||
|
|
Loading…
Reference in New Issue