From 6329290d778f19add9cc34fbcbdcd8de58e09b2c Mon Sep 17 00:00:00 2001 From: Gareth Latty Date: Sat, 12 Dec 2015 17:59:08 +0000 Subject: [PATCH] Groundwork for #19 - Eventing framework. --- client/src/MassiveDecks/Actions/Action.elm | 18 +++- client/src/MassiveDecks/Actions/Event.elm | 107 +++++++++++++++++++++ client/src/MassiveDecks/Config.elm | 37 +++++-- client/src/MassiveDecks/Models/Game.elm | 8 ++ client/src/MassiveDecks/Models/State.elm | 8 +- client/src/MassiveDecks/Playing.elm | 90 ++++++++--------- client/src/MassiveDecks/UI/Config.elm | 2 +- client/src/MassiveDecks/UI/Lobby.elm | 19 +++- client/src/MassiveDecks/UI/Playing.elm | 23 ++--- client/src/MassiveDecks/Util.elm | 5 + server/app/assets/stylesheets/lobby.less | 33 +++++++ 11 files changed, 268 insertions(+), 82 deletions(-) create mode 100644 client/src/MassiveDecks/Actions/Event.elm diff --git a/client/src/MassiveDecks/Actions/Action.elm b/client/src/MassiveDecks/Actions/Action.elm index d01a815..00b48d7 100644 --- a/client/src/MassiveDecks/Actions/Action.elm +++ b/client/src/MassiveDecks/Actions/Action.elm @@ -1,8 +1,12 @@ module MassiveDecks.Actions.Action where +import Task +import Effects + import MassiveDecks.Models.State exposing (InitialState) import MassiveDecks.Models.Game exposing (Lobby, LobbyAndHand) -import MassiveDecks.Models.Player exposing (Secret) +import MassiveDecks.Models.Player as Player +import MassiveDecks.Actions.Event exposing (Event, events) type APICall a @@ -16,7 +20,7 @@ type Action | UpdateInputValue String String | NewLobby (APICall Lobby) | JoinExistingLobby - | JoinLobby String Secret (APICall LobbyAndHand) + | JoinLobby String Player.Secret (APICall LobbyAndHand) | AddDeck (APICall LobbyAndHand) | StartGame (APICall LobbyAndHand) | Pick Int @@ -29,3 +33,13 @@ type Action | NextRound | SetInitialState InitialState | AnimatePlayedCards (List Int) + | GameEvent Event + + +eventEffects : Lobby -> Lobby -> Effects.Effects Action +eventEffects oldLobby newLobby + = events oldLobby newLobby + |> List.map GameEvent + |> List.map Task.succeed + |> List.map Effects.task + |> Effects.batch diff --git a/client/src/MassiveDecks/Actions/Event.elm b/client/src/MassiveDecks/Actions/Event.elm new file mode 100644 index 0000000..d6a5da4 --- /dev/null +++ b/client/src/MassiveDecks/Actions/Event.elm @@ -0,0 +1,107 @@ +module MassiveDecks.Actions.Event where + +import MassiveDecks.Models.Game exposing (..) +import MassiveDecks.Models.Player exposing (..) +import MassiveDecks.Models.Card exposing (..) +import MassiveDecks.Util as Util + + +type Event + = PlayerJoin Id + | PlayerStatus Id Status + | PlayerScore Id Int + | RoundStart Call Id + | RoundPlayed Int + | RoundJudging (List PlayedCards) + | RoundEnd Call Id (List PlayedCards) PlayedByAndWinner + + +events : Lobby -> Lobby -> List Event +events oldLobby newLobby = List.concat + [ diffPlayers oldLobby.players newLobby.players + , diffRound oldLobby.round newLobby.round + ] + + +diffPlayers : List Player -> List Player -> List Event +diffPlayers oldPlayers newPlayers = + List.concatMap (diffPlayer oldPlayers) newPlayers + +diffPlayer : List Player -> Player -> List Event +diffPlayer oldPlayers newPlayer = + let + id = newPlayer.id + oldPlayer = List.filter (\player -> player.id == id) oldPlayers |> List.head + in + case oldPlayer of + Just oldPlayer -> + List.concat + [ if oldPlayer.status /= newPlayer.status then [ playerStatus newPlayer ] else [] + , if oldPlayer.score /= newPlayer.score then [ playerScore newPlayer ] else [] + ] + + Nothing -> + Util.apply [ playerJoin, playerStatus, playerScore ] newPlayer + + +diffRound : Maybe Round -> Maybe Round -> List Event +diffRound oldRound newRound = + let + differentRound = Maybe.map .call oldRound /= Maybe.map .call newRound + in + if differentRound then + List.filterMap identity + [ Maybe.map roundEnd oldRound + , Maybe.map roundStart newRound + ] + else + Maybe.map2 changedRound oldRound newRound |> Maybe.withDefault [] + +changedRound : Round -> Round -> List Event +changedRound oldRound newRound = + case oldRound.responses of + Hidden oldCount -> + case newRound.responses of + Hidden newCount -> if (oldCount < newCount) then [ roundPlayed (newCount - oldCount) ] else [] + Revealed _ -> [ roundJudging newRound ] + Revealed _ -> + [] + + +{- Event Constructors -} + +playerJoin : Player -> Event +playerJoin player = PlayerJoin player.id + +playerStatus : Player -> Event +playerStatus player = PlayerStatus player.id player.status + +playerScore : Player -> Event +playerScore player = PlayerScore player.id player.score + +roundStart : Round -> Event +roundStart round = RoundStart round.call round.czar + +roundPlayed : Int -> Event +roundPlayed amount = RoundPlayed amount + +roundJudging : Round -> Event +roundJudging round = + let + responses = case round.responses of + Hidden _ -> Nothing + Revealed responses -> Just responses + played = Maybe.map .cards responses |> Maybe.withDefault [] + in + RoundJudging played + +roundEnd : Round -> Event +roundEnd round = + let + responses = case round.responses of + Hidden _ -> Nothing + Revealed responses -> Just responses + played = Maybe.map .cards responses |> Maybe.withDefault [] + playedByAndWinner = responses `Maybe.andThen` .playedByAndWinner |> Maybe.withDefault (PlayedByAndWinner [] 0) + in + RoundEnd round.call round.czar played playedByAndWinner diff --git a/client/src/MassiveDecks/Config.elm b/client/src/MassiveDecks/Config.elm index 3c40cbe..634234b 100644 --- a/client/src/MassiveDecks/Config.elm +++ b/client/src/MassiveDecks/Config.elm @@ -7,7 +7,7 @@ import Html exposing (Html) import MassiveDecks.Models.Player exposing (Secret) import MassiveDecks.Models.Game exposing (Lobby) import MassiveDecks.Models.State exposing (Model, State(..), ConfigData, PlayingData, Error, Global) -import MassiveDecks.Actions.Action exposing (Action(..), APICall(..)) +import MassiveDecks.Actions.Action exposing (Action(..), APICall(..), eventEffects) import MassiveDecks.UI.Config as UI import MassiveDecks.API as API import MassiveDecks.Playing as Playing @@ -27,13 +27,17 @@ update action global data = case action of |> API.toEffect) AddDeck (Result lobbyAndHand) -> - (model global { data | lobby = lobbyAndHand.lobby }, Effects.none) + let + (data, effects) = updateLobby data lobbyAndHand.lobby + in + (model global data, effects) StartGame Request -> (model global data, (API.newGame data.lobby.id data.secret) |> Task.map (StartGame << Result) |> API.toEffect) StartGame (Result lobbyAndHand) -> - (Playing.model global (PlayingData lobbyAndHand.lobby lobbyAndHand.hand data.secret [] Nothing Nothing []), Effects.none) + (Playing.model global (PlayingData lobbyAndHand.lobby lobbyAndHand.hand data.secret [] Nothing Nothing []), + eventEffects data.lobby lobbyAndHand.lobby) Notification lobby -> case lobby.round of @@ -41,13 +45,26 @@ update action global data = case action of (API.getLobbyAndHand lobby.id data.secret) |> Task.map (\lobbyAndHand -> JoinLobby lobby.id data.secret (Result lobbyAndHand)) |> API.toEffect) - Nothing -> (model global { data | lobby = lobby }, Effects.none) + Nothing -> + let + (data, effects) = updateLobby data lobby + in + (model global data, effects) JoinLobby lobbyId secret (Result lobbyAndHand) -> case lobbyAndHand.lobby.round of - Just _ -> (Playing.model global (PlayingData lobbyAndHand.lobby lobbyAndHand.hand secret [] Nothing Nothing []), Effects.none) - Nothing -> (model global { data | lobby = lobbyAndHand.lobby }, Effects.none) + Just _ -> + (Playing.model global (PlayingData lobbyAndHand.lobby lobbyAndHand.hand secret [] Nothing Nothing []), + eventEffects data.lobby lobbyAndHand.lobby) + Nothing -> + let + (data, effects) = updateLobby data lobbyAndHand.lobby + in + (model global data, effects) + GameEvent _ -> + (model global data, Effects.none) + other -> (model global data, DisplayError ("Got an action (" ++ (toString other) ++ ") that can't be handled in the current state (Config).") @@ -55,6 +72,14 @@ update action global data = case action of |> Effects.task) +updateLobby : ConfigData -> Lobby -> (ConfigData, Effects.Effects Action) +updateLobby data lobby = + let + events = eventEffects data.lobby lobby + in + ({ data | lobby = lobby }, events) + + model : Global -> ConfigData -> Model model global data = { state = SConfig data diff --git a/client/src/MassiveDecks/Models/Game.elm b/client/src/MassiveDecks/Models/Game.elm index 5198b1a..7be76d3 100644 --- a/client/src/MassiveDecks/Models/Game.elm +++ b/client/src/MassiveDecks/Models/Game.elm @@ -24,6 +24,14 @@ type alias Round = } +type alias FinishedRound = + { call : Call + , czar : Id + , responses : (List PlayedCards) + , playedByAndWinner : PlayedByAndWinner + } + + type alias Lobby = { id : String , config : Config diff --git a/client/src/MassiveDecks/Models/State.elm b/client/src/MassiveDecks/Models/State.elm index 42f952c..1982ee8 100644 --- a/client/src/MassiveDecks/Models/State.elm +++ b/client/src/MassiveDecks/Models/State.elm @@ -3,9 +3,9 @@ module MassiveDecks.Models.State where import Random import Html exposing (Attribute) -import MassiveDecks.Models.Card exposing (Hand) -import MassiveDecks.Models.Game exposing (Config, Lobby, Round) -import MassiveDecks.Models.Player exposing (Player, Secret) +import MassiveDecks.Models.Card exposing (Hand, Call, PlayedCards) +import MassiveDecks.Models.Game exposing (Config, Lobby, Round, FinishedRound) +import MassiveDecks.Models.Player exposing (Player, Secret, Id, PlayedByAndWinner) type alias Model = @@ -47,7 +47,7 @@ type alias PlayingData = , secret : Secret , picked : List Int , considering : Maybe Int - , lastFinishedRound : Maybe Round + , lastFinishedRound : Maybe FinishedRound , shownPlayed : List Attribute } diff --git a/client/src/MassiveDecks/Playing.elm b/client/src/MassiveDecks/Playing.elm index 08813b7..f8c4131 100644 --- a/client/src/MassiveDecks/Playing.elm +++ b/client/src/MassiveDecks/Playing.elm @@ -9,9 +9,10 @@ import Random exposing (Generator, Seed, list, bool, int) import MassiveDecks.Models.Player exposing (Secret) import MassiveDecks.Models.Card as Card -import MassiveDecks.Models.Game exposing (Lobby) +import MassiveDecks.Models.Game exposing (Lobby, FinishedRound) import MassiveDecks.Models.State exposing (State(..), Model, ConfigData, PlayingData, Error, Global) -import MassiveDecks.Actions.Action exposing (Action(..), APICall(..)) +import MassiveDecks.Actions.Action exposing (Action(..), APICall(..), eventEffects) +import MassiveDecks.Actions.Event exposing (Event(..)) import MassiveDecks.UI.Playing as UI import MassiveDecks.API as API import MassiveDecks.Util as Util @@ -39,30 +40,22 @@ update action global data = case action of (model global data, (API.play data.lobby.id data.secret data.picked) |> Task.map (Play << Result) |> API.toEffect) Play (Result lobbyAndHand) -> - let - (data, effect, seed) = (updateLobby data lobbyAndHand.lobby global.seed) - in - (model { global | seed = seed } - { data | hand = lobbyAndHand.hand + (model global + { data | lobby = lobbyAndHand.lobby + , hand = lobbyAndHand.hand , picked = [] - }, effect) + }, eventEffects data.lobby lobbyAndHand.lobby) Notification lobby -> case lobby.round of - Just _ -> - let - (data, effect, seed) = (updateLobby data lobby global.seed) - in - (model { global | seed = seed } data, effect) + Just _ -> (model global { data | lobby = lobby }, eventEffects data.lobby lobby) Nothing -> (configModel global (ConfigData lobby data.secret ""), Effects.none) JoinLobby lobbyId secret (Result lobbyAndHand) -> case lobbyAndHand.lobby.round of - Just _ -> - let - (data, effect, seed) = (updateLobby data lobbyAndHand.lobby global.seed) - in - (model { global | seed = seed } data, effect) + Just _ -> (model global { data | lobby = lobbyAndHand.lobby + , hand = lobbyAndHand.hand + }, eventEffects data.lobby lobbyAndHand.lobby) Nothing -> (configModel global (ConfigData lobbyAndHand.lobby data.secret ""), Effects.none) Consider potentialWinner -> @@ -72,13 +65,11 @@ update action global data = case action of (model global data, (API.choose data.lobby.id data.secret winner) |> Task.map (Choose winner << Result) |> API.toEffect) Choose winner (Result lobbyAndHand) -> - let - (data, effect, seed) = (updateLobby data lobbyAndHand.lobby global.seed) - in - (model { global | seed = seed } - { data | hand = lobbyAndHand.hand + (model global + { data | lobby = lobbyAndHand.lobby + , hand = lobbyAndHand.hand , picked = [] - }, effect) + }, eventEffects data.lobby lobbyAndHand.lobby) NextRound -> (model global { data | lastFinishedRound = Nothing }, Effects.none) @@ -89,6 +80,20 @@ update action global data = case action of in (model { global | seed = seed } { data | shownPlayed = shownPlayed }, Effects.none) + GameEvent event -> + case event of + RoundPlayed amount -> + let + (shownPlayed, effects, seed) = addShownPlayed amount data.shownPlayed global.seed + in + (model { global | seed = seed } { data | shownPlayed = shownPlayed }, effects) + + RoundEnd call czar responses playedByAndWinner -> + (model global { data | lastFinishedRound = Just (FinishedRound call czar responses playedByAndWinner) }, Effects.none) + + _ -> + (model global data, Effects.none) + other -> (model global data, DisplayError ("Got an action (" ++ (toString other) ++ ") that can't be handled in the current state (Playing).") @@ -96,34 +101,19 @@ update action global data = case action of |> Effects.task) -updateLobby : PlayingData -> Lobby -> Seed -> (PlayingData, Effects.Effects Action, Seed) -updateLobby data lobby seed = +addShownPlayed : Int -> List Attribute -> Seed -> (List Attribute, Effects.Effects Action, Seed) +addShownPlayed amount existing seed = let - lastFinishedRound = if (Maybe.map .call data.lobby.round) == (Maybe.map .call lobby.round) then - data.lastFinishedRound - else - data.lobby.round - oldShownPlayed = data.shownPlayed - oldShownPlayedLength = (List.length oldShownPlayed) - (shownPlayed, effects, resultSeed) = case Maybe.map .responses data.lobby.round of - Just (Card.Hidden amount) -> - let - (newShownPlayed, newSeed) = - Random.generate (list (amount - oldShownPlayedLength) initialRandomPositioning) seed - in - (List.concat [ oldShownPlayed, newShownPlayed ] - ,(Task.sleep (Time.millisecond * 250) - `Task.andThen` - \_ -> (Task.succeed (AnimatePlayedCards (Util.range oldShownPlayedLength (List.length newShownPlayed))))) - |> Effects.task - ,newSeed) - _ -> ([], Effects.none, seed) + existingLength = List.length existing + (new, newSeed) = Random.generate (list (amount - existingLength) initialRandomPositioning) seed in - ({ data | lobby = lobby - , lastFinishedRound = lastFinishedRound - , shownPlayed = shownPlayed - }, effects, resultSeed) - + ( List.concat [ existing, new ] + , (Task.sleep (Time.millisecond * 250) + `Task.andThen` + \_ -> (Task.succeed (AnimatePlayedCards (Util.range existingLength (List.length new))))) + |> Effects.task + , newSeed + ) updatePositioning : List Int -> List Attribute -> Seed -> (List Attribute, Seed) updatePositioning toAnimate existing seed = diff --git a/client/src/MassiveDecks/UI/Config.elm b/client/src/MassiveDecks/UI/Config.elm index 4b615e7..6058b8e 100644 --- a/client/src/MassiveDecks/UI/Config.elm +++ b/client/src/MassiveDecks/UI/Config.elm @@ -109,7 +109,7 @@ emptyDeckListInfo display = startGameWarning : Bool -> Html startGameWarning canStart = if canStart then text "" else - span [] [ icon "info-circle", text "You will need at least two players to start the game." ] + span [] [ icon "info-circle", text " You will need at least two players to start the game." ] startGameButton : Signal.Address Action -> Bool -> Bool -> Html diff --git a/client/src/MassiveDecks/UI/Lobby.elm b/client/src/MassiveDecks/UI/Lobby.elm index 6542922..3175ff2 100644 --- a/client/src/MassiveDecks/UI/Lobby.elm +++ b/client/src/MassiveDecks/UI/Lobby.elm @@ -49,12 +49,27 @@ score player = tr [ class (statusName player.status), title (statusDescription p appHeader : List Html -> Html appHeader contents = (header [] [ div [ class "mui-appbar mui--appbar-line-height" ] [ div [ class "mui--appbar-line-height" ] - [ button [ class "scores-toggle mui-btn mui-btn--small mui-btn--primary mui--visible-xs-inline-block js-show-scores" ] [ icon "users" ] - , button [ class "scores-toggle mui-btn mui-btn--small mui-btn--primary mui--hidden-xs js-hide-scores" ] [ icon "users" ] + [ span [] (List.append [ scoresButton True, scoresButton False ] (scoresBadge 0)) , span [ id "title", class "mui--text-title mui--visible-xs-inline-block" ] contents , gameMenu ] ] ]) +scoresButton : Bool -> Html +scoresButton shown = + let + showHideClasses = if shown then " mui--hidden-xs js-hide-scores" else " mui--visible-xs-inline-block js-show-scores" + in + button [ class ("scores-toggle mui-btn mui-btn--small mui-btn--primary badged" ++ showHideClasses)] [ icon "users" ] + + +scoresBadge : Int -> List Html +scoresBadge events = + if events > 0 then + [ div [ class "badge" ] [ icon "exclamation" ] ] + else + [] + + statusDescription : Status -> String statusDescription status = case status of NotPlayed -> "Choosing" diff --git a/client/src/MassiveDecks/UI/Playing.elm b/client/src/MassiveDecks/UI/Playing.elm index d5ba6a1..49305f6 100644 --- a/client/src/MassiveDecks/UI/Playing.elm +++ b/client/src/MassiveDecks/UI/Playing.elm @@ -7,7 +7,7 @@ import Html.Events exposing (..) import MassiveDecks.Models.State exposing (PlayingData, Error, Global) import MassiveDecks.Models.Card as Card import MassiveDecks.Models.Player exposing (Player, Id, Status(..)) -import MassiveDecks.Models.Game exposing (Round) +import MassiveDecks.Models.Game exposing (Round, FinishedRound) import MassiveDecks.Models.Card exposing (Response, Responses(..), PlayedCards) import MassiveDecks.Actions.Action exposing (Action(..), APICall(..)) import MassiveDecks.UI.Lobby as LobbyUI @@ -73,26 +73,15 @@ consideringView address considering consideringCards isCzar = let extra = if isCzar then [ chooseButton address considering ] else [] in - ol [ class "considering" ] + ol [ class "considering" ] (List.append (List.map (\card -> li [] [ (playedResponse card) ]) consideringCards) extra) -winnerContentsAndHeader : Signal.Address Action -> Round -> List Player -> (List Html, List Html) +winnerContentsAndHeader : Signal.Address Action -> FinishedRound -> List Player -> (List Html, List Html) winnerContentsAndHeader address round players = let - winning = case round.responses of - Revealed revealed -> - (Card.winningCards revealed.cards) - |> Maybe.andThen revealed.playedByAndWinner - |> Maybe.withDefault [] - Hidden _ -> [] - winner = case round.responses of - Revealed revealed -> - revealed.playedByAndWinner - |> Maybe.map .winner - |> Maybe.map (Util.get players) - |> Maybe.map .name - |> Maybe.withDefault "" - Hidden _ -> "" + cards = round.responses + winning = Card.winningCards cards round.playedByAndWinner |> Maybe.withDefault [] + winner = (Util.get players round.playedByAndWinner.winner).name in ([ div [ class "winner mui-panel" ] [ h1 [] [ icon "trophy" ] diff --git a/client/src/MassiveDecks/Util.elm b/client/src/MassiveDecks/Util.elm index b30c960..388ee32 100644 --- a/client/src/MassiveDecks/Util.elm +++ b/client/src/MassiveDecks/Util.elm @@ -17,6 +17,7 @@ interleave list1 list2 = y :: ys -> y :: x :: interleave xs ys + remove : List a -> Int -> List a remove list index = (List.take index list) ++ (List.drop (index + 1) list) @@ -49,3 +50,7 @@ inOrder : List (Generator a) -> Generator (List a) inOrder generators = case generators of [] -> Random.map (\_ -> []) Random.bool head :: tail -> head `Random.andThen` (\value -> Random.map ((::) value) (inOrder tail)) + + +apply : List (a -> b) -> a -> List b +apply fs value = List.map (\f -> f value) fs diff --git a/server/app/assets/stylesheets/lobby.less b/server/app/assets/stylesheets/lobby.less index 89133d2..376d4ee 100644 --- a/server/app/assets/stylesheets/lobby.less +++ b/server/app/assets/stylesheets/lobby.less @@ -233,6 +233,39 @@ ol.played { padding: 0px; } +.badge { + display: inline-flex; + width: 20px; + height: 20px; + border-radius: 50% 50%; + font-size: 14px; + line-height: 14px; + + position: relative; + z-index: 10; + left: -25px; + top: -10px; + + align-items: center; + justify-content: center; + + overflow: visible; + white-space: nowrap; + + background: #E91E63; + color: #ffffff; + + @media (min-width: @degrade-width) { + display: none !important; + } +} + +body.hide-scores { + .badge { + display: inline-flex !important; + } +} + body { #scores { position: fixed;