Resolve #129: Allow liking plays during revealing.

This commit is contained in:
Gareth Latty
2020-05-16 16:00:24 +01:00
parent 492d439929
commit d3d0d997df
21 changed files with 478 additions and 214 deletions
+3 -3
View File
@@ -2160,9 +2160,9 @@
"dev": true
},
"@types/chrome": {
"version": "0.0.111",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.111.tgz",
"integrity": "sha512-Vxo44zBE3EjBW5RZd11GL8aKo14EdcmPxaETtRR4khZHU2BEw0YBLLZMRRgcgRgpbO0pAGhSfUyI8htHJsyTIg==",
"version": "0.0.112",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.112.tgz",
"integrity": "sha512-VJjHiDuF4Xmcn1DuOnGcrMDiGBvoJnaIE6u3ZkhsSz2L16HhnfqIuuM8EAvgQa0ODbNpotHqtPRTKpYoNfpAqQ==",
"dev": true,
"requires": {
"@types/filesystem": "*",
+1 -1
View File
@@ -42,7 +42,7 @@
},
"devDependencies": {
"@types/canvas-confetti": "^1.0.0",
"@types/chrome": "^0.0.111",
"@types/chrome": "^0.0.112",
"@types/chromecast-caf-receiver": "^5.0.5",
"@types/chromecast-caf-sender": "^1.0.3",
"app-manifest-loader": "^2.4.1",
+82 -30
View File
@@ -154,18 +154,24 @@ update wrap shared msg model =
PickPlay id ->
let
makePick r wrapRound =
let
pick =
if r.pick == Just id then
Nothing
else
Just id
in
{ r | pick = pick } |> wrapRound
newRound =
case game.round of
Round.J judging ->
let
pick =
if judging.pick == Just id then
Nothing
makePick judging Round.J
else
Just id
in
{ judging | pick = pick } |> Round.J
Round.R revealing ->
makePick revealing Round.R
_ ->
game.round
@@ -251,17 +257,30 @@ update wrap shared msg model =
Like ->
let
( newRound, action ) =
let
like r roundWrap =
let
likeDetail =
r.likeDetail
insert =
r.pick
|> Maybe.map Set.insert
|> Maybe.withDefault identity
newLikeDetail =
{ likeDetail | liked = insert likeDetail.liked }
in
( { r | pick = Nothing, likeDetail = newLikeDetail } |> roundWrap
, r.pick |> Maybe.map Actions.like |> Maybe.withDefault Cmd.none
)
in
case game.round of
Round.J judging ->
( { judging
| liked =
judging.pick
|> Maybe.map (\p -> Set.insert p judging.liked)
|> Maybe.withDefault judging.liked
}
|> Round.J
, judging.pick |> Maybe.map Actions.like |> Maybe.withDefault Cmd.none
)
like judging Round.J
Round.R revealing ->
like revealing Round.R
_ ->
( game.round, Cmd.none )
@@ -455,15 +474,19 @@ applyGameEvent wrap wrapEvent auth shared gameEvent model =
Events.Timed (Events.WithTime { event, time }) ->
case event of
Events.StartRevealing { plays, drawn } ->
Events.StartRevealing { plays, afterPlaying } ->
case model.game.round of
Round.P r ->
let
{ played, drawn } =
afterPlaying
oldGame =
model.game
newRound =
Round.revealing
(Just { played = played, liked = Set.empty })
r.id
r.czar
r.players
@@ -638,33 +661,62 @@ applyGameEvent wrap wrapEvent auth shared gameEvent model =
in
( { model | game = { game | round = newRound } }, speech )
Events.StartJudging { plays } ->
Events.StartJudging { plays, afterPlaying } ->
let
{ played, drawn } =
afterPlaying
game =
model.game
newRound =
makeNewRound r pick ld known =
case known of
Just k ->
Round.judging pick
ld
r.id
r.czar
r.players
r.call
k
time
False
|> Round.J
Nothing ->
-- TODO: Error.
game.round
( newRound, newHand ) =
case game.round of
Round.P r ->
let
known =
-- TODO: Error on default here.
plays |> Maybe.withDefault []
picked =
r.pick.cards |> Set.fromList
nH =
model.hand
|> List.filter (\c -> not (Set.member c.details.id picked))
|> (\h -> h ++ (drawn |> Maybe.withDefault []))
in
Round.judging r.id r.czar r.players r.call known time False |> Round.J
( makeNewRound r Nothing (Just { played = played, liked = Set.empty }) plays
, nH
)
Round.R r ->
let
known =
plays |> Maybe.withDefault (r.plays |> List.filterMap Play.asKnown)
p =
plays
|> Maybe.withDefault (r.plays |> List.filterMap Play.asKnown)
|> Just
in
Round.judging r.id r.czar r.players r.call known time False |> Round.J
( makeNewRound r r.pick (Just r.likeDetail) p, model.hand )
_ ->
-- TODO: Error
game.round
-- TODO: Error.
( game.round, model.hand )
in
( { model | game = { game | round = newRound } }, Cmd.none )
( { model | game = { game | round = newRound }, hand = newHand }, Cmd.none )
Events.PlayerAway { player } ->
let
+45 -8
View File
@@ -3,6 +3,7 @@ module MassiveDecks.Game.Round exposing
, Data
, Id
, Judging
, LikeDetail
, Pick
, PickState(..)
, Playing
@@ -139,12 +140,31 @@ playing id czar players call played startedAt timedOut =
}
type alias LikeDetail =
{ played : Maybe Play.Id
, liked : Set Play.Id
}
defaultLikeDetail : LikeDetail
defaultLikeDetail =
{ played = Nothing
, liked = Set.empty
}
type alias Revealing =
Data { plays : List Play, lastRevealed : Maybe Play.Id, timedOut : Bool }
Data
{ plays : List Play
, lastRevealed : Maybe Play.Id
, pick : Maybe Play.Id
, likeDetail : LikeDetail
, timedOut : Bool
}
revealing : Id -> User.Id -> Set User.Id -> Card.Call -> List Play -> Time -> Bool -> Revealing
revealing id czar players call plays startedAt timedOut =
revealing : Maybe LikeDetail -> Id -> User.Id -> Set User.Id -> Card.Call -> List Play -> Time -> Bool -> Revealing
revealing likeDetail id czar players call plays startedAt timedOut =
{ id = id
, czar = czar
, players = players
@@ -152,6 +172,8 @@ revealing id czar players call plays startedAt timedOut =
, plays = plays
, lastRevealed = Nothing
, startedAt = startedAt
, pick = Nothing
, likeDetail = likeDetail |> Maybe.withDefault defaultLikeDetail
, timedOut = timedOut
}
@@ -159,18 +181,33 @@ revealing id czar players call plays startedAt timedOut =
{-| A round while the czar is judging a round.
-}
type alias Judging =
Data { plays : List Play.Known, pick : Maybe Play.Id, liked : Set Play.Id, timedOut : Bool }
Data
{ plays : List Play.Known
, pick : Maybe Play.Id
, likeDetail : LikeDetail
, timedOut : Bool
}
judging : Id -> User.Id -> Set User.Id -> Card.Call -> List Play.Known -> Time -> Bool -> Judging
judging id czar players call plays startedAt timedOut =
judging :
Maybe Play.Id
-> Maybe LikeDetail
-> Id
-> User.Id
-> Set User.Id
-> Card.Call
-> List Play.Known
-> Time
-> Bool
-> Judging
judging pick likeDetail id czar players call plays startedAt timedOut =
{ id = id
, czar = czar
, players = players
, call = call
, plays = plays
, pick = Nothing
, liked = Set.empty
, pick = pick
, likeDetail = likeDetail |> Maybe.withDefault defaultLikeDetail
, startedAt = startedAt
, timedOut = timedOut
}
@@ -24,16 +24,25 @@ view wrap auth shared config round =
role =
Player.role (Round.J round) auth.claims.uid
( action, instruction, isCzar ) =
{ action, msg, instruction, isCzar } =
case role of
Player.RCzar ->
( Maybe.map (always Action.Judge) round.pick, Strings.RevealPlaysInstruction, True )
{ action = Maybe.map (always Action.Judge) round.pick
, msg = \p -> p |> PickPlay |> wrap |> Just
, instruction = Strings.RevealPlaysInstruction
, isCzar = True
}
Player.RPlayer ->
( Maybe.andThen (\p -> Maybe.justIf (not (Set.member p round.liked)) Action.Like) round.pick
, Strings.WaitingForCzarInstruction
, False
)
let
canBeLiked play =
Just play /= round.likeDetail.played && not (Set.member play round.likeDetail.liked)
in
{ action = Maybe.andThen (\p -> Maybe.justIf (canBeLiked p) Action.Like) round.pick
, msg = \p -> p |> PickPlay |> wrap |> Maybe.justIf (canBeLiked p)
, instruction = Strings.WaitingForCzarInstruction
, isCzar = False
}
picked =
round.plays
@@ -43,7 +52,7 @@ view wrap auth shared config round =
|> Maybe.withDefault []
details =
round.plays |> List.map (playDetails wrap shared config round.liked)
round.plays |> List.map (playDetails shared config round.likeDetail.liked msg)
in
{ instruction = Just instruction
, action = action
@@ -56,9 +65,19 @@ view wrap auth shared config round =
{- Private -}
playDetails : (Msg -> msg) -> Shared -> Config -> Set Play.Id -> Play.Known -> Plays.Details msg
playDetails wrap shared config liked { id, responses } =
playDetails : Shared -> Config -> Set Play.Id -> (Play.Id -> Maybe msg) -> Play.Known -> Plays.Details msg
playDetails shared config liked msg { id, responses } =
let
maybeMsg =
msg id
cls =
if maybeMsg /= Nothing then
[ HtmlA.class "active" ]
else
[]
cards =
responses
|> List.map (\r -> Response.view shared config Card.Front [] r)
@@ -66,4 +85,4 @@ playDetails wrap shared config liked { id, responses } =
attrs =
[ HtmlA.class "liked" ] |> Maybe.justIf (Set.member id liked) |> Maybe.withDefault []
in
Plays.Details id cards (id |> PickPlay |> wrap |> Just) attrs
Plays.Details id cards maybeMsg (attrs ++ cls)
@@ -1,9 +1,11 @@
module MassiveDecks.Game.Round.Revealing exposing (view)
import Html.Attributes as HtmlA
import MassiveDecks.Card.Call as Call
import MassiveDecks.Card.Model as Card
import MassiveDecks.Card.Play exposing (Play)
import MassiveDecks.Card.Play as Play exposing (Play)
import MassiveDecks.Card.Response as Response
import MassiveDecks.Game.Action.Model as Action
import MassiveDecks.Game.Messages exposing (..)
import MassiveDecks.Game.Model exposing (RoundView)
import MassiveDecks.Game.Player as Player
@@ -14,6 +16,7 @@ import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config)
import MassiveDecks.Pages.Lobby.Model exposing (Auth)
import MassiveDecks.Strings as Strings
import MassiveDecks.Util.Maybe as Maybe
import Set exposing (Set)
view : (Msg -> msg) -> Auth -> Shared -> Config -> Round.Revealing -> RoundView msg
@@ -22,19 +25,38 @@ view wrap auth shared config round =
role =
Player.role (Round.R round) auth.claims.uid
( instruction, isCzar ) =
{ msg, action, instruction, isCzar } =
case role of
Player.RCzar ->
( Strings.RevealPlaysInstruction, True )
let
msgPick { id, responses } =
id |> Reveal |> wrap |> Maybe.justIf (responses == Nothing)
in
{ msg = msgPick
, action = Nothing
, instruction = Strings.RevealPlaysInstruction
, isCzar = True
}
Player.RPlayer ->
( Strings.WaitingForCzarInstruction, False )
let
canBeLiked id =
(Just id /= round.likeDetail.played) && not (Set.member id round.likeDetail.liked)
msgPick { id, responses } =
id |> PickPlay |> wrap |> Maybe.justIf (canBeLiked id && (responses /= Nothing))
in
{ msg = msgPick
, action = Maybe.andThen (\p -> Action.Like |> Maybe.justIf (canBeLiked p)) round.pick
, instruction = Strings.WaitingForCzarInstruction
, isCzar = False
}
slots =
Call.slotCount round.call
plays =
round.plays |> List.map (playDetails wrap shared config slots (role == Player.RCzar))
round.plays |> List.map (playDetails shared config round.likeDetail.liked slots msg)
lastRevealed =
case round.plays |> List.filter (\p -> Just p.id == round.lastRevealed) of
@@ -45,8 +67,8 @@ view wrap auth shared config round =
Nothing
in
{ instruction = Just instruction
, action = Nothing
, content = plays |> Plays.view [ ( "revealing", True ), ( "is-czar", isCzar ) ] Nothing
, action = action
, content = plays |> Plays.view [ ( "revealing", True ), ( "is-czar", isCzar ) ] round.pick
, fillCallWith = lastRevealed |> Maybe.withDefault []
}
@@ -55,12 +77,28 @@ view wrap auth shared config round =
{- Private -}
playDetails : (Msg -> msg) -> Shared -> Config -> Int -> Bool -> Play -> Plays.Details msg
playDetails wrap shared config slots isCzar { id, responses } =
playDetails : Shared -> Config -> Set Play.Id -> Int -> (Play -> Maybe msg) -> Play -> Plays.Details msg
playDetails shared config liked slots msg play =
let
{ id, responses } =
play
maybeMsg =
msg play
cls =
if maybeMsg /= Nothing then
[ HtmlA.class "active" ]
else
[]
cards =
responses
|> Maybe.map (List.map (\r -> Response.view shared config Card.Front [] r))
|> Maybe.withDefault (List.repeat slots (Response.viewUnknown shared []))
attrs =
[ HtmlA.class "liked" ] |> Maybe.justIf (Set.member id liked) |> Maybe.withDefault []
in
Plays.Details id cards (Reveal id |> wrap |> Maybe.justIf isCzar) []
Plays.Details id cards maybeMsg (attrs ++ cls)
+80 -58
View File
@@ -36,7 +36,7 @@ import MassiveDecks.Card.Source.Model as Source exposing (Source)
import MassiveDecks.Cast.Model as Cast
import MassiveDecks.Game.Model as Game exposing (Game)
import MassiveDecks.Game.Player as Player exposing (Player)
import MassiveDecks.Game.Round as Round exposing (Round)
import MassiveDecks.Game.Round as Round exposing (LikeDetail, Round)
import MassiveDecks.Game.Rules as Rules exposing (Rules)
import MassiveDecks.Game.Time as Time
import MassiveDecks.Model exposing (..)
@@ -248,26 +248,26 @@ languageFromCode code =
|> Maybe.withDefault (unknownValue "language code" code)
lobby : Json.Decoder Lobby
lobby =
lobby : Maybe LikeDetail -> Json.Decoder Lobby
lobby ld =
Json.map5 Lobby
(Json.field "users" users)
(Json.field "owner" userId)
(Json.field "config" config)
(Json.maybe (Json.field "game" game |> Json.map Game.emptyModel))
(Json.maybe (Json.field "game" (game ld) |> Json.map Game.emptyModel))
(Json.maybe (Json.field "errors" (Json.list gameStateError)) |> Json.map (Maybe.withDefault []))
game : Json.Decoder Game
game =
Json.map7 Game
(Json.field "round" round)
(Json.field "history" (Json.list completeRound))
(Json.field "playerOrder" (Json.list userId))
(Json.field "players" (Json.dict player))
(Json.field "rules" rules)
(Json.maybe (Json.field "winner" (Json.list userId |> Json.map Set.fromList)))
(Json.maybe (Json.field "paused" Json.bool) |> Json.map (Maybe.withDefault False))
game : Maybe LikeDetail -> Json.Decoder Game
game ld =
Json.succeed Game
|> Json.required "round" (round ld)
|> Json.required "history" (Json.list completeRound)
|> Json.required "playerOrder" (Json.list userId)
|> Json.required "players" (Json.dict player)
|> Json.required "rules" rules
|> Json.optional "winner" (Json.list userId |> Json.map (Set.fromList >> Just)) Nothing
|> Json.optional "paused" Json.bool False
config : Json.Decoder Configure.Config
@@ -757,17 +757,25 @@ playRevealed =
(Json.field "play" (Json.list response))
afterPlaying : Json.Decoder Events.AfterPlaying
afterPlaying =
Json.succeed Events.AfterPlaying
|> Json.optional "played" (playId |> Json.map Just) Nothing
|> Json.optional "drawn" (Json.list response |> Json.map Just) Nothing
startRevealing : Json.Decoder Events.TimedGameEvent
startRevealing =
Json.map2 (\ps -> \dr -> Events.StartRevealing { plays = ps, drawn = dr })
(Json.field "plays" (Json.list playId))
(Json.maybe (Json.field "drawn" (Json.list response)))
Json.succeed (\ps -> \ap -> Events.StartRevealing { plays = ps, afterPlaying = ap })
|> Json.required "plays" (Json.list playId)
|> Json.custom afterPlaying
startJudging : Json.Decoder Events.TimedGameEvent
startJudging =
Json.succeed (\ps -> Events.StartJudging { plays = ps })
Json.succeed (\ps -> \ap -> Events.StartJudging { plays = ps, afterPlaying = ap })
|> Json.optional "plays" (knownPlay |> Json.list |> Json.map Just) Nothing
|> Json.custom afterPlaying
gameEvent : Json.Decoder Events.GameEvent -> Json.Decoder Event
@@ -818,11 +826,18 @@ gameStarted =
sync : Json.Decoder Event
sync =
Json.map4 (\ls -> \h -> \p -> \pa -> Events.Sync { state = ls, hand = h, play = p, partialTimeAnchor = pa })
(Json.field "state" lobby)
(Json.maybe (Json.field "hand" (Json.list response)))
(Json.maybe (Json.field "play" (Json.list Json.string)))
(Json.field "gameTime" Time.partialAnchorDecoder)
let
construct ls h p pa =
Events.Sync { state = ls, hand = h, play = p, partialTimeAnchor = pa }
decodeSync ld =
Json.succeed construct
|> Json.required "state" (lobby ld)
|> Json.optional "hand" (Json.list response |> Json.map Just) Nothing
|> Json.optional "play" (Json.list Json.string |> Json.map Just) Nothing
|> Json.required "gameTime" Time.partialAnchorDecoder
in
Json.maybe (Json.field "likeDetail" likeDetail) |> Json.andThen decodeSync
connection : User.Connection -> Json.Decoder Event
@@ -990,22 +1005,22 @@ styleByName name =
unknownValue "style" name
round : Json.Decoder Round
round =
Json.field "stage" Json.string |> Json.andThen roundByName
round : Maybe LikeDetail -> Json.Decoder Round
round ld =
Json.field "stage" Json.string |> Json.andThen (roundByName ld)
roundByName : String -> Json.Decoder Round
roundByName name =
roundByName : Maybe LikeDetail -> String -> Json.Decoder Round
roundByName ld name =
case name of
"Playing" ->
playingRound |> Json.map Round.P
"Revealing" ->
revealingRound |> Json.map Round.R
revealingRound ld |> Json.map Round.R
"Judging" ->
judgingRound |> Json.map Round.J
judgingRound ld |> Json.map Round.J
"Complete" ->
completeRound |> Json.map Round.C
@@ -1021,38 +1036,45 @@ playerSet =
playingRound : Json.Decoder Round.Playing
playingRound =
Json.map7 Round.playing
(Json.field "id" Round.idDecoder)
(Json.field "czar" userId)
(Json.field "players" playerSet)
(Json.field "call" call)
(Json.field "played" playerSet)
(Json.field "startedAt" Time.timeDecoder)
(Json.maybe (Json.field "timedOut" Json.bool) |> Json.map (Maybe.withDefault False))
Json.succeed Round.playing
|> Json.required "id" Round.idDecoder
|> Json.required "czar" userId
|> Json.required "players" playerSet
|> Json.required "call" call
|> Json.required "played" playerSet
|> Json.required "startedAt" Time.timeDecoder
|> Json.optional "timedOut" Json.bool False
revealingRound : Json.Decoder Round.Revealing
revealingRound =
Json.map7 Round.revealing
(Json.field "id" Round.idDecoder)
(Json.field "czar" userId)
(Json.field "players" playerSet)
(Json.field "call" call)
(Json.field "plays" (Json.list play))
(Json.field "startedAt" Time.timeDecoder)
(Json.maybe (Json.field "timedOut" Json.bool) |> Json.map (Maybe.withDefault False))
likeDetail : Json.Decoder Round.LikeDetail
likeDetail =
Json.succeed Round.LikeDetail
|> Json.optional "played" (playId |> Json.map Just) Nothing
|> Json.required "liked" (Json.list playId |> Json.map Set.fromList)
judgingRound : Json.Decoder Round.Judging
judgingRound =
Json.map7 Round.judging
(Json.field "id" Round.idDecoder)
(Json.field "czar" userId)
(Json.field "players" playerSet)
(Json.field "call" call)
(Json.field "plays" (Json.list knownPlay))
(Json.field "startedAt" Time.timeDecoder)
(Json.maybe (Json.field "timedOut" Json.bool) |> Json.map (Maybe.withDefault False))
revealingRound : Maybe Round.LikeDetail -> Json.Decoder Round.Revealing
revealingRound ld =
Json.succeed (Round.revealing ld)
|> Json.required "id" Round.idDecoder
|> Json.required "czar" userId
|> Json.required "players" playerSet
|> Json.required "call" call
|> Json.required "plays" (Json.list play)
|> Json.required "startedAt" Time.timeDecoder
|> Json.optional "timedOut" Json.bool False
judgingRound : Maybe Round.LikeDetail -> Json.Decoder Round.Judging
judgingRound ld =
Json.succeed (Round.judging Nothing ld)
|> Json.required "id" Round.idDecoder
|> Json.required "czar" userId
|> Json.required "players" playerSet
|> Json.required "call" call
|> Json.required "plays" (Json.list knownPlay)
|> Json.required "startedAt" Time.timeDecoder
|> Json.optional "timedOut" Json.bool False
completeRound : Json.Decoder Round.Complete
@@ -1,5 +1,6 @@
module MassiveDecks.Pages.Lobby.Events exposing
( Event(..)
( AfterPlaying
, Event(..)
, GameEvent(..)
, PresenceState(..)
, TimedGameEvent(..)
@@ -63,6 +64,12 @@ type TimedState
| WithTime { event : TimedGameEvent, time : Time }
type alias AfterPlaying =
{ played : Maybe Play.Id
, drawn : Maybe (List Card.Response)
}
type TimedGameEvent
= RoundStarted
{ id : Round.Id
@@ -71,7 +78,7 @@ type TimedGameEvent
, call : Card.Call
, drawn : Maybe (List Card.Response)
}
| StartRevealing { plays : List Play.Id, drawn : Maybe (List Card.Response) }
| StartJudging { plays : Maybe (List Play.Known) }
| StartRevealing { plays : List Play.Id, afterPlaying : AfterPlaying }
| StartJudging { plays : Maybe (List Play.Known), afterPlaying : AfterPlaying }
| RoundFinished { winner : User.Id, playedBy : Dict Play.Id Play.Details }
| PlayRevealed { id : Play.Id, play : List Card.Response }
+6 -9
View File
@@ -19,19 +19,16 @@
}
.judging {
.game-card {
.active .game-card {
@include cards.shadow(colors.$primary);
}
}
.judging.is-czar {
.picked {
.side {
&.is-czar {
.active .game-card {
@include cards.shadow(colors.$secondary);
}
.picked .game-card .side {
border-color: colors.$secondary;
}
}
.game-card {
@include cards.shadow(colors.$secondary);
}
}
+11 -2
View File
@@ -1,8 +1,17 @@
@use "../_colors";
@use "../_cards";
.revealing.is-czar {
.game-card.face-down {
.revealing {
.active .game-card {
@include cards.shadow(colors.$primary);
}
&.is-czar {
.active .game-card {
@include cards.shadow(colors.$secondary);
}
.picked .game-card .side {
border-color: colors.$secondary;
}
}
}
+3 -3
View File
@@ -289,9 +289,9 @@
}
},
"@types/wu": {
"version": "2.1.41",
"resolved": "https://registry.npmjs.org/@types/wu/-/wu-2.1.41.tgz",
"integrity": "sha512-c9F5iHE1b7QF7v+/ur/orN1gkphE363aPwLA5SZxpeSma6FJq4NpMwwLgcByVIdyF6wOgVTe9kM2nWRtKVzT0A==",
"version": "2.1.42",
"resolved": "https://registry.npmjs.org/@types/wu/-/wu-2.1.42.tgz",
"integrity": "sha512-+GTq3oqVD+DY7iANRfazy8YhLlKwxIG2hTCF72W+yuCbnjZ6PSkxju/3emOZ4+lzMJ7ximpY3XnCQSBjIGbYgw==",
"dev": true
},
"@typescript-eslint/eslint-plugin": {
+1 -1
View File
@@ -64,7 +64,7 @@
"@types/qs": "^6.9.2",
"@types/source-map-support": "^0.5.1",
"@types/uuid": "^7.0.3",
"@types/wu": "^2.1.41",
"@types/wu": "^2.1.42",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"eslint": "^7.0.0",
@@ -29,6 +29,7 @@ class LikeActions extends Actions.Implementation<
if (
lobby.game.round.verifyStage<Round.Revealing | Round.Judging>(
action,
"Revealing",
"Judging"
)
) {
@@ -1,9 +1,11 @@
import * as Play from "../../games/cards/play";
import * as StartRevealing from "./start-revealing";
import * as Card from "../../games/cards/card";
/**
* Indicates the czar has finished revealing the plays and is now picking a winner.
*/
export interface StartJudging {
export interface StartJudging extends StartRevealing.AfterPlaying {
event: "StartJudging";
/**
* The plays that are to be judged. If the revealing stage was played, this won't be included as the data will have
@@ -12,7 +14,13 @@ export interface StartJudging {
plays?: Play.Revealed[];
}
export const of = (plays?: Play.Revealed[]): StartJudging => ({
export const of = (
plays?: Play.Revealed[],
played?: Play.Id,
drawn?: Card.Response[]
): StartJudging => ({
event: "StartJudging",
plays,
played,
drawn,
});
@@ -5,17 +5,32 @@ import * as Card from "../../games/cards/card";
* Indicates players have finished playing into the round and now the czar
* should reveal the plays.
*/
export interface StartRevealing {
export interface StartRevealing extends AfterPlaying {
event: "StartRevealing";
plays: Play.Id[];
}
/**
* Details in an event after finishing playing.
*/
export interface AfterPlaying {
/**
* The id of the play the user receiving this event played, if they did play one.
*/
played?: Play.Id;
/**
* The cards drawn by the player receiving this event.
*/
drawn?: Card.Response[];
}
export const of = (
plays: Play.Id[],
played?: Play.Id,
drawn?: Card.Response[]
): StartRevealing => ({
event: "StartRevealing",
plays,
...(drawn !== undefined ? { drawn } : {}),
played,
drawn,
});
+5 -1
View File
@@ -1,6 +1,7 @@
import * as Card from "../../games/cards/card";
import { Hand } from "../../games/cards/hand";
import * as Lobby from "../../lobby";
import { LikeDetail } from "../../games/game/round/public";
/**
* Synchronise the game state.
@@ -10,17 +11,20 @@ export interface Sync {
state: Lobby.Public;
hand?: Hand;
play?: Card.Id[];
likeDetail?: LikeDetail;
gameTime: number;
}
export const of = (
state: Lobby.Public,
hand?: Hand,
play?: Card.Id[]
play?: Card.Id[],
likeDetail?: LikeDetail
): Sync => ({
event: "Sync",
state,
...(hand !== undefined ? { hand } : {}),
...(play !== undefined ? { play } : {}),
...(likeDetail !== undefined ? { likeDetail } : {}),
gameTime: Date.now(),
});
+95 -17
View File
@@ -11,7 +11,9 @@ import * as RoundStageTimerDone from "../../timeout/round-stage-timer-done";
import * as Timeout from "../../timeout";
import * as Event from "../../event";
import * as StartJudging from "../../events/game-event/start-judging";
import * as StartRevealing from "../../events/game-event/start-revealing";
import * as Rules from "../rules";
import * as Game from "../game";
export type Round = Playing | Revealing | Judging | Complete;
@@ -45,12 +47,13 @@ export abstract class Base<TStage extends Stage> {
*/
public verifyStage<TRound extends Round>(
action: Action,
expected: TRound["stage"]
...expected: TRound["stage"][]
): this is TRound {
if (this.stage !== expected) {
throw new IncorrectRoundStageError(action, this.stage, expected);
if (expected.some((n) => n == this.stage)) {
return true;
} else {
throw new IncorrectRoundStageError(action, this.stage, ...expected);
}
return true;
}
}
@@ -182,7 +185,8 @@ export class Judging extends Base<"Judging"> implements Timed {
public start(
rules: Rules.Rules,
previouslyRevealed: boolean
previouslyRevealed: boolean,
newCardsAndPlayedByPlayer: Map<User.Id, StartRevealing.AfterPlaying>
): {
timeouts?: Iterable<Timeout.After>;
events?: Iterable<Event.Distributor>;
@@ -191,7 +195,15 @@ export class Judging extends Base<"Judging"> implements Timed {
const plays = previouslyRevealed
? undefined
: Array.from(this.revealedPlays());
const event = Event.targetAll(StartJudging.of(plays));
if (previouslyRevealed) {
for (const [_, v] of newCardsAndPlayedByPlayer) {
delete v.played;
}
}
const event = Event.additionally(
StartJudging.of(plays),
newCardsAndPlayedByPlayer
);
return {
timeouts: Util.asOptionalIterable(timeout),
events: Util.asOptionalIterable(event),
@@ -271,9 +283,30 @@ export class Revealing extends Base<"Revealing"> implements Timed {
this.timedOut = timedOut;
}
public start(
game: Game.Game
): {
events?: Iterable<Event.Distributor>;
timeouts?: Iterable<Timeout.After>;
} {
const playsToBeRevealed = Array.from(
wu(game.round.plays).map((play) => play.id)
);
const events = Util.asOptionalIterable(
Event.additionally(
StartRevealing.of(playsToBeRevealed),
this.getAfterPlayingDetails(game)
)
);
const timeouts = Util.asOptionalIterable(
RoundStageTimerDone.ifEnabled(game.round, game.rules.stages)
);
return { events, timeouts };
}
public advance(
rules: Rules.Rules,
previouslyRevealed = true
game: Game.Game,
previouslyRevealed: boolean
):
| {
round: Judging;
@@ -283,7 +316,11 @@ export class Revealing extends Base<"Revealing"> implements Timed {
| undefined {
if (StoredPlay.allRevealed(this)) {
const judging = new Judging(this.id, this.czar, this.call, this.plays);
const start = judging.start(rules, previouslyRevealed);
const start = judging.start(
game.rules,
previouslyRevealed,
this.getAfterPlayingDetails(game)
);
return {
round: judging,
...start,
@@ -293,6 +330,36 @@ export class Revealing extends Base<"Revealing"> implements Timed {
}
}
private getAfterPlayingDetails(
game: Game.Game
): Map<User.Id, StartRevealing.AfterPlaying> {
const slotCount = Card.slotCount(game.round.call);
const extraCards =
slotCount > 2 ||
(slotCount === 2 && game.rules.houseRules.packingHeat !== undefined)
? slotCount - 1
: 0;
const newCardsAndPlayedByPlayer = new Map<
User.Id,
StartRevealing.AfterPlaying
>();
for (const play of game.round.plays) {
const idSet = new Set(play.play.map((c) => c.id));
const player = game.players[play.playedBy];
if (player !== undefined) {
player.hand = player.hand.filter((card) => !idSet.has(card.id));
const toDraw = play.play.length - extraCards;
const drawn = game.decks.responses.draw(toDraw);
newCardsAndPlayedByPlayer.set(play.playedBy, {
drawn,
played: play.id,
});
player.hand.push(...drawn);
}
}
return newCardsAndPlayedByPlayer;
}
public waitingFor(): Set<User.Id> | null {
return new Set(this.czar);
}
@@ -363,31 +430,42 @@ export class Playing extends Base<"Playing"> implements Timed {
this.timedOut = timedOut;
}
public advance(): Revealing {
return new Revealing(
public advance(
game: Game.Game,
doNotStart = false
): {
round: Revealing;
events?: Iterable<Event.Distributor>;
timeouts?: Iterable<Timeout.After>;
} {
const revealing = new Revealing(
this.id,
this.czar,
this.call,
Util.shuffled(this.plays)
);
return {
round: revealing,
...(doNotStart ? {} : revealing.start(game)),
};
}
public skipToJudging(
rules: Rules.Rules
game: Game.Game
): {
round: Judging;
timeouts?: Iterable<Timeout.After>;
events?: Iterable<Event.Distributor>;
} {
const revealing = this.advance();
for (const play of revealing.plays) {
const advanceRevealing = this.advance(game, true);
for (const play of advanceRevealing.round.plays) {
play.revealed = true;
}
const advance = revealing.advance(rules, false);
if (advance === undefined) {
const advanceJudging = advanceRevealing.round.advance(game, false);
if (advanceJudging === undefined) {
throw new Error("All plays should have been revealed automatically.");
}
return advance;
return advanceJudging;
}
public waitingFor(): Set<User.Id> | null {
+5
View File
@@ -23,6 +23,11 @@ export interface Playing extends Base, Timed {
played: User.Id[];
}
export interface LikeDetail {
liked: Play.Id[];
played?: Play.Id;
}
export interface Revealing extends Base, Timed {
stage: "Revealing";
plays: Play.PotentiallyRevealed[];
+12 -1
View File
@@ -139,6 +139,7 @@ export class SocketManager {
await Change.apply(server, auth.gc, (lobby) => {
let hand = undefined;
let play = undefined;
let likeDetail = undefined;
if (lobby.game !== undefined) {
const player = lobby.game.players[uid];
if (player !== undefined) {
@@ -150,6 +151,16 @@ export class SocketManager {
if (potentialPlay !== undefined) {
play = potentialPlay.play.map((card) => card.id);
}
const round = lobby.game.round;
const stage = round.stage;
if (stage === "Revealing" || stage === "Judging") {
const liked = round.plays
.filter((p) => p.likes.some((l) => l === uid))
.map((p) => p.id);
const played = round.plays.find((p) => p.playedBy === uid)
?.id;
likeDetail = { played, liked };
}
}
const user = lobby.users[uid];
@@ -158,7 +169,7 @@ export class SocketManager {
lobby,
events: [
Event.targetOnly(
Sync.of(Lobby.censor(lobby), hand, play),
Sync.of(Lobby.censor(lobby), hand, play, likeDetail),
uid
),
],
+13 -52
View File
@@ -7,6 +7,8 @@ import * as Timeout from "../timeout";
import * as Util from "../util";
import * as RoundStageTimerDone from "./round-stage-timer-done";
import * as Rules from "../games/rules";
import * as User from "../user";
import * as Game from "../games/game";
/**
* Indicates that the round should start the revealing phase if it is appropriate
* to do so.
@@ -49,59 +51,18 @@ export const handle: Timeout.Handler<FinishedPlaying> = (
// We discard the plays first so if the deck runs out, all the cards get
// rotated in.
const responses = game.decks.responses;
for (const play of game.round.plays) {
responses.discard(play.play);
game.decks.responses.discard(play.play);
}
const slotCount = Card.slotCount(game.round.call);
const extraCards =
slotCount > 2 ||
(slotCount === 2 && game.rules.houseRules.packingHeat !== undefined)
? slotCount - 1
: 0;
const newCardsByPlayer = new Map();
for (const play of game.round.plays) {
const idSet = new Set(play.play.map((c) => c.id));
const player = game.players[play.playedBy];
if (player !== undefined) {
player.hand = player.hand.filter((card) => !idSet.has(card.id));
const toDraw = play.play.length - extraCards;
const drawn = responses.draw(toDraw);
newCardsByPlayer.set(play.playedBy, { drawn });
player.hand.push(...drawn);
}
}
if (game.rules.stages.revealing === undefined) {
const { round, events, timeouts } = game.round.skipToJudging(game.rules);
game.round = round;
return {
lobby,
events,
timeouts,
};
} else {
game.round = game.round.advance();
const playsToBeRevealed = Array.from(
wu(game.round.plays).map((play) => play.id)
);
const events = [
Event.additionally(
StartRevealing.of(playsToBeRevealed),
newCardsByPlayer
),
];
const timeouts = Util.asOptionalIterable(
RoundStageTimerDone.ifEnabled(game.round, game.rules.stages)
);
return {
lobby,
events: events,
timeouts: timeouts,
};
}
const { round, events, timeouts } =
game.rules.stages.revealing === undefined
? game.round.skipToJudging(game)
: game.round.advance(game);
game.round = round;
return {
lobby,
events,
timeouts,
};
};
+1 -1
View File
@@ -30,7 +30,7 @@ export const handle: Timeout.Handler<FinishedRevealing> = (
if (round.stage !== "Revealing") {
return {};
}
const advanced = round.advance(game.rules);
const advanced = round.advance(game, true);
if (advanced === undefined) {
return {};
}