puzzle racer WIP

puzzle-racer-road-translate
Thibault Duplessis 2021-03-04 20:27:44 +01:00
parent 4037ba6cd4
commit 10094de7c0
13 changed files with 154 additions and 49 deletions

View File

@ -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
)

View File

@ -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
}

View File

@ -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)

View File

@ -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 }
}

View File

@ -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))
}
}
}

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, 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)
}

View File

@ -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 {

View File

@ -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}"
}
}
}

View File

@ -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),
};

View File

@ -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),
};
}

View File

@ -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 {

View File

@ -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> =>

View File

@ -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);