diff --git a/assets/elm/Main.elm b/assets/elm/Main.elm index 1993634..9ccac29 100644 --- a/assets/elm/Main.elm +++ b/assets/elm/Main.elm @@ -265,8 +265,11 @@ playersListItem player = player.username else Maybe.withDefault "" player.displayName + + playerLink = + "players/" ++ (toString player.id) in li [ class "player-item list-group-item" ] - [ strong [] [ text displayName ] + [ strong [] [ a [ href playerLink ] [ text displayName ] ] , span [ class "badge" ] [ text (toString player.score) ] ] diff --git a/assets/elm/Platformer.elm b/assets/elm/Platformer.elm index 9a15e3e..202cfbc 100644 --- a/assets/elm/Platformer.elm +++ b/assets/elm/Platformer.elm @@ -39,6 +39,11 @@ main = -- MODEL +type Direction + = Left + | Right + + type GameState = StartScreen | Playing @@ -46,6 +51,13 @@ type GameState | GameOver +type alias Gameplay = + { gameId : Int + , playerId : Int + , playerScore : Int + } + + type alias Player = { displayName : Maybe String , id : Int @@ -55,36 +67,38 @@ type alias Player = type alias Model = - { errors : String - , gameId : Int - , gameState : GameState + { characterDirection : Direction , characterPositionX : Int , characterPositionY : Int + , errors : String + , gameId : Int + , gameplays : List Gameplay + , gameState : GameState , itemPositionX : Int , itemPositionY : Int , itemsCollected : Int , phxSocket : Phoenix.Socket.Socket Msg , playersList : List Player , playerScore : Int - , playerScores : List Score , timeRemaining : Int } initialModel : Flags -> Model initialModel flags = - { errors = "" - , gameId = 1 - , gameState = StartScreen + { characterDirection = Right , characterPositionX = 50 , characterPositionY = 300 + , errors = "" + , gameId = 1 + , gameplays = [] + , gameState = StartScreen , itemPositionX = 150 , itemPositionY = 300 , itemsCollected = 0 , phxSocket = initialSocketJoin flags , playersList = [] , playerScore = 0 - , playerScores = [] , timeRemaining = 10 } @@ -98,9 +112,9 @@ initialSocket flags = prodSocketServer = "wss://elixir-elm-tutorial.herokuapp.com/socket/websocket?token=" ++ flags.token in - Phoenix.Socket.init prodSocketServer + Phoenix.Socket.init devSocketServer |> Phoenix.Socket.withDebug - |> Phoenix.Socket.on "shout" "score:platformer" SendScore + |> Phoenix.Socket.on "save_score" "score:platformer" SaveScore |> Phoenix.Socket.on "save_score" "score:platformer" ReceiveScoreChanges |> Phoenix.Socket.join initialChannel @@ -124,7 +138,13 @@ initialSocketCommand flags = init : Flags -> ( Model, Cmd Msg ) init flags = - ( initialModel flags, Cmd.map PhoenixMsg (initialSocketCommand flags) ) + ( initialModel flags + , Cmd.batch + [ fetchGameplaysList + , fetchPlayersList + , Cmd.map PhoenixMsg (initialSocketCommand flags) + ] + ) @@ -153,16 +173,22 @@ decodePlayer = (Decode.field "username" Decode.string) -type alias Score = - { gameId : Int - , playerId : Int - , playerScore : Int - } +fetchGameplaysList : Cmd Msg +fetchGameplaysList = + Http.get "/api/gameplays" decodeGameplaysList + |> Http.send FetchGameplaysList + + +decodeGameplaysList : Decode.Decoder (List Gameplay) +decodeGameplaysList = + decodeGameplay + |> Decode.list + |> Decode.at [ "data" ] -scoreDecoder : Decode.Decoder Score -scoreDecoder = - Decode.map3 Score +decodeGameplay : Decode.Decoder Gameplay +decodeGameplay = + Decode.map3 Gameplay (Decode.field "game_id" Decode.int) (Decode.field "player_id" Decode.int) (Decode.field "player_score" Decode.int) @@ -175,6 +201,7 @@ scoreDecoder = type Msg = NoOp | CountdownTimer Time + | FetchGameplaysList (Result Http.Error (List Gameplay)) | FetchPlayersList (Result Http.Error (List Player)) | KeyDown KeyCode | PhoenixMsg (Phoenix.Socket.Msg Msg) @@ -182,9 +209,6 @@ type Msg | SaveScore Encode.Value | SaveScoreError Encode.Value | SaveScoreRequest - | SendScore Encode.Value - | SendScoreError Encode.Value - | SendScoreRequest | SetNewItemPositionX Int | TimeUpdate Time @@ -201,6 +225,14 @@ update msg model = else ( model, Cmd.none ) + FetchGameplaysList result -> + case result of + Ok gameplays -> + ( { model | gameplays = gameplays }, Cmd.none ) + + Err message -> + ( { model | errors = toString message }, Cmd.none ) + FetchPlayersList result -> case result of Ok players -> @@ -214,10 +246,11 @@ update msg model = 32 -> if model.gameState /= Playing then ( { model - | gameState = Playing + | characterDirection = Right , characterPositionX = 50 - , playerScore = 0 , itemsCollected = 0 + , gameState = Playing + , playerScore = 0 , timeRemaining = 10 } , Cmd.none @@ -227,13 +260,23 @@ update msg model = 37 -> if model.gameState == Playing then - ( { model | characterPositionX = model.characterPositionX - 15 }, Cmd.none ) + ( { model + | characterDirection = Left + , characterPositionX = model.characterPositionX - 15 + } + , Cmd.none + ) else ( model, Cmd.none ) 39 -> if model.gameState == Playing then - ( { model | characterPositionX = model.characterPositionX + 15 }, Cmd.none ) + ( { model + | characterDirection = Right + , characterPositionX = model.characterPositionX + 15 + } + , Cmd.none + ) else ( model, Cmd.none ) @@ -250,9 +293,9 @@ update msg model = ) ReceiveScoreChanges raw -> - case Decode.decodeValue scoreDecoder raw of + case Decode.decodeValue decodeGameplay raw of Ok scoreChange -> - ( { model | playerScores = scoreChange :: model.playerScores }, Cmd.none ) + ( { model | gameplays = scoreChange :: model.gameplays }, Cmd.none ) Err message -> ( { model | errors = message }, Cmd.none ) @@ -261,7 +304,7 @@ update msg model = ( model, Cmd.none ) SaveScoreError message -> - Debug.log "Error saveing score over socket." + Debug.log "Error saving score over socket." ( model, Cmd.none ) SaveScoreRequest -> @@ -282,31 +325,6 @@ update msg model = , Cmd.map PhoenixMsg phxCmd ) - SendScore value -> - ( model, Cmd.none ) - - SendScoreError message -> - Debug.log "Error sending score over socket." - ( model, Cmd.none ) - - SendScoreRequest -> - let - payload = - Encode.object [ ( "player_score", Encode.int model.playerScore ) ] - - phxPush = - Phoenix.Push.init "shout" "score:platformer" - |> Phoenix.Push.withPayload payload - |> Phoenix.Push.onOk SendScore - |> Phoenix.Push.onError SendScoreError - - ( phxSocket, phxCmd ) = - Phoenix.Socket.push phxPush model.phxSocket - in - ( { model | phxSocket = phxSocket } - , Cmd.map PhoenixMsg phxCmd - ) - SetNewItemPositionX newPositionX -> ( { model | itemPositionX = newPositionX }, Cmd.none ) @@ -363,20 +381,8 @@ view : Model -> Html Msg view model = div [] [ viewGame model - , viewSendScoreButton , viewSaveScoreButton - , viewPlayerScoresIndex model - ] - - -viewSendScoreButton : Html Msg -viewSendScoreButton = - div [] - [ button - [ onClick SendScoreRequest - , Html.Attributes.class "btn btn-primary" - ] - [ text "Send Score" ] + , viewGameplaysIndex model ] @@ -391,31 +397,50 @@ viewSaveScoreButton = ] -viewPlayerScoresIndex : Model -> Html Msg -viewPlayerScoresIndex model = - if List.isEmpty model.playerScores then +viewGameplaysIndex : Model -> Html Msg +viewGameplaysIndex model = + if List.isEmpty model.gameplays then div [] [] else div [ Html.Attributes.class "players-index" ] [ h1 [ Html.Attributes.class "players-section" ] [ text "Player Scores" ] - , viewPlayerScoresList model.playerScores + , viewGameplaysList model ] -viewPlayerScoresList : List Score -> Html Msg -viewPlayerScoresList scores = +viewGameplaysList : Model -> Html Msg +viewGameplaysList model = div [ Html.Attributes.class "players-list panel panel-info" ] - [ div [ Html.Attributes.class "panel-heading" ] [ text "Leaderboard" ] - , ul [ Html.Attributes.class "list-group" ] (List.map viewPlayerScoreItem scores) + [ div [ Html.Attributes.class "panel-heading" ] [ text "Scores" ] + , ul [ Html.Attributes.class "list-group" ] (List.map (viewPlayerScoreItem model) model.gameplays) ] -viewPlayerScoreItem : Score -> Html Msg -viewPlayerScoreItem score = - li [ Html.Attributes.class "player-item list-group-item" ] - [ strong [] [ text (toString score.playerId) ] - , span [ Html.Attributes.class "badge" ] [ text (toString score.playerScore) ] - ] +anonymousPlayer : Player +anonymousPlayer = + { displayName = Just "Anonymous User" + , id = 0 + , score = 0 + , username = "anonymous" + } + + +viewPlayerScoreItem : Model -> Gameplay -> Html Msg +viewPlayerScoreItem model gameplay = + let + currentPlayer = + model.playersList + |> List.filter (\player -> player.id == gameplay.playerId) + |> List.head + |> Maybe.withDefault anonymousPlayer + + displayName = + Maybe.withDefault currentPlayer.username currentPlayer.displayName + in + li [ Html.Attributes.class "player-item list-group-item" ] + [ strong [] [ text displayName ] + , span [ Html.Attributes.class "badge" ] [ text (toString gameplay.playerScore) ] + ] viewGame : Model -> Svg Msg @@ -527,14 +552,23 @@ viewGameGround = viewCharacter : Model -> Svg Msg viewCharacter model = - image - [ xlinkHref "/images/character.gif" - , x (toString model.characterPositionX) - , y (toString model.characterPositionY) - , width "50" - , height "50" - ] - [] + let + characterImage = + case model.characterDirection of + Left -> + "/images/character-left.gif" + + Right -> + "/images/character-right.gif" + in + image + [ xlinkHref characterImage + , x (toString model.characterPositionX) + , y (toString model.characterPositionY) + , width "50" + , height "50" + ] + [] viewItem : Model -> Svg Msg diff --git a/assets/js/app.js b/assets/js/app.js index d500c11..5ee11e6 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -21,7 +21,7 @@ import "phoenix_html" // import socket from "./socket" // Elm -const Elm = require("./elm.js"); +import Elm from "./elm"; const elmContainer = document.querySelector("#elm-container"); const platformer = document.querySelector("#platformer"); diff --git a/assets/js/elm.js b/assets/js/elm.js index a865de9..bc02df0 100644 --- a/assets/js/elm.js +++ b/assets/js/elm.js @@ -15476,6 +15476,10 @@ var _fbonetti$elm_phoenix_socket$Phoenix_Socket$listen = F2( }); var _user$project$Main$playersListItem = function (player) { + var playerLink = A2( + _elm_lang$core$Basics_ops['++'], + 'players/', + _elm_lang$core$Basics$toString(player.id)); var displayName = _elm_lang$core$Native_Utils.eq(player.displayName, _elm_lang$core$Maybe$Nothing) ? player.username : A2(_elm_lang$core$Maybe$withDefault, '', player.displayName); return A2( _elm_lang$html$Html$li, @@ -15491,7 +15495,18 @@ var _user$project$Main$playersListItem = function (player) { {ctor: '[]'}, { ctor: '::', - _0: _elm_lang$html$Html$text(displayName), + _0: A2( + _elm_lang$html$Html$a, + { + ctor: '::', + _0: _elm_lang$html$Html_Attributes$href(playerLink), + _1: {ctor: '[]'} + }, + { + ctor: '::', + _0: _elm_lang$html$Html$text(displayName), + _1: {ctor: '[]'} + }), _1: {ctor: '[]'} }), _1: { @@ -16139,11 +16154,19 @@ var _user$project$Platformer$viewItem = function (model) { {ctor: '[]'}); }; var _user$project$Platformer$viewCharacter = function (model) { + var characterImage = function () { + var _p0 = model.characterDirection; + if (_p0.ctor === 'Left') { + return '/images/character-left.gif'; + } else { + return '/images/character-right.gif'; + } + }(); return A2( _elm_lang$svg$Svg$image, { ctor: '::', - _0: _elm_lang$svg$Svg_Attributes$xlinkHref('/images/character.gif'), + _0: _elm_lang$svg$Svg_Attributes$xlinkHref(characterImage), _1: { ctor: '::', _0: _elm_lang$svg$Svg_Attributes$x( @@ -16271,8 +16294,8 @@ var _user$project$Platformer$viewStartScreenText = A2( } }); var _user$project$Platformer$viewGameState = function (model) { - var _p0 = model.gameState; - switch (_p0.ctor) { + var _p1 = model.gameState; + switch (_p1.ctor) { case 'StartScreen': return { ctor: '::', @@ -16405,45 +16428,62 @@ var _user$project$Platformer$viewGame = function (model) { }, _user$project$Platformer$viewGameState(model)); }; -var _user$project$Platformer$viewPlayerScoreItem = function (score) { - return A2( - _elm_lang$html$Html$li, - { - ctor: '::', - _0: _elm_lang$html$Html_Attributes$class('player-item list-group-item'), - _1: {ctor: '[]'} - }, - { - ctor: '::', - _0: A2( - _elm_lang$html$Html$strong, - {ctor: '[]'}, - { - ctor: '::', - _0: _elm_lang$svg$Svg$text( - _elm_lang$core$Basics$toString(score.playerId)), - _1: {ctor: '[]'} - }), - _1: { +var _user$project$Platformer$anonymousPlayer = { + displayName: _elm_lang$core$Maybe$Just('Anonymous User'), + id: 0, + score: 0, + username: 'anonymous' +}; +var _user$project$Platformer$viewPlayerScoreItem = F2( + function (model, gameplay) { + var currentPlayer = A2( + _elm_lang$core$Maybe$withDefault, + _user$project$Platformer$anonymousPlayer, + _elm_lang$core$List$head( + A2( + _elm_lang$core$List$filter, + function (player) { + return _elm_lang$core$Native_Utils.eq(player.id, gameplay.playerId); + }, + model.playersList))); + var displayName = A2(_elm_lang$core$Maybe$withDefault, currentPlayer.username, currentPlayer.displayName); + return A2( + _elm_lang$html$Html$li, + { + ctor: '::', + _0: _elm_lang$html$Html_Attributes$class('player-item list-group-item'), + _1: {ctor: '[]'} + }, + { ctor: '::', _0: A2( - _elm_lang$html$Html$span, - { - ctor: '::', - _0: _elm_lang$html$Html_Attributes$class('badge'), - _1: {ctor: '[]'} - }, + _elm_lang$html$Html$strong, + {ctor: '[]'}, { ctor: '::', - _0: _elm_lang$svg$Svg$text( - _elm_lang$core$Basics$toString(score.playerScore)), + _0: _elm_lang$svg$Svg$text(displayName), _1: {ctor: '[]'} }), - _1: {ctor: '[]'} - } - }); -}; -var _user$project$Platformer$viewPlayerScoresList = function (scores) { + _1: { + ctor: '::', + _0: A2( + _elm_lang$html$Html$span, + { + ctor: '::', + _0: _elm_lang$html$Html_Attributes$class('badge'), + _1: {ctor: '[]'} + }, + { + ctor: '::', + _0: _elm_lang$svg$Svg$text( + _elm_lang$core$Basics$toString(gameplay.playerScore)), + _1: {ctor: '[]'} + }), + _1: {ctor: '[]'} + } + }); + }); +var _user$project$Platformer$viewGameplaysList = function (model) { return A2( _elm_lang$html$Html$div, { @@ -16462,7 +16502,7 @@ var _user$project$Platformer$viewPlayerScoresList = function (scores) { }, { ctor: '::', - _0: _elm_lang$svg$Svg$text('Leaderboard'), + _0: _elm_lang$svg$Svg$text('Scores'), _1: {ctor: '[]'} }), _1: { @@ -16474,13 +16514,16 @@ var _user$project$Platformer$viewPlayerScoresList = function (scores) { _0: _elm_lang$html$Html_Attributes$class('list-group'), _1: {ctor: '[]'} }, - A2(_elm_lang$core$List$map, _user$project$Platformer$viewPlayerScoreItem, scores)), + A2( + _elm_lang$core$List$map, + _user$project$Platformer$viewPlayerScoreItem(model), + model.gameplays)), _1: {ctor: '[]'} } }); }; -var _user$project$Platformer$viewPlayerScoresIndex = function (model) { - return _elm_lang$core$List$isEmpty(model.playerScores) ? A2( +var _user$project$Platformer$viewGameplaysIndex = function (model) { + return _elm_lang$core$List$isEmpty(model.gameplays) ? A2( _elm_lang$html$Html$div, {ctor: '[]'}, {ctor: '[]'}) : A2( @@ -16506,7 +16549,7 @@ var _user$project$Platformer$viewPlayerScoresIndex = function (model) { }), _1: { ctor: '::', - _0: _user$project$Platformer$viewPlayerScoresList(model.playerScores), + _0: _user$project$Platformer$viewGameplaysList(model), _1: {ctor: '[]'} } }); @@ -16521,6 +16564,24 @@ var _user$project$Platformer$initialChannel = _fbonetti$elm_phoenix_socket$Phoen var _user$project$Platformer$Flags = function (a) { return {token: a}; }; +var _user$project$Platformer$Gameplay = F3( + function (a, b, c) { + return {gameId: a, playerId: b, playerScore: c}; + }); +var _user$project$Platformer$decodeGameplay = A4( + _elm_lang$core$Json_Decode$map3, + _user$project$Platformer$Gameplay, + A2(_elm_lang$core$Json_Decode$field, 'game_id', _elm_lang$core$Json_Decode$int), + A2(_elm_lang$core$Json_Decode$field, 'player_id', _elm_lang$core$Json_Decode$int), + A2(_elm_lang$core$Json_Decode$field, 'player_score', _elm_lang$core$Json_Decode$int)); +var _user$project$Platformer$decodeGameplaysList = A2( + _elm_lang$core$Json_Decode$at, + { + ctor: '::', + _0: 'data', + _1: {ctor: '[]'} + }, + _elm_lang$core$Json_Decode$list(_user$project$Platformer$decodeGameplay)); var _user$project$Platformer$Player = F4( function (a, b, c, d) { return {displayName: a, id: b, score: c, username: d}; @@ -16554,7 +16615,9 @@ var _user$project$Platformer$Model = function (a) { return function (k) { return function (l) { return function (m) { - return {errors: a, gameId: b, gameState: c, characterPositionX: d, characterPositionY: e, itemPositionX: f, itemPositionY: g, itemsCollected: h, phxSocket: i, playersList: j, playerScore: k, playerScores: l, timeRemaining: m}; + return function (n) { + return {characterDirection: a, characterPositionX: b, characterPositionY: c, errors: d, gameId: e, gameplays: f, gameState: g, itemPositionX: h, itemPositionY: i, itemsCollected: j, phxSocket: k, playersList: l, playerScore: m, timeRemaining: n}; + }; }; }; }; @@ -16568,16 +16631,8 @@ var _user$project$Platformer$Model = function (a) { }; }; }; -var _user$project$Platformer$Score = F3( - function (a, b, c) { - return {gameId: a, playerId: b, playerScore: c}; - }); -var _user$project$Platformer$scoreDecoder = A4( - _elm_lang$core$Json_Decode$map3, - _user$project$Platformer$Score, - A2(_elm_lang$core$Json_Decode$field, 'game_id', _elm_lang$core$Json_Decode$int), - A2(_elm_lang$core$Json_Decode$field, 'player_id', _elm_lang$core$Json_Decode$int), - A2(_elm_lang$core$Json_Decode$field, 'player_score', _elm_lang$core$Json_Decode$int)); +var _user$project$Platformer$Right = {ctor: 'Right'}; +var _user$project$Platformer$Left = {ctor: 'Left'}; var _user$project$Platformer$GameOver = {ctor: 'GameOver'}; var _user$project$Platformer$Success = {ctor: 'Success'}; var _user$project$Platformer$Playing = {ctor: 'Playing'}; @@ -16588,36 +16643,6 @@ var _user$project$Platformer$TimeUpdate = function (a) { var _user$project$Platformer$SetNewItemPositionX = function (a) { return {ctor: 'SetNewItemPositionX', _0: a}; }; -var _user$project$Platformer$SendScoreRequest = {ctor: 'SendScoreRequest'}; -var _user$project$Platformer$viewSendScoreButton = A2( - _elm_lang$html$Html$div, - {ctor: '[]'}, - { - ctor: '::', - _0: A2( - _elm_lang$html$Html$button, - { - ctor: '::', - _0: _elm_lang$html$Html_Events$onClick(_user$project$Platformer$SendScoreRequest), - _1: { - ctor: '::', - _0: _elm_lang$html$Html_Attributes$class('btn btn-primary'), - _1: {ctor: '[]'} - } - }, - { - ctor: '::', - _0: _elm_lang$svg$Svg$text('Send Score'), - _1: {ctor: '[]'} - }), - _1: {ctor: '[]'} - }); -var _user$project$Platformer$SendScoreError = function (a) { - return {ctor: 'SendScoreError', _0: a}; -}; -var _user$project$Platformer$SendScore = function (a) { - return {ctor: 'SendScore', _0: a}; -}; var _user$project$Platformer$SaveScoreRequest = {ctor: 'SaveScoreRequest'}; var _user$project$Platformer$viewSaveScoreButton = A2( _elm_lang$html$Html$div, @@ -16651,15 +16676,11 @@ var _user$project$Platformer$view = function (model) { _0: _user$project$Platformer$viewGame(model), _1: { ctor: '::', - _0: _user$project$Platformer$viewSendScoreButton, + _0: _user$project$Platformer$viewSaveScoreButton, _1: { ctor: '::', - _0: _user$project$Platformer$viewSaveScoreButton, - _1: { - ctor: '::', - _0: _user$project$Platformer$viewPlayerScoresIndex(model), - _1: {ctor: '[]'} - } + _0: _user$project$Platformer$viewGameplaysIndex(model), + _1: {ctor: '[]'} } } }); @@ -16686,11 +16707,11 @@ var _user$project$Platformer$initialSocket = function (flags) { _user$project$Platformer$ReceiveScoreChanges, A4( _fbonetti$elm_phoenix_socket$Phoenix_Socket$on, - 'shout', + 'save_score', 'score:platformer', - _user$project$Platformer$SendScore, + _user$project$Platformer$SaveScore, _fbonetti$elm_phoenix_socket$Phoenix_Socket$withDebug( - _fbonetti$elm_phoenix_socket$Phoenix_Socket$init(prodSocketServer))))); + _fbonetti$elm_phoenix_socket$Phoenix_Socket$init(devSocketServer))))); }; var _user$project$Platformer$initialSocketJoin = function (flags) { return _elm_lang$core$Tuple$first( @@ -16698,18 +16719,19 @@ var _user$project$Platformer$initialSocketJoin = function (flags) { }; var _user$project$Platformer$initialModel = function (flags) { return { + characterDirection: _user$project$Platformer$Right, + characterPositionX: 50, + characterPositionY: 300, errors: '', gameId: 1, + gameplays: {ctor: '[]'}, gameState: _user$project$Platformer$StartScreen, - characterPositionX: 50, - characterPositionY: 300, itemPositionX: 150, itemPositionY: 300, itemsCollected: 0, phxSocket: _user$project$Platformer$initialSocketJoin(flags), playersList: {ctor: '[]'}, playerScore: 0, - playerScores: {ctor: '[]'}, timeRemaining: 10 }; }; @@ -16720,20 +16742,10 @@ var _user$project$Platformer$initialSocketCommand = function (flags) { var _user$project$Platformer$PhoenixMsg = function (a) { return {ctor: 'PhoenixMsg', _0: a}; }; -var _user$project$Platformer$init = function (flags) { - return { - ctor: '_Tuple2', - _0: _user$project$Platformer$initialModel(flags), - _1: A2( - _elm_lang$core$Platform_Cmd$map, - _user$project$Platformer$PhoenixMsg, - _user$project$Platformer$initialSocketCommand(flags)) - }; -}; var _user$project$Platformer$update = F2( function (msg, model) { - var _p1 = msg; - switch (_p1.ctor) { + var _p2 = msg; + switch (_p2.ctor) { case 'NoOp': return {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; case 'CountdownTimer': @@ -16744,14 +16756,35 @@ var _user$project$Platformer$update = F2( {timeRemaining: model.timeRemaining - 1}), _1: _elm_lang$core$Platform_Cmd$none } : {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; + case 'FetchGameplaysList': + var _p3 = _p2._0; + if (_p3.ctor === 'Ok') { + return { + ctor: '_Tuple2', + _0: _elm_lang$core$Native_Utils.update( + model, + {gameplays: _p3._0}), + _1: _elm_lang$core$Platform_Cmd$none + }; + } else { + return { + ctor: '_Tuple2', + _0: _elm_lang$core$Native_Utils.update( + model, + { + errors: _elm_lang$core$Basics$toString(_p3._0) + }), + _1: _elm_lang$core$Platform_Cmd$none + }; + } case 'FetchPlayersList': - var _p2 = _p1._0; - if (_p2.ctor === 'Ok') { + var _p4 = _p2._0; + if (_p4.ctor === 'Ok') { return { ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( model, - {playersList: _p2._0}), + {playersList: _p4._0}), _1: _elm_lang$core$Platform_Cmd$none }; } else { @@ -16760,20 +16793,20 @@ var _user$project$Platformer$update = F2( _0: _elm_lang$core$Native_Utils.update( model, { - errors: _elm_lang$core$Basics$toString(_p2._0) + errors: _elm_lang$core$Basics$toString(_p4._0) }), _1: _elm_lang$core$Platform_Cmd$none }; } case 'KeyDown': - var _p3 = _p1._0; - switch (_p3) { + var _p5 = _p2._0; + switch (_p5) { case 32: return (!_elm_lang$core$Native_Utils.eq(model.gameState, _user$project$Platformer$Playing)) ? { ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( model, - {gameState: _user$project$Platformer$Playing, characterPositionX: 50, playerScore: 0, itemsCollected: 0, timeRemaining: 10}), + {characterDirection: _user$project$Platformer$Right, characterPositionX: 50, itemsCollected: 0, gameState: _user$project$Platformer$Playing, playerScore: 0, timeRemaining: 10}), _1: _elm_lang$core$Platform_Cmd$none } : {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; case 37: @@ -16781,7 +16814,7 @@ var _user$project$Platformer$update = F2( ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( model, - {characterPositionX: model.characterPositionX - 15}), + {characterDirection: _user$project$Platformer$Left, characterPositionX: model.characterPositionX - 15}), _1: _elm_lang$core$Platform_Cmd$none } : {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; case 39: @@ -16789,16 +16822,16 @@ var _user$project$Platformer$update = F2( ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( model, - {characterPositionX: model.characterPositionX + 15}), + {characterDirection: _user$project$Platformer$Right, characterPositionX: model.characterPositionX + 15}), _1: _elm_lang$core$Platform_Cmd$none } : {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; default: return {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; } case 'PhoenixMsg': - var _p4 = A2(_fbonetti$elm_phoenix_socket$Phoenix_Socket$update, _p1._0, model.phxSocket); - var phxSocket = _p4._0; - var phxCmd = _p4._1; + var _p6 = A2(_fbonetti$elm_phoenix_socket$Phoenix_Socket$update, _p2._0, model.phxSocket); + var phxSocket = _p6._0; + var phxCmd = _p6._1; return { ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( @@ -16807,14 +16840,14 @@ var _user$project$Platformer$update = F2( _1: A2(_elm_lang$core$Platform_Cmd$map, _user$project$Platformer$PhoenixMsg, phxCmd) }; case 'ReceiveScoreChanges': - var _p5 = A2(_elm_lang$core$Json_Decode$decodeValue, _user$project$Platformer$scoreDecoder, _p1._0); - if (_p5.ctor === 'Ok') { + var _p7 = A2(_elm_lang$core$Json_Decode$decodeValue, _user$project$Platformer$decodeGameplay, _p2._0); + if (_p7.ctor === 'Ok') { return { ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( model, { - playerScores: {ctor: '::', _0: _p5._0, _1: model.playerScores} + gameplays: {ctor: '::', _0: _p7._0, _1: model.gameplays} }), _1: _elm_lang$core$Platform_Cmd$none }; @@ -16823,7 +16856,7 @@ var _user$project$Platformer$update = F2( ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( model, - {errors: _p5._0}), + {errors: _p7._0}), _1: _elm_lang$core$Platform_Cmd$none }; } @@ -16832,7 +16865,7 @@ var _user$project$Platformer$update = F2( case 'SaveScoreError': return A2( _elm_lang$core$Debug$log, - 'Error saveing score over socket.', + 'Error saving score over socket.', {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}); case 'SaveScoreRequest': var payload = _elm_lang$core$Json_Encode$object( @@ -16855,47 +16888,9 @@ var _user$project$Platformer$update = F2( _fbonetti$elm_phoenix_socket$Phoenix_Push$withPayload, payload, A2(_fbonetti$elm_phoenix_socket$Phoenix_Push$init, 'save_score', 'score:platformer')))); - var _p6 = A2(_fbonetti$elm_phoenix_socket$Phoenix_Socket$push, phxPush, model.phxSocket); - var phxSocket = _p6._0; - var phxCmd = _p6._1; - return { - ctor: '_Tuple2', - _0: _elm_lang$core$Native_Utils.update( - model, - {phxSocket: phxSocket}), - _1: A2(_elm_lang$core$Platform_Cmd$map, _user$project$Platformer$PhoenixMsg, phxCmd) - }; - case 'SendScore': - return {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}; - case 'SendScoreError': - return A2( - _elm_lang$core$Debug$log, - 'Error sending score over socket.', - {ctor: '_Tuple2', _0: model, _1: _elm_lang$core$Platform_Cmd$none}); - case 'SendScoreRequest': - var payload = _elm_lang$core$Json_Encode$object( - { - ctor: '::', - _0: { - ctor: '_Tuple2', - _0: 'player_score', - _1: _elm_lang$core$Json_Encode$int(model.playerScore) - }, - _1: {ctor: '[]'} - }); - var phxPush = A2( - _fbonetti$elm_phoenix_socket$Phoenix_Push$onError, - _user$project$Platformer$SendScoreError, - A2( - _fbonetti$elm_phoenix_socket$Phoenix_Push$onOk, - _user$project$Platformer$SendScore, - A2( - _fbonetti$elm_phoenix_socket$Phoenix_Push$withPayload, - payload, - A2(_fbonetti$elm_phoenix_socket$Phoenix_Push$init, 'shout', 'score:platformer')))); - var _p7 = A2(_fbonetti$elm_phoenix_socket$Phoenix_Socket$push, phxPush, model.phxSocket); - var phxSocket = _p7._0; - var phxCmd = _p7._1; + var _p8 = A2(_fbonetti$elm_phoenix_socket$Phoenix_Socket$push, phxPush, model.phxSocket); + var phxSocket = _p8._0; + var phxCmd = _p8._1; return { ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( @@ -16908,7 +16903,7 @@ var _user$project$Platformer$update = F2( ctor: '_Tuple2', _0: _elm_lang$core$Native_Utils.update( model, - {itemPositionX: _p1._0}), + {itemPositionX: _p2._0}), _1: _elm_lang$core$Platform_Cmd$none }; default: @@ -16946,6 +16941,36 @@ var _user$project$Platformer$fetchPlayersList = A2( _elm_lang$http$Http$send, _user$project$Platformer$FetchPlayersList, A2(_elm_lang$http$Http$get, '/api/players', _user$project$Platformer$decodePlayersList)); +var _user$project$Platformer$FetchGameplaysList = function (a) { + return {ctor: 'FetchGameplaysList', _0: a}; +}; +var _user$project$Platformer$fetchGameplaysList = A2( + _elm_lang$http$Http$send, + _user$project$Platformer$FetchGameplaysList, + A2(_elm_lang$http$Http$get, '/api/gameplays', _user$project$Platformer$decodeGameplaysList)); +var _user$project$Platformer$init = function (flags) { + return { + ctor: '_Tuple2', + _0: _user$project$Platformer$initialModel(flags), + _1: _elm_lang$core$Platform_Cmd$batch( + { + ctor: '::', + _0: _user$project$Platformer$fetchGameplaysList, + _1: { + ctor: '::', + _0: _user$project$Platformer$fetchPlayersList, + _1: { + ctor: '::', + _0: A2( + _elm_lang$core$Platform_Cmd$map, + _user$project$Platformer$PhoenixMsg, + _user$project$Platformer$initialSocketCommand(flags)), + _1: {ctor: '[]'} + } + } + }) + }; +}; var _user$project$Platformer$CountdownTimer = function (a) { return {ctor: 'CountdownTimer', _0: a}; }; @@ -16987,7 +17012,7 @@ if (typeof _user$project$Main$main !== 'undefined') { } Elm['Platformer'] = Elm['Platformer'] || {}; if (typeof _user$project$Platformer$main !== 'undefined') { - _user$project$Platformer$main(Elm['Platformer'], 'Platformer', {"types":{"unions":{"Dict.LeafColor":{"args":[],"tags":{"LBBlack":[],"LBlack":[]}},"Platformer.Msg":{"args":[],"tags":{"SetNewItemPositionX":["Int"],"SendScoreRequest":[],"SaveScoreRequest":[],"CountdownTimer":["Time.Time"],"FetchPlayersList":["Result.Result Http.Error (List Platformer.Player)"],"SaveScore":["Json.Encode.Value"],"SendScore":["Json.Encode.Value"],"PhoenixMsg":["Phoenix.Socket.Msg Platformer.Msg"],"TimeUpdate":["Time.Time"],"SendScoreError":["Json.Encode.Value"],"KeyDown":["Keyboard.KeyCode"],"ReceiveScoreChanges":["Json.Encode.Value"],"NoOp":[],"SaveScoreError":["Json.Encode.Value"]}},"Json.Encode.Value":{"args":[],"tags":{"Value":[]}},"Dict.Dict":{"args":["k","v"],"tags":{"RBNode_elm_builtin":["Dict.NColor","k","v","Dict.Dict k v","Dict.Dict k v"],"RBEmpty_elm_builtin":["Dict.LeafColor"]}},"Maybe.Maybe":{"args":["a"],"tags":{"Just":["a"],"Nothing":[]}},"Dict.NColor":{"args":[],"tags":{"BBlack":[],"Red":[],"NBlack":[],"Black":[]}},"Http.Error":{"args":[],"tags":{"BadUrl":["String"],"NetworkError":[],"Timeout":[],"BadStatus":["Http.Response String"],"BadPayload":["String","Http.Response String"]}},"Result.Result":{"args":["error","value"],"tags":{"Ok":["value"],"Err":["error"]}},"Phoenix.Socket.Msg":{"args":["msg"],"tags":{"ChannelErrored":["String"],"ChannelClosed":["String"],"ExternalMsg":["msg"],"ChannelJoined":["String"],"Heartbeat":["Time.Time"],"NoOp":[],"ReceiveReply":["String","Int"]}}},"aliases":{"Http.Response":{"args":["body"],"type":"{ url : String , status : { code : Int, message : String } , headers : Dict.Dict String String , body : body }"},"Keyboard.KeyCode":{"args":[],"type":"Int"},"Platformer.Player":{"args":[],"type":"{ displayName : Maybe.Maybe String , id : Int , score : Int , username : String }"},"Time.Time":{"args":[],"type":"Float"}},"message":"Platformer.Msg"},"versions":{"elm":"0.18.0"}}); + _user$project$Platformer$main(Elm['Platformer'], 'Platformer', {"types":{"unions":{"Dict.LeafColor":{"args":[],"tags":{"LBBlack":[],"LBlack":[]}},"Platformer.Msg":{"args":[],"tags":{"FetchGameplaysList":["Result.Result Http.Error (List Platformer.Gameplay)"],"SetNewItemPositionX":["Int"],"SaveScoreRequest":[],"CountdownTimer":["Time.Time"],"FetchPlayersList":["Result.Result Http.Error (List Platformer.Player)"],"SaveScore":["Json.Encode.Value"],"PhoenixMsg":["Phoenix.Socket.Msg Platformer.Msg"],"TimeUpdate":["Time.Time"],"KeyDown":["Keyboard.KeyCode"],"ReceiveScoreChanges":["Json.Encode.Value"],"NoOp":[],"SaveScoreError":["Json.Encode.Value"]}},"Json.Encode.Value":{"args":[],"tags":{"Value":[]}},"Dict.Dict":{"args":["k","v"],"tags":{"RBNode_elm_builtin":["Dict.NColor","k","v","Dict.Dict k v","Dict.Dict k v"],"RBEmpty_elm_builtin":["Dict.LeafColor"]}},"Maybe.Maybe":{"args":["a"],"tags":{"Just":["a"],"Nothing":[]}},"Dict.NColor":{"args":[],"tags":{"BBlack":[],"Red":[],"NBlack":[],"Black":[]}},"Http.Error":{"args":[],"tags":{"BadUrl":["String"],"NetworkError":[],"Timeout":[],"BadStatus":["Http.Response String"],"BadPayload":["String","Http.Response String"]}},"Result.Result":{"args":["error","value"],"tags":{"Ok":["value"],"Err":["error"]}},"Phoenix.Socket.Msg":{"args":["msg"],"tags":{"ChannelErrored":["String"],"ChannelClosed":["String"],"ExternalMsg":["msg"],"ChannelJoined":["String"],"Heartbeat":["Time.Time"],"NoOp":[],"ReceiveReply":["String","Int"]}}},"aliases":{"Http.Response":{"args":["body"],"type":"{ url : String , status : { code : Int, message : String } , headers : Dict.Dict String String , body : body }"},"Keyboard.KeyCode":{"args":[],"type":"Int"},"Platformer.Gameplay":{"args":[],"type":"{ gameId : Int, playerId : Int, playerScore : Int }"},"Platformer.Player":{"args":[],"type":"{ displayName : Maybe.Maybe String , id : Int , score : Int , username : String }"},"Time.Time":{"args":[],"type":"Float"}},"message":"Platformer.Msg"},"versions":{"elm":"0.18.0"}}); } if (typeof define === "function" && define['amd']) diff --git a/assets/static/images/character-left.gif b/assets/static/images/character-left.gif new file mode 100644 index 0000000..ce49afe Binary files /dev/null and b/assets/static/images/character-left.gif differ diff --git a/assets/static/images/character-right.gif b/assets/static/images/character-right.gif new file mode 100644 index 0000000..cb86973 Binary files /dev/null and b/assets/static/images/character-right.gif differ diff --git a/lib/platform/accounts/accounts.ex b/lib/platform/accounts/accounts.ex index fc105fe..cc6d222 100644 --- a/lib/platform/accounts/accounts.ex +++ b/lib/platform/accounts/accounts.ex @@ -18,7 +18,10 @@ defmodule Platform.Accounts do """ def list_players do - Repo.all(Player) + Player + |> preload(:games) + |> preload(:gameplays) + |> Repo.all() end @doc """ @@ -35,7 +38,12 @@ defmodule Platform.Accounts do ** (Ecto.NoResultsError) """ - def get_player!(id), do: Repo.get!(Player, id) + def get_player!(id) do + Player + |> preload(:games) + |> preload(:gameplays) + |> Repo.get!(id) + end @doc """ Creates a player. diff --git a/lib/platform/accounts/player.ex b/lib/platform/accounts/player.ex index 51df7a3..fcbe6b4 100644 --- a/lib/platform/accounts/player.ex +++ b/lib/platform/accounts/player.ex @@ -8,6 +8,7 @@ defmodule Platform.Accounts.Player do schema "players" do many_to_many :games, Game, join_through: Gameplay + has_many :gameplays, Gameplay field :display_name, :string field :password, :string, virtual: true @@ -21,6 +22,8 @@ defmodule Platform.Accounts.Player do @doc false def changeset(%Player{} = player, attrs) do player + |> cast_assoc(:games) + |> Ecto.build_assoc(:gameplays) |> cast(attrs, [:display_name, :password, :score, :username]) |> validate_required([:username]) |> unique_constraint(:username) diff --git a/lib/platform/products/game.ex b/lib/platform/products/game.ex index e49a546..b4573eb 100644 --- a/lib/platform/products/game.ex +++ b/lib/platform/products/game.ex @@ -1,13 +1,14 @@ defmodule Platform.Products.Game do use Ecto.Schema import Ecto.Changeset + alias Platform.Accounts.Player alias Platform.Products.Game alias Platform.Products.Gameplay - alias Platform.Accounts.Player schema "games" do many_to_many :players, Player, join_through: Gameplay + has_many :gameplays, Gameplay field :description, :string field :featured, :boolean, default: false @@ -21,6 +22,8 @@ defmodule Platform.Products.Game do @doc false def changeset(%Game{} = game, attrs) do game + |> cast_assoc(:players) + |> Ecto.build_assoc(:gameplays) |> cast(attrs, [:description, :featured, :slug, :thumbnail, :title]) |> validate_required([:description, :featured, :slug, :thumbnail, :title]) |> unique_constraint(:slug) diff --git a/lib/platform/products/gameplay.ex b/lib/platform/products/gameplay.ex index d28398f..6c085b9 100644 --- a/lib/platform/products/gameplay.ex +++ b/lib/platform/products/gameplay.ex @@ -17,6 +17,8 @@ defmodule Platform.Products.Gameplay do @doc false def changeset(%Gameplay{} = gameplay, attrs) do gameplay + # |> cast_assoc(:game) + # |> cast_assoc(:player) |> cast(attrs, [:game_id, :player_id, :player_score]) |> validate_required([:game_id, :player_id, :player_score]) end diff --git a/lib/platform/products/products.ex b/lib/platform/products/products.ex index 747f443..f6a0db9 100644 --- a/lib/platform/products/products.ex +++ b/lib/platform/products/products.ex @@ -6,6 +6,7 @@ defmodule Platform.Products do import Ecto.Query, warn: false alias Platform.Repo + alias Platform.Accounts alias Platform.Products.Game alias Platform.Products.Gameplay @@ -19,11 +20,10 @@ defmodule Platform.Products do """ def list_games do - Repo.all(Game) - end - - def list_gameplays do - Repo.all(Gameplay) + Game + |> preload(:players) + |> preload(:gameplays) + |> Repo.all() end @doc """ @@ -40,12 +40,15 @@ defmodule Platform.Products do ** (Ecto.NoResultsError) """ - def get_game!(id), do: Repo.get!(Game, id) - def get_game_by_slug!(slug), do: Repo.get_by!(Game, slug: slug) + def get_game!(id) do + Game + |> preload(:players) + |> preload(:gameplays) + |> Repo.get!(id) + end - def get_gameplays_by_id!(id) do - query = from gp in "gameplays", where: gp.game_id == ^id, select: [:player_id, :player_score] - Repo.all(query) + def get_game_by_slug!(slug) do + Repo.get_by!(Game, slug: slug) end @doc """ @@ -66,11 +69,6 @@ defmodule Platform.Products do |> Repo.insert() end - def create_gameplay(attrs \\ %{}) do - %Gameplay{} - |> Gameplay.changeset(attrs) - |> Repo.insert() - end @doc """ Updates a game. @@ -118,4 +116,118 @@ defmodule Platform.Products do def change_game(%Game{} = game) do Game.changeset(game, %{}) end + + @doc """ + Returns the list of gameplays. + + ## Examples + + iex> list_gameplays() + [%Gameplay{}, ...] + + """ + def list_gameplays do + Gameplay + |> preload(:game) + |> preload(:player) + |> Repo.all() + end + + @doc """ + Gets a single gameplay. + + Raises `Ecto.NoResultsError` if the Gameplay does not exist. + + ## Examples + + iex> get_gameplay!(123) + %Gameplay{} + + iex> get_gameplay!(456) + ** (Ecto.NoResultsError) + + """ + def get_gameplay!(id) do + Gameplay + |> preload(:game) + |> preload(:player) + |> Repo.get!(id) + end + + def get_gameplays_by_id!(id) do + query = from gp in "gameplays", where: gp.game_id == ^id, select: [:player_id, :player_score] + Repo.all(query) + end + + @doc """ + Creates a gameplay. + + ## Examples + + iex> create_gameplay(%{field: value}) + {:ok, %Gameplay{}} + + iex> create_gameplay(%{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def create_gameplay(attrs \\ %{}) do + attrs = Enum.into(attrs, %{game_id: attrs[:game].id, player_id: attrs[:player].id}) + + # Update player total score. + # player = Accounts.get_player!(attrs[:player_id]) + # Accounts.update_player(player, %{score: player.score + attrs[:player_score]}) + + # Create gameplay record. + %Gameplay{} + |> Gameplay.changeset(attrs) + |> Repo.insert() + end + + @doc """ + Updates a gameplay. + + ## Examples + + iex> update_gameplay(gameplay, %{field: new_value}) + {:ok, %Gameplay{}} + + iex> update_gameplay(gameplay, %{field: bad_value}) + {:error, %Ecto.Changeset{}} + + """ + def update_gameplay(%Gameplay{} = gameplay, attrs) do + gameplay + |> Gameplay.changeset(attrs) + |> Repo.update() + end + + @doc """ + Deletes a Gameplay. + + ## Examples + + iex> delete_gameplay(gameplay) + {:ok, %Gameplay{}} + + iex> delete_gameplay(gameplay) + {:error, %Ecto.Changeset{}} + + """ + def delete_gameplay(%Gameplay{} = gameplay) do + Repo.delete(gameplay) + end + + @doc """ + Returns an `%Ecto.Changeset{}` for tracking gameplay changes. + + ## Examples + + iex> change_gameplay(gameplay) + %Ecto.Changeset{source: %Gameplay{}} + + """ + def change_gameplay(%Gameplay{} = gameplay) do + Gameplay.changeset(gameplay, %{}) + end end diff --git a/lib/platform_web/controllers/gameplay_controller.ex b/lib/platform_web/controllers/gameplay_controller.ex new file mode 100644 index 0000000..67fcc2d --- /dev/null +++ b/lib/platform_web/controllers/gameplay_controller.ex @@ -0,0 +1,42 @@ +defmodule PlatformWeb.GameplayController do + use PlatformWeb, :controller + + alias Platform.Products + alias Platform.Products.Gameplay + + action_fallback PlatformWeb.FallbackController + + def index(conn, _params) do + gameplays = Products.list_gameplays() + render(conn, "index.json", gameplays: gameplays) + end + + def create(conn, %{"gameplay" => gameplay_params}) do + with {:ok, %Gameplay{} = gameplay} <- Products.create_gameplay(gameplay_params) do + conn + |> put_status(:created) + |> put_resp_header("location", gameplay_path(conn, :show, gameplay)) + |> render("show.json", gameplay: gameplay) + end + end + + def show(conn, %{"id" => id}) do + gameplay = Products.get_gameplay!(id) + render(conn, "show.json", gameplay: gameplay) + end + + def update(conn, %{"id" => id, "gameplay" => gameplay_params}) do + gameplay = Products.get_gameplay!(id) + + with {:ok, %Gameplay{} = gameplay} <- Products.update_gameplay(gameplay, gameplay_params) do + render(conn, "show.json", gameplay: gameplay) + end + end + + def delete(conn, %{"id" => id}) do + gameplay = Products.get_gameplay!(id) + with {:ok, %Gameplay{}} <- Products.delete_gameplay(gameplay) do + send_resp(conn, :no_content, "") + end + end +end diff --git a/lib/platform_web/router.ex b/lib/platform_web/router.ex index de76ace..f9178ae 100644 --- a/lib/platform_web/router.ex +++ b/lib/platform_web/router.ex @@ -27,8 +27,9 @@ defmodule PlatformWeb.Router do scope "/api", PlatformWeb do pipe_through :api - resources "/players", PlayerApiController, except: [:new, :edit] resources "/games", GameController, except: [:new, :edit] + resources "/gameplays", GameplayController, except: [:new, :edit] + resources "/players", PlayerApiController, except: [:new, :edit] end defp put_user_token(conn, _) do diff --git a/lib/platform_web/templates/player/show.html.eex b/lib/platform_web/templates/player/show.html.eex index a1359ee..7e93f25 100644 --- a/lib/platform_web/templates/player/show.html.eex +++ b/lib/platform_web/templates/player/show.html.eex @@ -1,11 +1,38 @@ -

Show Player

- - +
+

Show Player

+ + +
+ +<%= if Enum.count(@player.gameplays) > 0 do %> +
+

Player Scores

+ + + + + + + + + + + <%= for gameplay <- @player.gameplays do %> + + + + + + <% end %> + +
Game PlayedPlayer IDPlayer Score
<%= game_title(gameplay.game_id) %><%= player_name(gameplay.player_id) %><%= gameplay.player_score %>
+
+<% end %> <%= link "Edit", to: player_path(@conn, :edit, @player), class: "btn btn-default" %> <%= link "Back", to: page_path(@conn, :index), class: "btn btn-default" %> diff --git a/lib/platform_web/views/gameplay_view.ex b/lib/platform_web/views/gameplay_view.ex new file mode 100644 index 0000000..429b509 --- /dev/null +++ b/lib/platform_web/views/gameplay_view.ex @@ -0,0 +1,19 @@ +defmodule PlatformWeb.GameplayView do + use PlatformWeb, :view + alias PlatformWeb.GameplayView + + def render("index.json", %{gameplays: gameplays}) do + %{data: render_many(gameplays, GameplayView, "gameplay.json")} + end + + def render("show.json", %{gameplay: gameplay}) do + %{data: render_one(gameplay, GameplayView, "gameplay.json")} + end + + def render("gameplay.json", %{gameplay: gameplay}) do + %{id: gameplay.id, + game_id: gameplay.game_id, + player_id: gameplay.player_id, + player_score: gameplay.player_score} + end +end diff --git a/lib/platform_web/views/player_view.ex b/lib/platform_web/views/player_view.ex index 0b5b004..15d2d16 100644 --- a/lib/platform_web/views/player_view.ex +++ b/lib/platform_web/views/player_view.ex @@ -1,3 +1,16 @@ defmodule PlatformWeb.PlayerView do use PlatformWeb, :view + + alias Platform.Accounts + alias Platform.Products + + def game_title(game_id) do + game = Products.get_game!(game_id) + game.title + end + + def player_name(id) do + player = Accounts.get_player!(id) + if player.display_name != nil, do: player.display_name, else: player.username + end end diff --git a/mix.exs b/mix.exs index e0dc4ef..4bc7060 100644 --- a/mix.exs +++ b/mix.exs @@ -42,7 +42,8 @@ defmodule Platform.Mixfile do {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, {:comeonin, "~> 4.0"}, - {:bcrypt_elixir, "~> 0.12"} + {:bcrypt_elixir, "~> 0.12"}, + {:ex_machina, "~> 2.1", only: :test}, ] end diff --git a/mix.lock b/mix.lock index 71d59c3..778b959 100644 --- a/mix.lock +++ b/mix.lock @@ -7,6 +7,7 @@ "decimal": {:hex, :decimal, "1.4.0", "fac965ce71a46aab53d3a6ce45662806bdd708a4a95a65cde8a12eb0124a1333", [], [], "hexpm"}, "ecto": {:hex, :ecto, "2.1.6", "29b45f393c2ecd99f83e418ea9b0a2af6078ecb30f401481abac8a473c490f84", [], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.0", "992f38fabe705bb45821a728f20914c554b276838433349d4f2341f7a687cddf", [], [], "hexpm"}, + "ex_machina": {:hex, :ex_machina, "2.1.0", "4874dc9c78e7cf2d429f24dc3c4005674d4e4da6a08be961ffccc08fb528e28b", [], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [], [], "hexpm"}, "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"}, "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"}, diff --git a/priv/repo/migrations/20170826154100_create_games.exs b/priv/repo/migrations/20170826154100_create_games.exs index d657eed..2a5821d 100644 --- a/priv/repo/migrations/20170826154100_create_games.exs +++ b/priv/repo/migrations/20170826154100_create_games.exs @@ -10,13 +10,5 @@ defmodule Platform.Repo.Migrations.CreateGames do timestamps() end - - create table(:gameplays) do - add :game_id, references(:games, on_delete: :nothing), null: false - add :player_id, references(:players, on_delete: :nothing), null: false - add :player_score, :integer - - timestamps() - end end end diff --git a/priv/repo/migrations/20180103033750_create_gameplays.exs b/priv/repo/migrations/20180103033750_create_gameplays.exs new file mode 100644 index 0000000..939c73a --- /dev/null +++ b/priv/repo/migrations/20180103033750_create_gameplays.exs @@ -0,0 +1,16 @@ +defmodule Platform.Repo.Migrations.CreateGameplays do + use Ecto.Migration + + def change do + create table(:gameplays) do + add :player_score, :integer + add :game_id, references(:games, on_delete: :nothing) + add :player_id, references(:players, on_delete: :nothing) + + timestamps() + end + + create index(:gameplays, [:game_id]) + create index(:gameplays, [:player_id]) + end +end diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index e52ce83..d57bd1a 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -17,10 +17,10 @@ alias Platform.Products # Players -Accounts.create_player(%{display_name: "José Valim", username: "josevalim", password: "josevalim", score: 1000}) -Accounts.create_player(%{display_name: "Evan Czaplicki", username: "evancz", password: "evancz", score: 2000}) -Accounts.create_player(%{display_name: "Chris McCord", username: "chrismccord", password: "chrismccord", score: 3000}) +Accounts.create_player(%{display_name: "José Valim", username: "josevalim", password: "josevalim", score: 0}) +Accounts.create_player(%{display_name: "Evan Czaplicki", username: "evancz", password: "evancz", score: 0}) +Accounts.create_player(%{display_name: "Chris McCord", username: "chrismccord", password: "chrismccord", score: 0}) # Games -Products.create_game(%{title: "Platformer", slug: "platformer", description: "Platform game example.", thumbnail: "http://via.placeholder.com/300x200", featured: true}) +Products.create_game(%{title: "Platformer", slug: "platformer", description: "Platform game example.", thumbnail: "https://i.imgur.com/L6ci0xL.png", featured: true}) diff --git a/test/platform/accounts/accounts_test.exs b/test/platform/accounts/accounts_test.exs index 3c9aa9e..97f5e29 100644 --- a/test/platform/accounts/accounts_test.exs +++ b/test/platform/accounts/accounts_test.exs @@ -2,6 +2,7 @@ defmodule Platform.AccountsTest do use Platform.DataCase alias Platform.Accounts + import Platform.Factory describe "players" do alias Platform.Accounts.Player @@ -10,28 +11,13 @@ defmodule Platform.AccountsTest do @update_attrs %{display_name: "some updated display name", password: "some updated password", score: 43, username: "some updated username"} @invalid_attrs %{password: nil, username: nil} - def player_fixture(attrs \\ %{}) do - {:ok, player} = - attrs - |> Enum.into(@valid_attrs) - |> Accounts.create_player() - - player_attrs_map = - player - |> Map.from_struct() - |> Map.delete(:password) - - %Platform.Accounts.Player{} - |> Map.merge(player_attrs_map) - end - test "list_players/0 returns all players" do - player = player_fixture() + player = insert(:player) assert Accounts.list_players() == [player] end test "get_player!/1 returns the player with given id" do - player = player_fixture() + player = insert(:player) assert Accounts.get_player!(player.id) == player end @@ -46,7 +32,7 @@ defmodule Platform.AccountsTest do end test "update_player/2 with valid data updates the player" do - player = player_fixture() + player = insert(:player) assert {:ok, player} = Accounts.update_player(player, @update_attrs) assert %Player{} = player assert player.score == 43 @@ -54,19 +40,19 @@ defmodule Platform.AccountsTest do end test "update_player/2 with invalid data returns error changeset" do - player = player_fixture() + player = insert(:player) assert {:error, %Ecto.Changeset{}} = Accounts.update_player(player, @invalid_attrs) assert player == Accounts.get_player!(player.id) end test "delete_player/1 deletes the player" do - player = player_fixture() + player = insert(:player) assert {:ok, %Player{}} = Accounts.delete_player(player) assert_raise Ecto.NoResultsError, fn -> Accounts.get_player!(player.id) end end test "change_player/1 returns a player changeset" do - player = player_fixture() + player = insert(:player) assert %Ecto.Changeset{} = Accounts.change_player(player) end end diff --git a/test/platform/products/products_test.exs b/test/platform/products/products_test.exs index 15e7b00..e497eff 100644 --- a/test/platform/products/products_test.exs +++ b/test/platform/products/products_test.exs @@ -2,6 +2,7 @@ defmodule Platform.ProductsTest do use Platform.DataCase alias Platform.Products + import Platform.Factory describe "games" do alias Platform.Products.Game @@ -68,4 +69,64 @@ defmodule Platform.ProductsTest do assert %Ecto.Changeset{} = Products.change_game(game) end end + + describe "gameplays" do + alias Platform.Products.Gameplay + + @valid_attrs %{game_id: 42, player_id: 42, player_score: 42} + @update_attrs %{game_id: 42, player_id: 42, player_score: 42} + @invalid_attrs %{game_id: nil, player_id: nil, player_score: nil} + + def gameplay_fixture(attrs \\ %{}) do + {:ok, gameplay} = + attrs + |> Enum.into(@valid_attrs) + |> Products.create_gameplay() + + gameplay + end + + test "list_gameplays/0 returns all gameplays" do + gameplay = gameplay_fixture() + assert Products.list_gameplays() == [gameplay] + end + + test "get_gameplay!/1 returns the gameplay with given id" do + gameplay = gameplay_fixture() + assert Products.get_gameplay!(gameplay.id) == gameplay + end + + test "create_gameplay/1 with valid data creates a gameplay" do + assert {:ok, %Gameplay{} = gameplay} = Products.create_gameplay(@valid_attrs) + assert gameplay.player_score == 42 + end + + test "create_gameplay/1 with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Products.create_gameplay(@invalid_attrs) + end + + test "update_gameplay/2 with valid data updates the gameplay" do + gameplay = gameplay_fixture() + assert {:ok, gameplay} = Products.update_gameplay(gameplay, @update_attrs) + assert %Gameplay{} = gameplay + assert gameplay.player_score == 43 + end + + test "update_gameplay/2 with invalid data returns error changeset" do + gameplay = gameplay_fixture() + assert {:error, %Ecto.Changeset{}} = Products.update_gameplay(gameplay, @invalid_attrs) + assert gameplay == Products.get_gameplay!(gameplay.id) + end + + test "delete_gameplay/1 deletes the gameplay" do + gameplay = gameplay_fixture() + assert {:ok, %Gameplay{}} = Products.delete_gameplay(gameplay) + assert_raise Ecto.NoResultsError, fn -> Products.get_gameplay!(gameplay.id) end + end + + test "change_gameplay/1 returns a gameplay changeset" do + gameplay = gameplay_fixture() + assert %Ecto.Changeset{} = Products.change_gameplay(gameplay) + end + end end diff --git a/test/platform_web/controllers/gameplay_controller_test.exs b/test/platform_web/controllers/gameplay_controller_test.exs new file mode 100644 index 0000000..e2419ce --- /dev/null +++ b/test/platform_web/controllers/gameplay_controller_test.exs @@ -0,0 +1,79 @@ +defmodule PlatformWeb.GameplayControllerTest do + use PlatformWeb.ConnCase + + alias Platform.Products + alias Platform.Products.Gameplay + + @create_attrs %{player_score: 42} + @update_attrs %{player_score: 43} + @invalid_attrs %{player_score: nil} + + def fixture(:gameplay) do + {:ok, gameplay} = Products.create_gameplay(@create_attrs) + gameplay + end + + setup %{conn: conn} do + {:ok, conn: put_req_header(conn, "accept", "application/json")} + end + + describe "index" do + test "lists all gameplays", %{conn: conn} do + conn = get conn, gameplay_path(conn, :index) + assert json_response(conn, 200)["data"] == [] + end + end + + describe "create gameplay" do + test "renders gameplay when data is valid", %{conn: conn} do + conn = post conn, gameplay_path(conn, :create), gameplay: @create_attrs + assert %{"id" => id} = json_response(conn, 201)["data"] + + conn = get conn, gameplay_path(conn, :show, id) + assert json_response(conn, 200)["data"] == %{ + "id" => id, + "player_score" => 42} + end + + test "renders errors when data is invalid", %{conn: conn} do + conn = post conn, gameplay_path(conn, :create), gameplay: @invalid_attrs + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "update gameplay" do + setup [:create_gameplay] + + test "renders gameplay when data is valid", %{conn: conn, gameplay: %Gameplay{id: id} = gameplay} do + conn = put conn, gameplay_path(conn, :update, gameplay), gameplay: @update_attrs + assert %{"id" => ^id} = json_response(conn, 200)["data"] + + conn = get conn, gameplay_path(conn, :show, id) + assert json_response(conn, 200)["data"] == %{ + "id" => id, + "player_score" => 43} + end + + test "renders errors when data is invalid", %{conn: conn, gameplay: gameplay} do + conn = put conn, gameplay_path(conn, :update, gameplay), gameplay: @invalid_attrs + assert json_response(conn, 422)["errors"] != %{} + end + end + + describe "delete gameplay" do + setup [:create_gameplay] + + test "deletes chosen gameplay", %{conn: conn, gameplay: gameplay} do + conn = delete conn, gameplay_path(conn, :delete, gameplay) + assert response(conn, 204) + assert_error_sent 404, fn -> + get conn, gameplay_path(conn, :show, gameplay) + end + end + end + + defp create_gameplay(_) do + gameplay = fixture(:gameplay) + {:ok, gameplay: gameplay} + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex new file mode 100644 index 0000000..81930ba --- /dev/null +++ b/test/support/factory.ex @@ -0,0 +1,33 @@ +defmodule Platform.Factory do + use ExMachina.Ecto, repo: Platform.Repo + + def player_factory do + %Platform.Accounts.Player{ + display_name: "José Valim", + games: [], + gameplays: [], + username: "josevalim", + score: 0, + } + end + + def game_factory do + %Platform.Products.Game{ + description: "Platformer game example.", + featured: true, + gameplays: [], + players: [], + slug: Enum.random(0..1000) |> Integer.to_string, + thumbnail: "https://i.imgur.com/L6ci0xL.png", + title: "Platformer", + } + end + + def gameplay_factory do + %Platform.Products.Gameplay{ + game: build(:game), + player: build(:player), + player_score: 666 + } + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index ce0dee4..d319922 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -2,3 +2,4 @@ ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Platform.Repo, :manual) +{:ok, _} = Application.ensure_all_started(:ex_machina)