Week 46 of 2020

Development log of Word Snake

4 items
  1. Show the current score after the game is finished
  2. Merge branch 'game-score' into 'master'
  3. Add 4 sentences
  4. Update static/levels/dutch-words.snake

Show the current score after the game is finished

On by Tad Lispy

index e54140c..11f7b9f 100644
--- a/src/Game.elm
+++ b/src/Game.elm
@@ -1,4 +1,15 @@
-port module Game exposing (Area, Model, Msg, init, snakeEscaped, subscriptions, update, view)
+port module Game exposing
+    ( Area
+    , Model
+    , Msg
+    , Outcome(..)
+    , init
+    , outcome
+    , snakeEscaped
+    , subscriptions
+    , update
+    , view
+    )
=
=import Dict exposing (Dict)
=import Html exposing (Html)
@@ -737,6 +748,24 @@ updateLetters wanted snake letters =
=-- game state predicates
=
=
+type Outcome
+    = Won
+    | Lost
+    | InProgress
+
+
+outcome : Model -> Outcome
+outcome model =
+    if snakeEscaped model then
+        Won
+
+    else if model.snake.alive then
+        InProgress
+
+    else
+        Lost
+
+
=passwordCollected : Model -> Bool
=passwordCollected model =
=    String.toUpper model.word == String.toUpper model.level.password
index 2a894cc..753d685 100644
--- a/src/Level.elm
+++ b/src/Level.elm
@@ -1,4 +1,4 @@
-module Level exposing (Charset, Level)
+module Level exposing (Charset, Level, serialize)
=
=
=type alias Level =
@@ -11,3 +11,19 @@ type alias Level =
=
=type alias Charset =
=    List Char
+
+
+serialize : Level -> String
+serialize level =
+    [ "< "
+    , level.charset
+        |> List.map String.fromChar
+        |> String.join " "
+    , " > "
+    , level.intro
+    , "{ "
+    , level.password
+    , " }"
+    , level.outro
+    ]
+        |> String.join ""
index d9c1dc5..44abf40 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -1,6 +1,7 @@
=port module Main exposing (Model, Msg, init, main, subscriptions, update, view)
=
=import Browser
+import Dict exposing (Dict)
=import Game
=import Html exposing (Html)
=import Html.Attributes
@@ -29,9 +30,14 @@ main =
=type alias Model =
=    { game : Maybe Game.Model
=    , levels : WebData (List Level)
+    , outcomes : Outcomes
=    }
=
=
+type alias Outcomes =
+    Dict String Game.Outcome
+
+
=
=-- INIT
=
@@ -45,6 +51,7 @@ init : Flags -> ( Model, Cmd Msg )
=init flags =
=    ( { game = Nothing
=      , levels = RemoteData.Loading
+      , outcomes = Dict.empty
=      }
=    , fetchLevels GotLevelsResponse flags.levels_url
=    )
@@ -84,7 +91,7 @@ view model =
=
=        Just game ->
=            [ gameView game
-            , popupView game
+            , popupView model.outcomes game
=            ]
=                |> Html.div
=                    [ Html.Attributes.style "height" "100%"
@@ -156,8 +163,8 @@ gameView game =
=        |> Html.map GotGameMsg
=
=
-popupView : Game.Model -> Html Msg
-popupView game =
+popupView : Outcomes -> Game.Model -> Html Msg
+popupView outcomes game =
=    let
=        popup : Bool -> List (Html Msg) -> Html Msg
=        popup show elements =
@@ -203,38 +210,68 @@ popupView game =
=                    , Html.Attributes.style "transition" "opacity 500ms 1000ms"
=                    ]
=    in
-    if Game.snakeEscaped game then
-        popup True
-            [ Html.h2 [] [ Html.text "🙏" ]
-            , Html.p [] [ Html.text "Bravo! The snake is safe." ]
-            , Html.button
-                [ Html.Events.onClick PlayAgainButtonClicked
-                , Html.Attributes.autofocus True
-                , Html.Attributes.style "height" "8rem"
-                , Html.Attributes.style "width" "8rem"
-                , Html.Attributes.style "font-size" "1.2rem"
-                , Html.Attributes.style "font-weight" "bold"
+    case Game.outcome game of
+        Game.Won ->
+            popup True
+                [ Html.h2 [] [ Html.text "🙏" ]
+                , Html.p [] [ Html.text "Bravo! The snake is safe." ]
+                , socreView outcomes
+                , Html.button
+                    [ Html.Events.onClick PlayAgainButtonClicked
+                    , Html.Attributes.autofocus True
+                    , Html.Attributes.style "height" "8rem"
+                    , Html.Attributes.style "width" "8rem"
+                    , Html.Attributes.style "font-size" "1.2rem"
+                    , Html.Attributes.style "font-weight" "bold"
+                    ]
+                    [ Html.text "Play again" ]
=                ]
-                [ Html.text "Play again" ]
-            ]
=
-    else if not game.snake.alive then
-        popup True
-            [ Html.h2 [] [ Html.text "☠️" ]
-            , Html.p [] [ Html.text "Oh, no! Your snake is dead." ]
-            , Html.button
-                [ Html.Events.onClick PlayAgainButtonClicked
-                , Html.Attributes.autofocus True
-                , Html.Attributes.style "height" "8rem"
-                , Html.Attributes.style "width" "8rem"
-                , Html.Attributes.style "font-size" "1.2rem"
-                , Html.Attributes.style "font-weight" "bold"
+        Game.Lost ->
+            popup True
+                [ Html.h2 [] [ Html.text "☠️" ]
+                , Html.p [] [ Html.text "Oh, no! Your snake is dead." ]
+                , socreView outcomes
+                , Html.button
+                    [ Html.Events.onClick PlayAgainButtonClicked
+                    , Html.Attributes.autofocus True
+                    , Html.Attributes.style "height" "8rem"
+                    , Html.Attributes.style "width" "8rem"
+                    , Html.Attributes.style "font-size" "1.2rem"
+                    , Html.Attributes.style "font-weight" "bold"
+                    ]
+                    [ Html.text "Play again" ]
=                ]
-                [ Html.text "Play again" ]
-            ]
=
-    else
-        popup False []
+        Game.InProgress ->
+            popup False []
+
+
+socreView : Outcomes -> Html msg
+socreView outcomes =
+    let
+        { killed, saved } =
+            score outcomes
+    in
+    Html.div
+        [ Html.Attributes.style "display" "flex"
+        , Html.Attributes.style "justify-content" "space-around"
+        , Html.Attributes.style "width" "100%"
+        , Html.Attributes.style "padding" "1em 0"
+        ]
+        [ [ "🐍 "
+          , saved |> String.fromInt
+          ]
+            |> List.map Html.text
+            |> Html.div
+                []
+        , [ "☠️ "
+          , killed |> String.fromInt
+          ]
+            |> List.map Html.text
+            |> Html.div
+                []
+        ]
=
=
=
@@ -280,19 +317,24 @@ update msg model =
=
=                        Just levels ->
=                            let
+                                outcome =
+                                    Game.outcome game
+
=                                trackGoal =
-                                    if won then
-                                        -- TODO: Extract Fathom analytics to own module. Have a union type for goal ids.
-                                        goalTracking ( "MNUMD96E", 0 )
+                                    case outcome of
+                                        Game.Won ->
+                                            -- TODO: Extract Fathom analytics to own module. Have a union type for goal ids.
+                                            goalTracking ( "MNUMD96E", 0 )
=
-                                    else
-                                        goalTracking ( "QHPNWQCC", 0 )
+                                        Game.Lost ->
+                                            goalTracking ( "QHPNWQCC", 0 )
=
-                                won =
-                                    Game.snakeEscaped game
+                                        Game.InProgress ->
+                                            -- TODO: Report an error
+                                            Cmd.none
=
=                                rescheduledLevels =
-                                    scheduleLevel 5 won levels
+                                    scheduleLevel 5 outcome levels
=                            in
=                            ( model
=                            , [ generateRandomGameFlags GotRandomGameFlags rescheduledLevels
@@ -313,6 +355,11 @@ update msg model =
=                    in
=                    ( { model
=                        | game = Just updatedGame
+                        , outcomes =
+                            Dict.insert
+                                (Level.serialize game.level)
+                                (Game.outcome game)
+                                model.outcomes
=                      }
=                    , Cmd.map GotGameMsg gameCmd
=                    )
@@ -355,14 +402,48 @@ subscriptions model =
=-- HELPERS
=
=
-scheduleLevel : Int -> Bool -> List Level -> List Level
-scheduleLevel push won levels =
+type alias Score =
+    { saved : Int
+    , killed : Int
+    , pending : Int
+    }
+
+
+score : Outcomes -> Score
+score outcomes =
+    let
+        iterator : String -> Game.Outcome -> Score -> Score
+        iterator _ outcome memo =
+            case outcome of
+                Game.Won ->
+                    { memo
+                        | saved = memo.saved + 1
+                        , pending = memo.pending - 1
+                    }
+
+                Game.Lost ->
+                    { memo
+                        | killed = memo.killed + 1
+                        , pending = memo.pending - 1
+                    }
+
+                Game.InProgress ->
+                    memo
+    in
+    Dict.foldl
+        iterator
+        (Score 0 0 (Dict.size outcomes))
+        outcomes
+
+
+scheduleLevel : Int -> Game.Outcome -> List Level -> List Level
+scheduleLevel push outcome levels =
=    case levels of
=        [] ->
=            levels
=
=        head :: rest ->
-            if won then
+            if outcome == Game.Won then
=                -- Move level to the end of the queue
=                rest
=                    |> List.reverse

Merge branch 'game-score' into 'master'

On by Tad Lispy

Show the current score after the game is finished

See merge request hornbook/word-snake!9

Add 4 sentences

On by Fana Mehari Hagos

index bff8b8d..ea0332d 100644
--- a/static/levels/dutch-words.snake
+++ b/static/levels/dutch-words.snake
@@ -15,3 +15,7 @@ Het metrostel hangt zo'n tien meter boven het { WATER } op het kunstwerk.
=Morgenochtend wordt het met kranen van { HET } kunstwerk gehaald.
=De berging { BEGINT } niet voor 06.30 uur.
=Dat heeft te { MAKEN } met de wind.
+Onderzoekers bouwden in de dierentuin verschillende tunnels { voor } de apen.
+Een reden hiervoor kan zijn dat de verkeersgeluiden een { beetje } lijken op d geluiden die de apen zelf maken.
+Om te communiceren met elkaar, maken ze hoge sis-, piep- en kwaak-{ geluiden }.
+De Duitse herder Major van Joe Biden is waarschijnlijk de { eerste } asielhond ooit die in het Witte Huis gaat wonen.

Update static/levels/dutch-words.snake

On by Fana Mehari

index ea0332d..23d8936 100644
--- a/static/levels/dutch-words.snake
+++ b/static/levels/dutch-words.snake
@@ -15,7 +15,7 @@ Het metrostel hangt zo'n tien meter boven het { WATER } op het kunstwerk.
=Morgenochtend wordt het met kranen van { HET } kunstwerk gehaald.
=De berging { BEGINT } niet voor 06.30 uur.
=Dat heeft te { MAKEN } met de wind.
-Onderzoekers bouwden in de dierentuin verschillende tunnels { voor } de apen.
-Een reden hiervoor kan zijn dat de verkeersgeluiden een { beetje } lijken op d geluiden die de apen zelf maken.
-Om te communiceren met elkaar, maken ze hoge sis-, piep- en kwaak-{ geluiden }.
-De Duitse herder Major van Joe Biden is waarschijnlijk de { eerste } asielhond ooit die in het Witte Huis gaat wonen.
+Onderzoekers bouwden in de dierentuin verschillende tunnels { VOOR } de apen.
+Een reden hiervoor kan zijn dat de verkeersgeluiden een { BEETJE } lijken op d geluiden die de apen zelf maken.
+Om te communiceren met elkaar, maken ze hoge sis-, piep- en kwaak-{ GELUIDEN }.
+De Duitse herder Major van Joe Biden is waarschijnlijk de { EERSTE } asielhond ooit die in het Witte Huis gaat wonen.