├── src ├── apple.png ├── challenge4.html ├── challenge2.html ├── challenge3.html ├── challenge1.html ├── challenge5.html ├── challenge1.elm ├── challenge2.elm ├── challenge3.elm ├── challenge4.elm └── challenge5.elm ├── elm.json └── README.md /src/apple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pdamoc/elmChallenges/HEAD/src/apple.png -------------------------------------------------------------------------------- /src/challenge4.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/challenge2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/challenge3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | 19 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/challenge1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.0", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.1", 10 | "elm/core": "1.0.0", 11 | "elm/html": "1.0.0", 12 | "elm/http": "1.0.0", 13 | "elm/json": "1.0.0", 14 | "elm/random": "1.0.0", 15 | "elm/time": "1.0.0", 16 | "elm-community/list-extra": "8.1.0" 17 | }, 18 | "indirect": { 19 | "elm/url": "1.0.0", 20 | "elm/virtual-dom": "1.0.2" 21 | } 22 | }, 23 | "test-dependencies": { 24 | "direct": {}, 25 | "indirect": {} 26 | } 27 | } -------------------------------------------------------------------------------- /src/challenge5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 29 | 30 | 31 | 32 | 33 | 34 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/challenge1.elm: -------------------------------------------------------------------------------- 1 | module Challenge1 exposing (main) 2 | 3 | import Browser 4 | import Browser.Events exposing (onMouseMove, onResize) 5 | import Html exposing (Html, button, div, text) 6 | import Html.Attributes exposing (class, style) 7 | import Json.Decode as Decode 8 | 9 | 10 | type alias Model = 11 | { windowWidth : Int 12 | , mouseX : Int 13 | } 14 | 15 | 16 | init : Model -> ( Model, Cmd Msg ) 17 | init initialModel = 18 | ( initialModel, Cmd.none ) 19 | 20 | 21 | type Msg 22 | = SetMouseX Int 23 | | SetWindowWidth Int 24 | 25 | 26 | update : Msg -> Model -> ( Model, Cmd Msg ) 27 | update msg model = 28 | case msg of 29 | SetMouseX mouseX -> 30 | ( { model | mouseX = mouseX }, Cmd.none ) 31 | 32 | SetWindowWidth windowWidth -> 33 | ( { model | windowWidth = windowWidth }, Cmd.none ) 34 | 35 | 36 | view : Model -> Html Msg 37 | view model = 38 | let 39 | ( message, color, backgroundColor ) = 40 | if model.mouseX < (model.windowWidth // 2) then 41 | ( "LEFT", "white", "black" ) 42 | 43 | else 44 | ( "RIGHT", "black", "white" ) 45 | in 46 | div [ class "container", style "color" color, style "background-color" backgroundColor ] 47 | [ text message ] 48 | 49 | 50 | mouseXDecoder : Decode.Decoder Int 51 | mouseXDecoder = 52 | Decode.field "clientX" Decode.int 53 | 54 | 55 | subscriptions : Model -> Sub Msg 56 | subscriptions model = 57 | Sub.batch 58 | [ onResize (\width height -> SetWindowWidth width) 59 | , onMouseMove (Decode.map SetMouseX mouseXDecoder) 60 | ] 61 | 62 | 63 | main : Program Model Model Msg 64 | main = 65 | Browser.element 66 | { init = init 67 | , view = view 68 | , update = update 69 | , subscriptions = subscriptions 70 | } 71 | -------------------------------------------------------------------------------- /src/challenge2.elm: -------------------------------------------------------------------------------- 1 | module Challenge2 exposing (main) 2 | 3 | import Browser 4 | import Browser.Events exposing (onResize) 5 | import Html exposing (Html, div, text) 6 | import Html.Attributes exposing (class, style) 7 | import Random 8 | import Time 9 | 10 | 11 | type alias WindowSize = 12 | { width : Int, height : Int } 13 | 14 | 15 | type alias Dot = 16 | { x : Float, y : Float } 17 | 18 | 19 | type alias Model = 20 | { dots : List Dot 21 | , windowSize : WindowSize 22 | } 23 | 24 | 25 | init : WindowSize -> ( Model, Cmd Msg ) 26 | init windowSize = 27 | ( { dots = [], windowSize = windowSize }, Cmd.none ) 28 | 29 | 30 | type Msg 31 | = SetWindowSize WindowSize 32 | | Tick 33 | | NewDot Dot 34 | 35 | 36 | generateNewDot : Cmd Msg 37 | generateNewDot = 38 | Random.generate NewDot 39 | (Random.pair (Random.float 0 1) (Random.float 0 1) 40 | |> Random.map (\( x, y ) -> { x = x, y = y }) 41 | ) 42 | 43 | 44 | update : Msg -> Model -> ( Model, Cmd Msg ) 45 | update msg model = 46 | case msg of 47 | SetWindowSize windowSize -> 48 | ( { model | windowSize = windowSize }, Cmd.none ) 49 | 50 | Tick -> 51 | ( model, generateNewDot ) 52 | 53 | NewDot dot -> 54 | ( { model | dots = dot :: model.dots }, Cmd.none ) 55 | 56 | 57 | view : Model -> Html Msg 58 | view model = 59 | div [] 60 | (List.map 61 | (\dot -> 62 | let 63 | left = 64 | String.fromFloat (toFloat model.windowSize.width * dot.x) ++ "px" 65 | 66 | top = 67 | String.fromFloat (toFloat model.windowSize.height * dot.y) ++ "px" 68 | in 69 | div [ class "dot", style "left" left, style "top" top ] [] 70 | ) 71 | model.dots 72 | ) 73 | 74 | 75 | subscriptions : Model -> Sub Msg 76 | subscriptions model = 77 | Sub.batch 78 | [ onResize (\width height -> SetWindowSize { width = width, height = height }) 79 | , Time.every 500 (\_ -> Tick) 80 | ] 81 | 82 | 83 | main : Program WindowSize Model Msg 84 | main = 85 | Browser.element 86 | { init = init 87 | , view = view 88 | , update = update 89 | , subscriptions = subscriptions 90 | } 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elmChallenges 2 | 3 | This is a learning project that provides some interesting challenges for the Elm programming language, intended to be completed after reading the [Elm Architecture guide](http://guide.elm-lang.org/architecture/index.html). 4 | 5 | The challenges were provided by Gil Mizrahi. 6 | 7 | The example code that comes with these challenges requires Elm 0.19 8 | 9 | ## Challenge No. 1 10 | 11 | Create a program that does the following: 12 | 13 | The program will print "Right" in the middle of the screen if the mouse cursor is on the right half of the screen or will print "Left" instead in middle of the screen if the mouse cursor in on the left half of the screen. Try to add different background and foreground colors as well :) also, resizing the browser window should not break the application. 14 | 15 | Build instructions: `elm make src/challenge1.elm --output=src/challenge1.js` and then open `src/challenge1.html`. 16 | 17 | ## Challenge No. 2 18 | 19 | Write a program that fills the screen with small blue circles, produced (pseudo) randomly and over time (choose how much time to wait between generating a new circle). 20 | 21 | 22 | Build instructions: `elm make src/challenge2.elm --output=src/challenge2.js` and then open `src/challenge2.html`. 23 | 24 | ## Challenge No. 3 25 | 26 | Add two new abilities: 27 | 28 | - when the 'P' key on the keyboard is pressed, pause the generation of new circles and when pressed again, resume. 29 | - when the 'R' key on the keyboard is pressed, reset and start the animation from the beginning. 30 | 31 | Extra: try to add two buttons which will do the same things as 'P' and 'R'. 32 | 33 | Build instructions: `elm make src/challenge3.elm --output=src/challenge3.js` and then open `src/challenge3.html`. 34 | 35 | ## Challenge No. 4 36 | 37 | Write a program that asks the user for a Github username (from a text field) and displays this user's name, avatar and lists his/her programming languages. Fetch the information only after the user stops typing for one second. 38 | 39 | 40 | Build instructions: `elm make src/challenge4.elm --output=src/challenge4.js` and then open `src/challenge4.html`. 41 | 42 | ## Challenge No. 5 43 | 44 | Write a snake clone. Make it save the top score for each user using local storage. Make it possible to start the game again without refreshing the page. 45 | 46 | Build instructions: `elm make src/challenge5.elm --output=src/challenge5.js` and then open `src/challenge5.html`. 47 | 48 | ## Challenge No. 6 49 | 50 | With the previous challenge, separate the high score from the rest of the application in its own module so it can be reused in another game. Separate the game into its own module so it can be loaded from another application. 51 | 52 | ## Solutions on Ellie 53 | 54 | - Challenge No. 1: https://ellie-app.com/3GLYq5bGBrTa1 55 | - Challenge No. 2: https://ellie-app.com/3JRgRxpsmrga1 56 | - Challenge No. 3: https://ellie-app.com/3JRgcdfNbcqa1 57 | - Challenge No. 4: https://ellie-app.com/3JRkNHxXSBKa1 58 | - Challenge No. 5: https://ellie-app.com/3JR928vMzJBa1 59 | 60 | 61 | The update for `0.19` was done by [Bryan Jennings](https://github.com/bryanjenningz). -------------------------------------------------------------------------------- /src/challenge3.elm: -------------------------------------------------------------------------------- 1 | module Challenge3 exposing (main) 2 | 3 | import Browser 4 | import Browser.Events exposing (onKeyPress, onResize) 5 | import Html exposing (Html, button, div, text) 6 | import Html.Attributes exposing (class, style) 7 | import Html.Events exposing (onClick) 8 | import Json.Decode as Decode 9 | import Random 10 | import Time 11 | 12 | 13 | type alias WindowSize = 14 | { width : Int, height : Int } 15 | 16 | 17 | type alias Dot = 18 | { x : Float, y : Float } 19 | 20 | 21 | type alias Model = 22 | { dots : List Dot 23 | , windowSize : WindowSize 24 | , isRunning : Bool 25 | } 26 | 27 | 28 | init : WindowSize -> ( Model, Cmd Msg ) 29 | init windowSize = 30 | ( { dots = [], windowSize = windowSize, isRunning = True }, Cmd.none ) 31 | 32 | 33 | type Msg 34 | = SetWindowSize WindowSize 35 | | Tick 36 | | NewDot Dot 37 | | ToggleRunning 38 | | Restart 39 | | DoNothing 40 | 41 | 42 | generateNewDot : Cmd Msg 43 | generateNewDot = 44 | Random.generate NewDot 45 | (Random.pair (Random.float 0 1) (Random.float 0 1) 46 | |> Random.map (\( x, y ) -> { x = x, y = y }) 47 | ) 48 | 49 | 50 | update : Msg -> Model -> ( Model, Cmd Msg ) 51 | update msg model = 52 | case msg of 53 | SetWindowSize windowSize -> 54 | ( { model | windowSize = windowSize }, Cmd.none ) 55 | 56 | Tick -> 57 | ( model, generateNewDot ) 58 | 59 | NewDot dot -> 60 | ( { model | dots = dot :: model.dots }, Cmd.none ) 61 | 62 | ToggleRunning -> 63 | ( { model | isRunning = not model.isRunning }, Cmd.none ) 64 | 65 | Restart -> 66 | ( { model | dots = [], isRunning = True }, Cmd.none ) 67 | 68 | DoNothing -> 69 | ( model, Cmd.none ) 70 | 71 | 72 | view : Model -> Html Msg 73 | view model = 74 | div [] 75 | [ div [] 76 | (List.map 77 | (\dot -> 78 | let 79 | left = 80 | String.fromFloat (toFloat model.windowSize.width * dot.x) ++ "px" 81 | 82 | top = 83 | String.fromFloat (toFloat model.windowSize.height * dot.y) ++ "px" 84 | in 85 | div [ class "dot", style "left" left, style "top" top ] [] 86 | ) 87 | model.dots 88 | ) 89 | , button [ onClick Restart ] [ text "Restart" ] 90 | , button [ onClick ToggleRunning ] 91 | [ text <| 92 | if model.isRunning then 93 | "Pause" 94 | 95 | else 96 | "Resume" 97 | ] 98 | ] 99 | 100 | 101 | keyPressDecoder : Decode.Decoder Msg 102 | keyPressDecoder = 103 | Decode.map 104 | (\key -> 105 | case String.toUpper key of 106 | "P" -> 107 | ToggleRunning 108 | 109 | "R" -> 110 | Restart 111 | 112 | _ -> 113 | DoNothing 114 | ) 115 | (Decode.field "key" Decode.string) 116 | 117 | 118 | subscriptions : Model -> Sub Msg 119 | subscriptions model = 120 | Sub.batch 121 | [ onResize (\width height -> SetWindowSize { width = width, height = height }) 122 | , Time.every 500 123 | (\_ -> 124 | if model.isRunning then 125 | Tick 126 | 127 | else 128 | DoNothing 129 | ) 130 | , onKeyPress keyPressDecoder 131 | ] 132 | 133 | 134 | main : Program WindowSize Model Msg 135 | main = 136 | Browser.element 137 | { init = init 138 | , view = view 139 | , update = update 140 | , subscriptions = subscriptions 141 | } 142 | -------------------------------------------------------------------------------- /src/challenge4.elm: -------------------------------------------------------------------------------- 1 | module Challenge4 exposing (main) 2 | 3 | import Browser 4 | import Html exposing (Html, div, img, input, text) 5 | import Html.Attributes exposing (placeholder, src, value) 6 | import Html.Events exposing (onInput) 7 | import Http 8 | import Json.Decode as Decode 9 | import List.Extra as List 10 | import Task exposing (Task) 11 | import Time exposing (Posix) 12 | 13 | 14 | type alias Model = 15 | { username : String 16 | , user : Maybe User 17 | , lastKeyPress : Maybe Posix 18 | , lastUsername : String 19 | } 20 | 21 | 22 | init : () -> ( Model, Cmd Msg ) 23 | init () = 24 | ( { username = "evancz" 25 | , user = Nothing 26 | , lastKeyPress = Nothing 27 | , lastUsername = "" 28 | } 29 | , Cmd.none 30 | ) 31 | 32 | 33 | type Msg 34 | = SetUsername String 35 | | GetUser 36 | | SetUser (Maybe User) 37 | | Tick Posix 38 | 39 | 40 | update : Msg -> Model -> ( Model, Cmd Msg ) 41 | update msg model = 42 | case msg of 43 | SetUsername username -> 44 | ( { model | username = username }, Cmd.none ) 45 | 46 | GetUser -> 47 | ( model, getUser model.username ) 48 | 49 | SetUser user -> 50 | ( { model | user = user }, Cmd.none ) 51 | 52 | Tick now -> 53 | case model.lastKeyPress of 54 | Nothing -> 55 | ( { model | lastKeyPress = Just now }, Cmd.none ) 56 | 57 | Just lastKeyPress -> 58 | if Time.posixToMillis now - Time.posixToMillis lastKeyPress >= 1000 then 59 | ( { model | lastKeyPress = Just now, lastUsername = model.username } 60 | , getUser model.username 61 | ) 62 | 63 | else 64 | ( model, Cmd.none ) 65 | 66 | 67 | knownLanguages : List String -> String 68 | knownLanguages languages = 69 | "Knows the following languages: " ++ String.join ", " languages 70 | 71 | 72 | view : Model -> Html Msg 73 | view model = 74 | div [] 75 | [ input 76 | [ placeholder "Github username" 77 | , onInput SetUsername 78 | , value model.username 79 | ] 80 | [] 81 | , case model.user of 82 | Nothing -> 83 | text "" 84 | 85 | Just user -> 86 | div [] 87 | [ div [] [ text user.name ] 88 | , img [ src user.picture ] [] 89 | , div [] [ text (knownLanguages user.languages) ] 90 | ] 91 | ] 92 | 93 | 94 | type alias User = 95 | { name : String 96 | , picture : String 97 | , reposUrl : String 98 | , languages : List String 99 | } 100 | 101 | 102 | getUrl : String -> Decode.Decoder a -> Task Http.Error a 103 | getUrl url decoder = 104 | Http.get url decoder 105 | |> Http.toTask 106 | 107 | 108 | userDecoder : Decode.Decoder User 109 | userDecoder = 110 | Decode.map4 User 111 | (Decode.field "name" Decode.string) 112 | (Decode.field "avatar_url" Decode.string) 113 | (Decode.field "repos_url" Decode.string) 114 | (Decode.succeed []) 115 | 116 | 117 | languagesDecoder : Decode.Decoder (List String) 118 | languagesDecoder = 119 | Decode.map 120 | (\languages -> List.unique (List.filter (\s -> s /= "") languages)) 121 | (Decode.list 122 | (Decode.oneOf 123 | [ Decode.at [ "language" ] Decode.string 124 | , Decode.succeed "" 125 | ] 126 | ) 127 | ) 128 | 129 | 130 | errorToString : Http.Error -> String 131 | errorToString error = 132 | case error of 133 | Http.BadUrl message -> 134 | "Bad Url, " ++ message 135 | 136 | Http.Timeout -> 137 | "Request timeout error" 138 | 139 | Http.NetworkError -> 140 | "Network error, make sure you're connected to the internet" 141 | 142 | Http.BadStatus response -> 143 | "Bad status, " ++ response.status.message 144 | 145 | Http.BadPayload message response -> 146 | "Bad payload, " ++ message 147 | 148 | 149 | getUser : String -> Cmd Msg 150 | getUser username = 151 | getUrl ("https://api.github.com/users/" ++ username) userDecoder 152 | |> Task.andThen 153 | (\user -> 154 | (getUrl user.reposUrl languagesDecoder 155 | |> Task.onError (\msg -> Task.succeed [ errorToString msg ]) 156 | ) 157 | |> Task.andThen 158 | (\languages -> 159 | Task.succeed { user | languages = languages } 160 | ) 161 | ) 162 | |> Task.attempt (Result.toMaybe >> SetUser) 163 | 164 | 165 | subscriptions : Model -> Sub Msg 166 | subscriptions model = 167 | Time.every 100 Tick 168 | 169 | 170 | main : Program () Model Msg 171 | main = 172 | Browser.element 173 | { init = init 174 | , view = view 175 | , update = update 176 | , subscriptions = subscriptions 177 | } 178 | -------------------------------------------------------------------------------- /src/challenge5.elm: -------------------------------------------------------------------------------- 1 | port module Challenge5 exposing (main) 2 | 3 | import Browser 4 | import Browser.Events exposing (onKeyPress) 5 | import Html exposing (Html, button, div, text) 6 | import Html.Attributes exposing (class, style) 7 | import Html.Events exposing (onClick) 8 | import Json.Decode as Decode 9 | import List.Extra as List 10 | import Random 11 | import Time 12 | 13 | 14 | type alias Box = 15 | { x : Int 16 | , y : Int 17 | } 18 | 19 | 20 | type Direction 21 | = Up 22 | | Down 23 | | Left 24 | | Right 25 | 26 | 27 | type alias Model = 28 | { direction : Direction 29 | , head : Box 30 | , tail : List Box 31 | , isGameover : Bool 32 | , food : Box 33 | , score : Int 34 | , highScore : Int 35 | } 36 | 37 | 38 | initialModel : Model 39 | initialModel = 40 | { direction = Right 41 | , head = { x = 0, y = 9 } 42 | , tail = [] 43 | , isGameover = False 44 | , food = { x = 9, y = 9 } 45 | , score = 0 46 | , highScore = 0 47 | } 48 | 49 | 50 | init : Decode.Value -> ( Model, Cmd Msg ) 51 | init value = 52 | let 53 | highScore = 54 | Decode.decodeValue Decode.string value 55 | |> Result.withDefault "0" 56 | |> String.toInt 57 | |> Maybe.withDefault 0 58 | in 59 | ( { initialModel | highScore = highScore }, Cmd.none ) 60 | 61 | 62 | type Msg 63 | = NoOp 64 | | Tick 65 | | SetDirection Direction 66 | | SetFood Box 67 | | Restart 68 | 69 | 70 | areOppositeDirections : Direction -> Direction -> Bool 71 | areOppositeDirections direction direction_ = 72 | case direction of 73 | Up -> 74 | direction_ == Down 75 | 76 | Down -> 77 | direction_ == Up 78 | 79 | Left -> 80 | direction_ == Right 81 | 82 | Right -> 83 | direction_ == Left 84 | 85 | 86 | isOutOfBounds : Box -> Bool 87 | isOutOfBounds { x, y } = 88 | x < 0 || x >= 20 || y < 0 || y >= 20 89 | 90 | 91 | update : Msg -> Model -> ( Model, Cmd Msg ) 92 | update msg ({ head, tail, direction, isGameover, food, score, highScore } as model) = 93 | case msg of 94 | NoOp -> 95 | ( model, Cmd.none ) 96 | 97 | Tick -> 98 | if isGameover then 99 | ( model, Cmd.none ) 100 | 101 | else 102 | let 103 | newHead = 104 | case direction of 105 | Up -> 106 | { head | y = head.y - 1 } 107 | 108 | Down -> 109 | { head | y = head.y + 1 } 110 | 111 | Left -> 112 | { head | x = head.x - 1 } 113 | 114 | Right -> 115 | { head | x = head.x + 1 } 116 | 117 | isEatingFood = 118 | newHead.x == food.x && newHead.y == food.y 119 | 120 | newTail = 121 | if isEatingFood then 122 | head :: tail 123 | 124 | else 125 | case List.init tail of 126 | Nothing -> 127 | [] 128 | 129 | Just end -> 130 | head :: end 131 | 132 | newScore = 133 | if isEatingFood then 134 | score + 1 135 | 136 | else 137 | score 138 | 139 | newHighScore = 140 | max highScore newScore 141 | in 142 | if List.any ((==) newHead) newTail || isOutOfBounds newHead then 143 | ( { model | isGameover = True }, Cmd.none ) 144 | 145 | else 146 | ( { model 147 | | head = newHead 148 | , tail = newTail 149 | , score = newScore 150 | , highScore = newHighScore 151 | } 152 | , if isEatingFood then 153 | Cmd.batch [ generateFood, saveHighScore newHighScore ] 154 | 155 | else 156 | Cmd.none 157 | ) 158 | 159 | SetDirection newDirection -> 160 | if areOppositeDirections direction newDirection then 161 | ( model, Cmd.none ) 162 | 163 | else 164 | ( { model | direction = newDirection }, Cmd.none ) 165 | 166 | SetFood newFood -> 167 | ( { model | food = newFood }, Cmd.none ) 168 | 169 | Restart -> 170 | ( { initialModel | highScore = highScore }, Cmd.none ) 171 | 172 | 173 | view : Model -> Html Msg 174 | view ({ head, tail, isGameover, food, score, highScore } as model) = 175 | div [] 176 | [ div [ class "screen" ] 177 | [ div 178 | [ class "box" 179 | , style "background-color" "green" 180 | , style "left" (String.fromInt (food.x * 20) ++ "px") 181 | , style "top" (String.fromInt (food.y * 20) ++ "px") 182 | ] 183 | [] 184 | , div [] 185 | (List.map 186 | (\box -> 187 | div 188 | [ class "box" 189 | , style "left" (String.fromInt (box.x * 20) ++ "px") 190 | , style "top" (String.fromInt (box.y * 20) ++ "px") 191 | ] 192 | [] 193 | ) 194 | (head :: tail) 195 | ) 196 | ] 197 | , div [ class "message" ] [ text ("High Score: " ++ String.fromInt highScore) ] 198 | , div [ class "message" ] [ text ("Score: " ++ String.fromInt score) ] 199 | , if isGameover then 200 | div [] 201 | [ div [ class "message" ] [ text "Game Over" ] 202 | , button [ class "restart-button", onClick Restart ] 203 | [ text "Restart" ] 204 | ] 205 | 206 | else 207 | text "" 208 | ] 209 | 210 | 211 | generateFood : Cmd Msg 212 | generateFood = 213 | Random.generate SetFood 214 | (Random.map2 215 | (\x y -> { x = x, y = y }) 216 | (Random.int 0 19) 217 | (Random.int 0 19) 218 | ) 219 | 220 | 221 | keyPressDecoder : Decode.Decoder Msg 222 | keyPressDecoder = 223 | Decode.field "key" Decode.string 224 | |> Decode.map 225 | (\key -> 226 | case String.toUpper key of 227 | "W" -> 228 | SetDirection Up 229 | 230 | "S" -> 231 | SetDirection Down 232 | 233 | "A" -> 234 | SetDirection Left 235 | 236 | "D" -> 237 | SetDirection Right 238 | 239 | _ -> 240 | NoOp 241 | ) 242 | 243 | 244 | subscriptions : Model -> Sub Msg 245 | subscriptions model = 246 | Sub.batch 247 | [ Time.every 200 (\_ -> Tick) 248 | , onKeyPress keyPressDecoder 249 | ] 250 | 251 | 252 | port saveHighScore : Int -> Cmd msg 253 | 254 | 255 | main : Program Decode.Value Model Msg 256 | main = 257 | Browser.element 258 | { init = init 259 | , view = view 260 | , update = update 261 | , subscriptions = subscriptions 262 | } 263 | --------------------------------------------------------------------------------