From fa8d80bec21fa8fb2a5b4d01a4b5c2515acbd6a6 Mon Sep 17 00:00:00 2001 From: Gareth Latty Date: Wed, 31 Jul 2019 11:07:35 +0100 Subject: [PATCH] Add game history view. --- client/src/elm/MassiveDecks/Game.elm | 99 ++++++++++++------- client/src/elm/MassiveDecks/Game/History.elm | 87 ++++++++++++++++ client/src/elm/MassiveDecks/Game/Messages.elm | 1 + client/src/elm/MassiveDecks/Game/Model.elm | 2 + client/src/elm/MassiveDecks/Game/Round.elm | 6 ++ .../elm/MassiveDecks/Game/Round/Complete.elm | 18 +--- .../src/elm/MassiveDecks/Game/Round/Plays.elm | 23 ++++- client/src/elm/MassiveDecks/Pages/Lobby.elm | 8 +- client/src/elm/MassiveDecks/Strings.elm | 1 + .../elm/MassiveDecks/Strings/Languages/En.elm | 3 + client/src/scss/card/variables.scss | 6 +- client/src/scss/game.scss | 25 ++++- client/src/scss/game/history.scss | 57 +++++++++++ client/src/scss/game/plays.scss | 8 +- client/src/scss/pages/lobby.scss | 10 +- .../src/ts/action/game-action/czar/judge.ts | 1 + 16 files changed, 288 insertions(+), 67 deletions(-) create mode 100644 client/src/elm/MassiveDecks/Game/History.elm create mode 100644 client/src/scss/game/history.scss diff --git a/client/src/elm/MassiveDecks/Game.elm b/client/src/elm/MassiveDecks/Game.elm index 185b310..306b9d0 100644 --- a/client/src/elm/MassiveDecks/Game.elm +++ b/client/src/elm/MassiveDecks/Game.elm @@ -19,10 +19,10 @@ import MassiveDecks.Card.Model as Card import MassiveDecks.Card.Play as Play exposing (Play) import MassiveDecks.Components as Components import MassiveDecks.Game.Action as Action -import MassiveDecks.Game.Action.Model as Action exposing (Action) +import MassiveDecks.Game.Action.Model exposing (Action) +import MassiveDecks.Game.History as History import MassiveDecks.Game.Messages exposing (..) import MassiveDecks.Game.Model exposing (..) -import MassiveDecks.Game.Player as Player exposing (Player) import MassiveDecks.Game.Round as Round exposing (Round) import MassiveDecks.Game.Round.Complete as Complete import MassiveDecks.Game.Round.Judging as Judging @@ -191,40 +191,53 @@ update msg model = Redraw -> ( model, Actions.redraw ) + ToggleHistoryView -> + ( { model | viewingHistory = not model.viewingHistory }, Cmd.none ) + subscriptions : Model -> Sub Global.Msg -subscriptions model = +subscriptions _ = Sub.none -view : Shared -> Lobby.Auth -> Config -> Dict User.Id User -> Model -> Html Global.Msg -view shared auth config users model = +view : Shared -> Lobby.Auth -> String -> Config -> Dict User.Id User -> Model -> Html Global.Msg +view shared auth name config users model = let - { instruction, action, content, fillCallWith } = + gameView = + if model.viewingHistory then + History.view shared config users name model.game.history + + else + viewRound shared auth config users model + in + Html.div [ HtmlA.id "game" ] gameView + + +viewRound : Shared -> Lobby.Auth -> Config -> Dict User.Id User -> Model -> List (Html Global.Msg) +viewRound shared auth config users model = + let + ( call, { instruction, action, content, fillCallWith } ) = case model.completeRound of Just completeRound -> - Complete.view shared (Round.C completeRound /= game.round) config users completeRound + ( completeRound.call, Complete.view shared True config users completeRound ) Nothing -> case game.round of Round.P round -> - Playing.view auth config model round + ( round.call, Playing.view auth config model round ) Round.R round -> - Revealing.view auth config round + ( round.call, Revealing.view auth config round ) Round.J round -> - Judging.view auth config round + ( round.call, Judging.view auth config round ) Round.C round -> - Complete.view shared False config users round + ( round.call, Complete.view shared False config users round ) game = model.game - { call } = - Round.data game.round - parts = fillCallWith |> List.map .body @@ -257,27 +270,23 @@ view shared auth config users model = in Html.div [ HtmlA.id "context-help" ] helpContent in - Html.div [ HtmlA.id "game" ] - [ minorActions shared auth game - , help - , Html.div [ HtmlA.class "round" ] - [ renderedCall - , viewAction shared action - ] - , content - , Html.div [ HtmlA.class "scroll-top-spacer" ] [] + [ minorActions shared auth game + , help + , Html.div [ HtmlA.class "round" ] [ renderedCall, viewAction shared action ] + , content + , Html.div [ HtmlA.class "scroll-top-spacer" ] [] - -- TODO: Hide this when at top. Waiting on native elm scroll events, as currently we'd have to ping constantly. - , Html.div [ HtmlA.class "scroll-top" ] - [ Wl.button - [ WlA.flat - , WlA.fab - , WlA.inverted - , ScrollToTop |> lift |> HtmlE.onClick - ] - [ Icon.view Icon.arrowUp ] + -- TODO: Hide this when at top. Waiting on native elm scroll events, as currently we'd have to ping constantly. + , Html.div [ HtmlA.class "scroll-top" ] + [ Wl.button + [ WlA.flat + , WlA.fab + , WlA.inverted + , ScrollToTop |> lift |> HtmlE.onClick ] + [ Icon.view Icon.arrowUp ] ] + ] minorActions : Shared -> Lobby.Auth -> Game -> Html Global.Msg @@ -288,13 +297,24 @@ minorActions shared auth game = in Html.div [ HtmlA.id "minor-actions" ] (List.filterMap identity - [ Maybe.map2 (\score -> \reboot -> rebootButton shared score reboot) + [ Just (historyButton shared) + , Maybe.map2 (\score -> \reboot -> rebootButton shared score reboot) (localPlayer |> Maybe.map .score) game.rules.houseRules.reboot ] ) +historyButton : Shared -> Html Global.Msg +historyButton shared = + Components.iconButton + [ HtmlA.id "history-button" + , Strings.ViewGameHistoryAction |> Lang.title shared + , ToggleHistoryView |> lift |> HtmlE.onClick + ] + Icon.history + + rebootButton : Shared -> Int -> Rules.Reboot -> Html Global.Msg rebootButton shared score reboot = Components.iconButton @@ -401,20 +421,23 @@ applyGameEvent auth gameEvent model = newPlayers = game.players |> Dict.update winner (Maybe.map (\p -> { p | score = p.score + 1 })) - newRound = - case model.game.round of + ( newRound, history ) = + case game.round of Round.J r -> let plays = r.plays |> List.filterMap (resolvePlayedBy playedBy) |> Dict.fromList + + complete = + Round.complete r.id r.czar r.players r.call plays winner in - Round.complete r.id r.czar r.players r.call plays winner |> Round.C + ( complete |> Round.C, complete :: game.history ) _ -> -- TODO: Error - model.game.round + ( game.round, game.history ) in - ( { model | game = { game | round = newRound, players = newPlayers } }, Cmd.none ) + ( { model | game = { game | round = newRound, players = newPlayers, history = history } }, Cmd.none ) Events.RoundStarted { id, czar, players, call, drawn } -> let diff --git a/client/src/elm/MassiveDecks/Game/History.elm b/client/src/elm/MassiveDecks/Game/History.elm new file mode 100644 index 0000000..181a512 --- /dev/null +++ b/client/src/elm/MassiveDecks/Game/History.elm @@ -0,0 +1,87 @@ +module MassiveDecks.Game.History exposing (view) + +import Dict exposing (Dict) +import FontAwesome.Solid as Icon +import Html exposing (Html) +import Html.Attributes as HtmlA +import Html.Events as HtmlE +import Html.Keyed as HtmlK +import MassiveDecks.Card.Call as Call +import MassiveDecks.Card.Model as Card +import MassiveDecks.Card.Response as Response +import MassiveDecks.Components as Components +import MassiveDecks.Game.Messages exposing (Msg(..)) +import MassiveDecks.Game.Round as Round +import MassiveDecks.Game.Round.Plays as Plays +import MassiveDecks.Messages as Global +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Pages.Lobby.Configure.Model exposing (Config) +import MassiveDecks.Pages.Lobby.Messages as Lobby +import MassiveDecks.Strings as Strings +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.User as User exposing (User) +import MassiveDecks.Util.Maybe as Maybe + + +view : Shared -> Config -> Dict User.Id User -> String -> List Round.Complete -> List (Html Global.Msg) +view shared config users name history = + [ Html.div [ HtmlA.id "minor-actions" ] + [ Components.iconButton + [ HtmlA.id "return-to-game-button" + , Strings.ViewGameHistoryAction |> Lang.title shared + , ToggleHistoryView |> lift |> HtmlE.onClick + ] + Icon.arrowLeft + ] + , Html.div [ HtmlA.id "history" ] + [ Html.h2 [] [ Html.text name ] + , HtmlK.ol + [ HtmlA.class "historic-rounds" + , HtmlA.reversed True + , HtmlA.style "counter-reset" + ("historic-round-number " ++ (history |> List.length |> (+) 1 |> String.fromInt)) + ] + (history |> List.map (viewRound shared config users)) + ] + ] + + + +{- Private -} + + +viewRound : Shared -> Config -> Dict User.Id User -> Round.Complete -> ( String, Html msg ) +viewRound shared config users round = + let + winning = + round.plays |> Dict.get round.winner |> Maybe.withDefault [] |> List.map .body + in + ( Round.idString round.id + , Html.li [ HtmlA.class "historic-round" ] + [ Html.div [ HtmlA.class "historic-call with-byline" ] + [ Plays.byLine shared users round.czar (Just Icon.gavel) + , Html.div [] [ Call.viewFilled shared config Card.Front [] winning round.call ] + ] + , HtmlK.ul [ HtmlA.class "plays cards" ] + (round.plays |> Dict.toList |> List.map (viewPlay shared config users round.winner)) + ] + ) + + +viewPlay : Shared -> Config -> Dict User.Id User -> User.Id -> ( User.Id, List Card.Response ) -> ( String, Html msg ) +viewPlay shared config users winner ( id, responses ) = + let + cards = + responses |> List.map (\r -> ( r.details.id, Html.li [] [ Response.view config Card.Front [] r ] )) + in + ( id + , Html.li [ HtmlA.class "with-byline" ] + [ Plays.byLine shared users id (Icon.trophy |> Maybe.justIf (winner == id)) + , HtmlK.ol [ HtmlA.class "play card-set" ] cards + ] + ) + + +lift : Msg -> Global.Msg +lift msg = + msg |> Lobby.GameMsg |> Global.LobbyMsg diff --git a/client/src/elm/MassiveDecks/Game/Messages.elm b/client/src/elm/MassiveDecks/Game/Messages.elm index 7abe92f..a8fc78e 100644 --- a/client/src/elm/MassiveDecks/Game/Messages.elm +++ b/client/src/elm/MassiveDecks/Game/Messages.elm @@ -17,3 +17,4 @@ type Msg | SetPlayStyles PlayStyles | AdvanceRound | Redraw + | ToggleHistoryView diff --git a/client/src/elm/MassiveDecks/Game/Model.elm b/client/src/elm/MassiveDecks/Game/Model.elm index 5911fa1..98308f0 100644 --- a/client/src/elm/MassiveDecks/Game/Model.elm +++ b/client/src/elm/MassiveDecks/Game/Model.elm @@ -26,6 +26,7 @@ type alias Model = , hand : List Card.Response , playStyles : PlayStyles , completeRound : Maybe Round.Complete + , viewingHistory : Bool } @@ -37,6 +38,7 @@ emptyModel game = , hand = [] , playStyles = Dict.empty , completeRound = Nothing + , viewingHistory = False } diff --git a/client/src/elm/MassiveDecks/Game/Round.elm b/client/src/elm/MassiveDecks/Game/Round.elm index edc856f..6cebba6 100644 --- a/client/src/elm/MassiveDecks/Game/Round.elm +++ b/client/src/elm/MassiveDecks/Game/Round.elm @@ -12,6 +12,7 @@ module MassiveDecks.Game.Round exposing , complete , data , idDecoder + , idString , judging , noPick , playing @@ -40,6 +41,11 @@ idDecoder = Json.string |> Json.map Id +idString : Id -> String +idString (Id id) = + id + + {-| The stage of the round. -} type Stage diff --git a/client/src/elm/MassiveDecks/Game/Round/Complete.elm b/client/src/elm/MassiveDecks/Game/Round/Complete.elm index 1a1dffb..6b2d8a0 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Complete.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Complete.elm @@ -1,7 +1,6 @@ module MassiveDecks.Game.Round.Complete exposing (view) import Dict exposing (Dict) -import FontAwesome.Icon as Icon import FontAwesome.Solid as Icon import Html exposing (Html) import Html.Attributes as HtmlA @@ -11,13 +10,12 @@ import MassiveDecks.Card.Response as Response import MassiveDecks.Game.Action.Model as Action import MassiveDecks.Game.Model exposing (..) import MassiveDecks.Game.Round as Round +import MassiveDecks.Game.Round.Plays as Plays import MassiveDecks.Messages as Global import MassiveDecks.Model exposing (..) import MassiveDecks.Pages.Lobby.Configure.Model as Configure exposing (Config) import MassiveDecks.Strings as Strings -import MassiveDecks.Strings.Languages as Lang import MassiveDecks.User as User exposing (User) -import MassiveDecks.Util.Html as Html import MassiveDecks.Util.Maybe as Maybe @@ -40,19 +38,11 @@ viewPlay : Shared -> Config -> Dict User.Id User -> User.Id -> ( User.Id, List C viewPlay shared config users winner ( id, responses ) = let cards = - responses |> List.map (\r -> ( r.details.id, Response.view config Card.Front [] r )) - - playedBy = - users |> Dict.get id |> Maybe.map .name |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) - - byline = - [ Icon.view Icon.trophy |> Maybe.justIf (winner == id) |> Maybe.withDefault Html.nothing - , Html.text playedBy - ] + responses |> List.map (\r -> ( r.details.id, Html.li [] [ Response.view config Card.Front [] r ] )) in ( id - , Html.li [ HtmlA.class "play-with-byline" ] - [ Html.span [ HtmlA.class "byline" ] byline + , Html.li [ HtmlA.class "with-byline" ] + [ Plays.byLine shared users id (Icon.trophy |> Maybe.justIf (winner == id)) , HtmlK.ol [ HtmlA.class "play card-set" ] cards ] ) diff --git a/client/src/elm/MassiveDecks/Game/Round/Plays.elm b/client/src/elm/MassiveDecks/Game/Round/Plays.elm index 579db47..a80533f 100644 --- a/client/src/elm/MassiveDecks/Game/Round/Plays.elm +++ b/client/src/elm/MassiveDecks/Game/Round/Plays.elm @@ -1,10 +1,17 @@ -module MassiveDecks.Game.Round.Plays exposing (Details, view) +module MassiveDecks.Game.Round.Plays exposing (Details, byLine, view) +import Dict exposing (Dict) +import FontAwesome.Icon as Icon exposing (Icon) import Html exposing (Html) import Html.Attributes as HtmlA import Html.Events as HtmlE import Html.Keyed as HtmlK import MassiveDecks.Card.Play as Play +import MassiveDecks.Model exposing (Shared) +import MassiveDecks.Strings as Strings +import MassiveDecks.Strings.Languages as Lang +import MassiveDecks.User as User exposing (User) +import MassiveDecks.Util.Html as Html import MassiveDecks.Util.Html.Attributes as HtmlA @@ -25,6 +32,20 @@ view class picked details = HtmlK.ul [ HtmlA.classList [ ( class, True ), ( "cards", True ), ( "plays", True ) ] ] (details |> List.map (viewPlay picked)) +{-| Create a byline. +-} +byLine : Shared -> Dict User.Id User -> User.Id -> Maybe Icon -> Html msg +byLine shared users id icon = + let + name = + users |> Dict.get id |> Maybe.map .name |> Maybe.withDefault (Strings.UnknownUser |> Lang.string shared) + in + Html.span [ HtmlA.class "byline", HtmlA.title name ] + [ icon |> Maybe.map Icon.view |> Maybe.withDefault Html.nothing + , Html.text name + ] + + {- Private -} diff --git a/client/src/elm/MassiveDecks/Pages/Lobby.elm b/client/src/elm/MassiveDecks/Pages/Lobby.elm index f0466a1..9d67ca6 100644 --- a/client/src/elm/MassiveDecks/Pages/Lobby.elm +++ b/client/src/elm/MassiveDecks/Pages/Lobby.elm @@ -503,7 +503,7 @@ viewLobby shared configure auth lobby = [ viewUsers shared auth.claims.uid lobby.users game , Html.div [ HtmlA.id "scroll-frame" ] [ lobby.game - |> Maybe.map (Game.view shared auth lobby.config lobby.users) + |> Maybe.map (Game.view shared auth lobby.name lobby.config lobby.users) |> Maybe.withDefault (Configure.view shared privileged configure auth.claims.gc lobby lobby.config) ] ] @@ -624,10 +624,8 @@ viewUser shared player game ( userId, user ) = , clickable , HtmlA.id id ] - [ Html.div [ HtmlA.class "user compressed-terms" ] - (Html.span [ HtmlA.title user.name ] [ Html.text user.name ] - :: secondary - ) + [ Html.span [ HtmlA.class "user", HtmlA.title user.name ] [ Html.text user.name ] + , Html.div [ HtmlA.class "compressed-terms" ] secondary , Html.span [ WlA.listItemSlot WlA.AfterItem ] score ] , menu diff --git a/client/src/elm/MassiveDecks/Strings.elm b/client/src/elm/MassiveDecks/Strings.elm index 7cde2c3..f2fcee3 100644 --- a/client/src/elm/MassiveDecks/Strings.elm +++ b/client/src/elm/MassiveDecks/Strings.elm @@ -185,6 +185,7 @@ type MdString | Revealing -- A description of the stage of the round where the czar is revealing the plays. | Judging -- A description of the stage of the round where the czar is picking a winner. | Complete -- A description of the stage of the round where it is finished. + | ViewGameHistoryAction -- A description of the action of viewing the history of the game. -- Instructions | WhatToDo -- A description of the action of asking for help on what to do in the game at this time. | PlayInstruction { numberOfCards : Int } -- Instruction to the player on how to play cards. diff --git a/client/src/elm/MassiveDecks/Strings/Languages/En.elm b/client/src/elm/MassiveDecks/Strings/Languages/En.elm index 8474a58..303b7dd 100644 --- a/client/src/elm/MassiveDecks/Strings/Languages/En.elm +++ b/client/src/elm/MassiveDecks/Strings/Languages/En.elm @@ -650,6 +650,9 @@ translate mdString = Complete -> [ Text "Finished" ] + ViewGameHistoryAction -> + [ Text "View previous rounds from this game." ] + -- Instructions WhatToDo -> [ Text "What should I be doing?" ] diff --git a/client/src/scss/card/variables.scss b/client/src/scss/card/variables.scss index c7a5aea..5da332f 100644 --- a/client/src/scss/card/variables.scss +++ b/client/src/scss/card/variables.scss @@ -6,7 +6,7 @@ $response-background: $call-color; $card-width: 18em; $card-margin: 0.5em; -$card-overlap: 2em; +$card-overlap: 4em; $card-ratio: 5/7; $card-compact-ratio: 1; @@ -15,6 +15,10 @@ $card-compact-ratio: 1; padding-top: #{calc(1 / #{$ratio} * 100%)}; } +@function height($width, $ratio) { + @return calc(#{$width} * 1 / #{$ratio}); +} + @function strip-unit($value) { @return $value / ($value * 0 + 1); } diff --git a/client/src/scss/game.scss b/client/src/scss/game.scss index a95ba03..04ebfa7 100644 --- a/client/src/scss/game.scss +++ b/client/src/scss/game.scss @@ -4,6 +4,7 @@ @import "./game/judging"; @import "./game/playing"; @import "./game/plays"; +@import "./game/history"; @import "./pages/lobby/variables"; // The main game area. @@ -47,9 +48,11 @@ right: 0; z-index: 1; --card-padding: 0.5em; + margin: 1rem; #context-help-button { font-size: 1.2rem; + margin: 0; } wl-popover-card { @@ -65,9 +68,11 @@ flex-direction: column; transition: left 0.3s; z-index: 1; + margin: 1rem; wl-button { font-size: 1.2rem; + margin: 0; } } @@ -113,7 +118,6 @@ flex-direction: column; justify-content: center; align-items: center; - min-width: $card-width; padding: 1em 0; } @@ -137,12 +141,31 @@ ul.cards { list-style: none; padding: 0; margin-top: 0.5em; + + li { + min-width: $card-width; + } } // Multiple cards that are grouped together. ol.card-set { margin: 0.5em; display: flex; + flex-wrap: wrap; + justify-content: center; list-style: none; padding: 0; + + li { + height: $card-overlap; + overflow: visible; + + &:hover { + z-index: 1; + } + + &:last-child { + height: height($card-width, var(--card-aspect-ratio)); + } + } } diff --git a/client/src/scss/game/history.scss b/client/src/scss/game/history.scss new file mode 100644 index 0000000..d86e18f --- /dev/null +++ b/client/src/scss/game/history.scss @@ -0,0 +1,57 @@ +@import "../colors"; +@import "../card/size"; + +#history { + h2 { + text-align: center; + font-size: 2em; + margin: 1em 3em; + word-break: break-word; + } + + .historic-rounds { + list-style: none; + padding: 0; + counter-reset: historic-round-number; + + .historic-round:nth-child(even) { + background-color: transparentize($primary-light, 0.6); + } + .historic-round:nth-child(odd) { + background-color: transparentize($secondary-light, 0.6); + } + } + + .historic-round { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + counter-increment: historic-round-number -1; + padding: 1em 0 0.5em 0; + + &::before { + display: flex; + flex-direction: column; + justify-content: center; + content: counter(historic-round-number); + align-self: stretch; + font-size: 3em; + padding: 0 0.2em; + font-weight: bold; + } + + .historic-call { + flex-basis: $card-width; + min-width: $card-width; + + & > div { + margin: 0.5em; + } + } + + .plays { + justify-content: center; + } + } +} diff --git a/client/src/scss/game/plays.scss b/client/src/scss/game/plays.scss index 1f40b72..5c8f8c6 100644 --- a/client/src/scss/game/plays.scss +++ b/client/src/scss/game/plays.scss @@ -1,14 +1,20 @@ // A play with the user's name. -.play-with-byline { +.with-byline { display: flex; flex-direction: column; align-items: center; + margin: 0.5em; } // The user's name. .byline { font-size: 2em; text-align: center; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; + max-width: 100%; + overflow: hidden; svg { margin-right: 0.5em; diff --git a/client/src/scss/pages/lobby.scss b/client/src/scss/pages/lobby.scss index 2c98b22..546c279 100644 --- a/client/src/scss/pages/lobby.scss +++ b/client/src/scss/pages/lobby.scss @@ -98,18 +98,16 @@ transition: width 0.3s; --list-item-padding: 0.5em; + --list-item-left-margin: 0.5em; .collapsible { padding: 1em; } .user { - position: relative; - display: flex; - - .term { - margin-left: 0.5em; - } + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } ol { diff --git a/server/src/ts/action/game-action/czar/judge.ts b/server/src/ts/action/game-action/czar/judge.ts index f29ee33..c37c2e5 100644 --- a/server/src/ts/action/game-action/czar/judge.ts +++ b/server/src/ts/action/game-action/czar/judge.ts @@ -43,6 +43,7 @@ export const handle: gameAction.Handler = ( winner: play.playedBy }; lobby.game.round = completedRound; + lobby.game.history.splice(0, 0, completedRound); return { lobby, events: [event.targetAll(roundFinished.of(completedRound))],