puzzle racer lobby
parent
59a9a8bb39
commit
ff3d5ee6a3
|
@ -22,8 +22,8 @@ final class Racer(env: Env)(implicit mat: akka.stream.Materializer) extends Lila
|
|||
|
||||
def create =
|
||||
WithPlayerId { implicit ctx => playerId =>
|
||||
env.racer.api.create(playerId) map { race =>
|
||||
Redirect(routes.Racer.show(race.id.value))
|
||||
env.racer.api.createAndJoin(playerId) map { raceId =>
|
||||
Redirect(routes.Racer.show(raceId.value))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,13 @@ final class Racer(env: Env)(implicit mat: akka.stream.Materializer) extends Lila
|
|||
}
|
||||
}
|
||||
|
||||
def lobby =
|
||||
WithPlayerId { implicit ctx => playerId =>
|
||||
env.racer.lobby.join(playerId) map { raceId =>
|
||||
Redirect(routes.Racer.show(raceId.value))
|
||||
}
|
||||
}
|
||||
|
||||
private def WithPlayerId(f: Context => RacerPlayer.Id => Fu[Result]): Action[Unit] =
|
||||
Open { implicit ctx =>
|
||||
NoBot {
|
||||
|
|
|
@ -24,6 +24,11 @@ object racer {
|
|||
postForm(cls := "racer__create", action := routes.Racer.create)(
|
||||
submitButton(cls := "button button-fat")("Race your friends")
|
||||
)
|
||||
),
|
||||
div(
|
||||
postForm(cls := "racer__lobby", action := routes.Racer.lobby)(
|
||||
submitButton(cls := "button button-fat")("Join a public race")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -120,6 +120,7 @@ GET /racer controllers.Racer.home
|
|||
POST /racer controllers.Racer.create
|
||||
GET /racer/:id controllers.Racer.show(id: String)
|
||||
GET /racer/:id/rematch controllers.Racer.rematch(id: String)
|
||||
POST /racer/lobby controllers.Racer.lobby
|
||||
|
||||
# User Analysis
|
||||
GET /analysis/help controllers.UserAnalysis.help
|
||||
|
|
|
@ -32,6 +32,8 @@ final class Env(
|
|||
|
||||
lazy val api = wire[RacerApi]
|
||||
|
||||
lazy val lobby = wire[RacerLobby]
|
||||
|
||||
lazy val json = wire[RacerJson]
|
||||
|
||||
private val socket = wire[RacerSocket] // requires eager eval
|
||||
|
|
|
@ -30,7 +30,13 @@ final class RacerApi(colls: RacerColls, selector: StormSelector, cacheApi: Cache
|
|||
case None => RacerPlayer.Id.Anon(sessionId)
|
||||
}
|
||||
|
||||
def create(player: RacerPlayer.Id): Fu[RacerRace] =
|
||||
def createAndJoin(player: RacerPlayer.Id): Fu[RacerRace.Id] =
|
||||
create(player).map { id =>
|
||||
join(id, player)
|
||||
id
|
||||
}
|
||||
|
||||
def create(player: RacerPlayer.Id): Fu[RacerRace.Id] =
|
||||
selector.apply map { puzzles =>
|
||||
val race = RacerRace
|
||||
.make(
|
||||
|
@ -38,7 +44,7 @@ final class RacerApi(colls: RacerColls, selector: StormSelector, cacheApi: Cache
|
|||
puzzles = puzzles.grouped(2).flatMap(_.headOption).toList
|
||||
)
|
||||
store.put(race.id, race)
|
||||
race
|
||||
race.id
|
||||
}
|
||||
|
||||
private val rematchQueue =
|
||||
|
@ -55,9 +61,9 @@ final class RacerApi(colls: RacerColls, selector: StormSelector, cacheApi: Cache
|
|||
fuccess(found.id)
|
||||
case None =>
|
||||
rematchQueue {
|
||||
create(player) map { rematch =>
|
||||
save(race.copy(rematch = rematch.id.some))
|
||||
rematch.id
|
||||
createAndJoin(player) map { rematchId =>
|
||||
save(race.copy(rematch = rematchId.some))
|
||||
rematchId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ final class RacerJson(stormJson: StormJson, sign: StormSign, lightUserSync: Ligh
|
|||
.obj(
|
||||
"race" -> Json
|
||||
.obj("id" -> race.id.value)
|
||||
.add("alreadyStarted" -> race.hasStarted),
|
||||
.add("lobby", race.isLobby),
|
||||
"player" -> player,
|
||||
"puzzles" -> race.puzzles
|
||||
) ++ state(race)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package lila.racer
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext
|
||||
import lila.user.User
|
||||
|
||||
final class RacerLobby(api: RacerApi)(implicit ec: ExecutionContext, system: akka.actor.ActorSystem) {
|
||||
|
||||
def join(player: RacerPlayer.Id): Fu[RacerRace.Id] = workQueue {
|
||||
currentRace flatMap {
|
||||
case race if race.players.sizeIs >= RacerRace.maxPlayers => makeNewRaceFor(player)
|
||||
case race if race.startsInMillis.exists(_ < 4000) => makeNewRaceFor(player)
|
||||
case race => fuccess(race.id)
|
||||
} map { raceId =>
|
||||
api.join(raceId, player)
|
||||
raceId
|
||||
}
|
||||
}
|
||||
|
||||
private val workQueue =
|
||||
new lila.hub.DuctSequencer(
|
||||
maxSize = 128,
|
||||
timeout = 20 seconds,
|
||||
name = "racer.lobby"
|
||||
)
|
||||
|
||||
private val fallbackRace = RacerRace.make(RacerPlayer.lichess, Nil)
|
||||
|
||||
private var currentId: Fu[RacerRace.Id] = api create RacerPlayer.lichess
|
||||
|
||||
private def currentRace: Fu[RacerRace] = currentId.map(api.get) dmap { _ | fallbackRace }
|
||||
|
||||
private def makeNewRaceFor(player: RacerPlayer.Id): Fu[RacerRace.Id] = {
|
||||
currentId = api create RacerPlayer.lichess
|
||||
currentId
|
||||
}
|
||||
}
|
|
@ -29,5 +29,7 @@ object RacerPlayer {
|
|||
else User(str)
|
||||
}
|
||||
|
||||
val lichess = Id.User("Lichess")
|
||||
|
||||
def make(id: Id) = RacerPlayer(id = id, score = 0, createdAt = DateTime.now, end = false)
|
||||
}
|
||||
|
|
|
@ -51,6 +51,8 @@ case class RacerRace(
|
|||
)
|
||||
|
||||
def finished = players.forall(_.end)
|
||||
|
||||
def isLobby = owner == RacerPlayer.lichess
|
||||
}
|
||||
|
||||
object RacerRace {
|
||||
|
@ -62,9 +64,7 @@ object RacerRace {
|
|||
def make(owner: RacerPlayer.Id, puzzles: List[StormPuzzle]) = RacerRace(
|
||||
_id = Id(lila.common.ThreadLocalRandom nextString 8),
|
||||
owner = owner,
|
||||
players = List(
|
||||
RacerPlayer.make(owner)
|
||||
),
|
||||
players = Nil,
|
||||
puzzles = puzzles,
|
||||
createdAt = DateTime.now,
|
||||
startsAt = none,
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
|
||||
.button-navaway {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ export interface RacerData extends UpdatableData {
|
|||
|
||||
export interface Race {
|
||||
id: string;
|
||||
lobby?: boolean;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
|
|
|
@ -25,11 +25,13 @@ export default function (ctrl: RacerCtrl): VNode {
|
|||
const selectScreen = (ctrl: RacerCtrl): MaybeVNodes => {
|
||||
switch (ctrl.status()) {
|
||||
case 'pre':
|
||||
return [
|
||||
waitingToStart(),
|
||||
ctrl.raceFull() ? undefined : ctrl.isPlayer() ? renderLink(ctrl) : renderJoin(ctrl),
|
||||
comboZone(ctrl),
|
||||
];
|
||||
return ctrl.race.lobby
|
||||
? [waitingToStart(), comboZone(ctrl)]
|
||||
: [
|
||||
waitingToStart(),
|
||||
ctrl.raceFull() ? undefined : ctrl.isPlayer() ? renderLink(ctrl) : renderJoin(ctrl),
|
||||
comboZone(ctrl),
|
||||
];
|
||||
case 'racing':
|
||||
if (ctrl.isPlayer())
|
||||
return ctrl.run.endAt
|
||||
|
@ -38,25 +40,25 @@ const selectScreen = (ctrl: RacerCtrl): MaybeVNodes => {
|
|||
h('div.racer__end', [
|
||||
h('h2', 'Your time is up!'),
|
||||
h('div.race__end__players', playersInTheRace(ctrl)),
|
||||
waitForRematch(),
|
||||
newRaceButton('.button-empty'),
|
||||
ctrl.race.lobby ? [newRaceForm(ctrl)] : [waitForRematch(), newRaceForm(ctrl)],
|
||||
]),
|
||||
comboZone(ctrl),
|
||||
]
|
||||
: [playerScore(ctrl.run), renderClock(ctrl.run, ctrl.endNow), comboZone(ctrl)];
|
||||
return [
|
||||
spectating(),
|
||||
h('div.racer__spectating', [playersInTheRace(ctrl), waitForRematch(), newRaceButton('.button-empty')]),
|
||||
h('div.racer__spectating', [playersInTheRace(ctrl), ctrl.race.lobby ? newRaceForm(ctrl) : waitForRematch()]),
|
||||
comboZone(ctrl),
|
||||
];
|
||||
case 'post':
|
||||
const nextRace = ctrl.race.lobby ? newRaceForm(ctrl) : rematchButton(ctrl);
|
||||
return ctrl.isPlayer()
|
||||
? [
|
||||
playerScore(ctrl.run),
|
||||
h('div.racer__post', [h('h2', 'Race complete!'), yourRank(ctrl), rematchButton(ctrl)]),
|
||||
h('div.racer__post', [h('h2', 'Race complete!'), yourRank(ctrl), nextRace]),
|
||||
comboZone(ctrl),
|
||||
]
|
||||
: [spectating(), h('div.racer__post', [h('h2', 'Race complete!'), rematchButton(ctrl)]), comboZone(ctrl)];
|
||||
: [spectating(), h('div.racer__post', [h('h2', 'Race complete!'), nextRace]), comboZone(ctrl)];
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -69,10 +71,7 @@ const waitingToStart = () =>
|
|||
);
|
||||
|
||||
const spectating = () =>
|
||||
h(
|
||||
'div.puz-side__top.puz-side__start',
|
||||
h('div.puz-side__start__text', [puzzleRacer(), h('span', 'Spectating the race')])
|
||||
);
|
||||
h('div.puz-side__top.puz-side__start', h('div.puz-side__start__text', [puzzleRacer(), h('span', 'Spectating')]));
|
||||
|
||||
const comboZone = (ctrl: RacerCtrl) => h('div.puz-side__table', [renderCombo(config)(ctrl.run)]);
|
||||
|
||||
|
@ -132,13 +131,20 @@ const waitForRematch = () =>
|
|||
'Wait for rematch'
|
||||
);
|
||||
|
||||
const newRaceButton = (cls: string = '') =>
|
||||
const newRaceForm = (ctrl: RacerCtrl) =>
|
||||
h(
|
||||
`a.racer__new-race.button.button-navaway${cls}`,
|
||||
'form',
|
||||
{
|
||||
attrs: { href: '/racer' },
|
||||
attrs: {
|
||||
action: ctrl.race.lobby ? '/racer/lobby' : '/racer',
|
||||
method: 'post',
|
||||
},
|
||||
},
|
||||
'New race'
|
||||
[
|
||||
h(`button.racer__new-race.button.button-navaway${ctrl.race.lobby ? '.button-fat' : '.button-empty'}`, [
|
||||
ctrl.race.lobby ? 'Next race' : 'New race',
|
||||
]),
|
||||
]
|
||||
);
|
||||
|
||||
const rematchButton = (ctrl: RacerCtrl) =>
|
||||
|
|
Loading…
Reference in New Issue