puzzle racer lobby

puzzle-racer
Thibault Duplessis 2021-03-14 11:48:38 +01:00
parent 59a9a8bb39
commit ff3d5ee6a3
12 changed files with 97 additions and 29 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@
.button-navaway {
display: block;
width: 100%;
margin-top: 2em;
}

View File

@ -27,6 +27,7 @@ export interface RacerData extends UpdatableData {
export interface Race {
id: string;
lobby?: boolean;
}
export interface Player {

View File

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