From d3d0d997df1ca461ed527565eeb7ca0dd931960f Mon Sep 17 00:00:00 2001 From: Gareth Latty Date: Sat, 16 May 2020 16:00:24 +0100 Subject: [PATCH] Resolve #129: Allow liking plays during revealing. --- client/package-lock.json | 6 +- client/package.json | 2 +- client/src/elm/MassiveDecks/Game.elm | 112 ++++++++++---- client/src/elm/MassiveDecks/Game/Round.elm | 53 ++++++- .../elm/MassiveDecks/Game/Round/Judging.elm | 39 +++-- .../elm/MassiveDecks/Game/Round/Revealing.elm | 58 ++++++-- .../src/elm/MassiveDecks/Models/Decoders.elm | 138 ++++++++++-------- .../elm/MassiveDecks/Pages/Lobby/Events.elm | 13 +- client/src/scss/game/_judging.scss | 15 +- client/src/scss/game/_revealing.scss | 13 +- server/package-lock.json | 6 +- server/package.json | 2 +- .../src/ts/action/game-action/player/like.ts | 1 + .../src/ts/events/game-event/start-judging.ts | 12 +- .../ts/events/game-event/start-revealing.ts | 19 ++- server/src/ts/events/user-event/sync.ts | 6 +- server/src/ts/games/game/round.ts | 112 +++++++++++--- server/src/ts/games/game/round/public.ts | 5 + server/src/ts/socket-manager.ts | 13 +- server/src/ts/timeout/finished-playing.ts | 65 ++------- server/src/ts/timeout/finished-revealing.ts | 2 +- 21 files changed, 478 insertions(+), 214 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index c5b0157..d28f840 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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": "*", diff --git a/client/package.json b/client/package.json index 0b3b129..05c3461 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/elm/MassiveDecks/Game.elm b/client/src/elm/MassiveDecks/Game.elm index cd7d3fa..fe31a8d 100644 --- a/client/src/elm/MassiveDecks/Game.elm +++ b/client/src/elm/MassiveDecks/Game.elm @@ -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 diff --git a/client/src/elm/MassiveDecks/Game/Round.elm b/client/src/elm/MassiveDecks/Game/Round.elm index 82f4832..3e89914 100644 --- a/client/src/elm/MassiveDecks/Game/Round.elm +++ b/client/src/elm/MassiveDecks/Game/Round.elm @@ -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 } diff --git a/client/src/elm/MassiveDecks/Game/Round/Judging.elm b/client/src/elm/MassiveDecks/Game/Round/Judging.elm index a50fdf9..5d3de4c 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Judging.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Judging.elm @@ -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) diff --git a/client/src/elm/MassiveDecks/Game/Round/Revealing.elm b/client/src/elm/MassiveDecks/Game/Round/Revealing.elm index fb5c743..b4424df 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Revealing.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Revealing.elm @@ -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) diff --git a/client/src/elm/MassiveDecks/Models/Decoders.elm b/client/src/elm/MassiveDecks/Models/Decoders.elm index 1b824fa..57d1648 100644 --- a/client/src/elm/MassiveDecks/Models/Decoders.elm +++ b/client/src/elm/MassiveDecks/Models/Decoders.elm @@ -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 diff --git a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm index e0de305..bade729 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby/Events.elm @@ -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 } diff --git a/client/src/scss/game/_judging.scss b/client/src/scss/game/_judging.scss index 1f78c7a..7715cc8 100644 --- a/client/src/scss/game/_judging.scss +++ b/client/src/scss/game/_judging.scss @@ -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); - } } diff --git a/client/src/scss/game/_revealing.scss b/client/src/scss/game/_revealing.scss index 2ff259d..1b9e15b 100644 --- a/client/src/scss/game/_revealing.scss +++ b/client/src/scss/game/_revealing.scss @@ -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; + } + } } diff --git a/server/package-lock.json b/server/package-lock.json index 5ae3f04..fa1ab8d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -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": { diff --git a/server/package.json b/server/package.json index b9ea912..e07a060 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/src/ts/action/game-action/player/like.ts b/server/src/ts/action/game-action/player/like.ts index 3924bf7..cf9ffef 100644 --- a/server/src/ts/action/game-action/player/like.ts +++ b/server/src/ts/action/game-action/player/like.ts @@ -29,6 +29,7 @@ class LikeActions extends Actions.Implementation< if ( lobby.game.round.verifyStage( action, + "Revealing", "Judging" ) ) { diff --git a/server/src/ts/events/game-event/start-judging.ts b/server/src/ts/events/game-event/start-judging.ts index f0e808a..a515ec2 100644 --- a/server/src/ts/events/game-event/start-judging.ts +++ b/server/src/ts/events/game-event/start-judging.ts @@ -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, }); diff --git a/server/src/ts/events/game-event/start-revealing.ts b/server/src/ts/events/game-event/start-revealing.ts index 3e59b49..e3b30a9 100644 --- a/server/src/ts/events/game-event/start-revealing.ts +++ b/server/src/ts/events/game-event/start-revealing.ts @@ -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, }); diff --git a/server/src/ts/events/user-event/sync.ts b/server/src/ts/events/user-event/sync.ts index 35a6d88..3898b94 100644 --- a/server/src/ts/events/user-event/sync.ts +++ b/server/src/ts/events/user-event/sync.ts @@ -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(), }); diff --git a/server/src/ts/games/game/round.ts b/server/src/ts/games/game/round.ts index 7785edd..dae7a57 100644 --- a/server/src/ts/games/game/round.ts +++ b/server/src/ts/games/game/round.ts @@ -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 { */ public verifyStage( 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 ): { timeouts?: Iterable; events?: Iterable; @@ -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; + timeouts?: Iterable; + } { + 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 { + 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 | 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; + timeouts?: Iterable; + } { + 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; events?: Iterable; } { - 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 | null { diff --git a/server/src/ts/games/game/round/public.ts b/server/src/ts/games/game/round/public.ts index 32ffa0f..724d1fe 100644 --- a/server/src/ts/games/game/round/public.ts +++ b/server/src/ts/games/game/round/public.ts @@ -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[]; diff --git a/server/src/ts/socket-manager.ts b/server/src/ts/socket-manager.ts index 3117a70..482c519 100644 --- a/server/src/ts/socket-manager.ts +++ b/server/src/ts/socket-manager.ts @@ -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 ), ], diff --git a/server/src/ts/timeout/finished-playing.ts b/server/src/ts/timeout/finished-playing.ts index a639ea8..691c904 100644 --- a/server/src/ts/timeout/finished-playing.ts +++ b/server/src/ts/timeout/finished-playing.ts @@ -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 = ( // 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, + }; }; diff --git a/server/src/ts/timeout/finished-revealing.ts b/server/src/ts/timeout/finished-revealing.ts index a04a2ae..903a507 100644 --- a/server/src/ts/timeout/finished-revealing.ts +++ b/server/src/ts/timeout/finished-revealing.ts @@ -30,7 +30,7 @@ export const handle: Timeout.Handler = ( if (round.stage !== "Revealing") { return {}; } - const advanced = round.advance(game.rules); + const advanced = round.advance(game, true); if (advanced === undefined) { return {}; }