Week 17 of 2021
Development log of Word Snake
6 items
Fix layout issues on Apple Safari
On by
It proved quite difficult. In the end I decided to change the user interface a bit, but I think the change is good regardless of Safari bugs.
Now footer with score is static, i.e. doesn't slide in and out like it used to. The only element that it was revealing was the big "Play again" button. But since there is a full screen overlay (curtain) that fades in when the game is over, it's better to place this button there. Actually I'm considering removing the footer altogether and only show score on the game-over curtain.
Since there is no sliding footer, there is also no need for shifting the game area.
The content on the curtain is now centered using the flexbox justify-self property. This fixes some layout problems on Safari, but also is just better.
There was also a zoom-in and -out animation for game over content. It was excessive and I removed it. The fade-in and -out of the curtain is enough.
index 427d1f6..630b8cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2033,9 +2033,9 @@
= }
= },
= "caniuse-lite": {
- "version": "1.0.30001150",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz",
- "integrity": "sha512-kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ==",
+ "version": "1.0.30001208",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz",
+ "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==",
= "dev": true
= },
= "caseless": {index 58d721c..860d82b 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -93,81 +93,24 @@ view : Model -> Html Msg
=view model =
= case model.game of
= Nothing ->
- introView
+ viewIntro
=
= Just game ->
- [ passwordView game
- , gameView game
- , popupView model.outcomes game
- , curtainView model.countdown game
+ [ viewHeader game
+ , viewGame game
+ , viewFooter model.outcomes
+ , viewCurtain model.countdown game
= ]
= |> Html.div
= [ Html.Attributes.style "height" "100%"
= , Html.Attributes.style "width" "100%"
- , Html.Attributes.style "aling-self" "center"
= , Html.Attributes.style "display" "flex"
= , Html.Attributes.style "flex-direction" "column"
= ]
=
=
-introView : Html Msg
-introView =
- Html.div
- [ Html.Attributes.style "text-align" "center"
- , Html.Attributes.style "max-width" "640px"
- , Html.Attributes.style "padding" "2rem"
- , Html.Attributes.style "line-height" "150%"
- ]
- [ Html.div []
- [ Html.h1 [] [ Html.text "Save the snake!" ]
- , Html.p [] [ Html.text "Guess the password. Use W A S D (or swipe 👆) to move around and collect the letters to fill the missing word. Once the password is collected you will be able to escape the fire trap." ]
- , Html.p [ Html.Attributes.style "font-size" "2rem" ] [ Html.text "🐍" ]
- , Html.p [] [ Html.text "The most important: avoid fire and poop." ]
- , Html.button
- [ Html.Events.onClick StartButtonClicked
- , 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 "Start" ]
- ]
- , Html.footer [ Html.Attributes.style "font-size" "0.8rem" ]
- [ Html.p []
- [ Html.text "Made by "
- , Html.a
- [ Html.Attributes.href "https://tad-lispy.com/" ]
- [ Html.text "Tad Lispy" ]
- , Html.text ". "
- ]
- , Html.p []
- [ Html.text "This game is a "
- , Html.strong []
- [ Html.text "free software" ]
- , Html.text ". You may copy, modify, redistribute it etc. under the terms of "
- , Html.a
- [ Html.Attributes.href "https://www.gnu.org/licenses/gpl-3.0.html"
- , Html.Attributes.target "_blank"
- ]
- [ Html.text "the GNU General Public License v. 3.0" ]
- , Html.text " or later. "
- , Html.a [ Html.Attributes.href "https://gitlab.com/hornbook/word-snake/" ]
- [ Html.text "See the source code" ]
- , Html.text ". I respect your "
- , Html.a
- [ Html.Attributes.href "privacy.html"
- , Html.Attributes.target "_blank"
- ]
- [ Html.text "privacy" ]
- , Html.text ". Good luck, have fun!"
- ]
- ]
- ]
-
-
-passwordView : Game.Model -> Html Msg
-passwordView game =
+viewHeader : Game.Model -> Html Msg
+viewHeader game =
= game
= |> Game.passwordView
= |> Html.map GotGameMsg
@@ -179,90 +122,45 @@ passwordView game =
= ]
=
=
-gameView : Game.Model -> Html Msg
-gameView game =
+viewGame : Game.Model -> Html Msg
+viewGame game =
= game
= |> Game.areaView
= |> Html.map GotGameMsg
- |> List.singleton
- |> Html.div
- [ Html.Attributes.style "height" "0"
- , Html.Attributes.style "flex" "auto"
- , Html.Attributes.style "transition" "transform 1500ms 500ms"
- , Html.Attributes.style "transform" <|
- if Game.outcome game == Game.InProgress then
- "translateY(0)"
-
- else
- "translateY(-9rem)"
- ]
=
=
-popupView : Outcomes -> Game.Model -> Html Msg
-popupView outcomes game =
+viewFooter : Outcomes -> Html msg
+viewFooter outcomes =
= let
- popup : Bool -> List (Html Msg) -> Html Msg
- popup show elements =
- elements
- |> Html.div
- [ Html.Attributes.style "text-align" "center"
- , Html.Attributes.style "padding" "0.2em 0"
- , Html.Attributes.style "position" "absolute"
- , Html.Attributes.style "bottom" "0"
- , Html.Attributes.style "width" "100%"
- , Html.Attributes.style "background" "hsl(0, 0%, 86%)"
- , Html.Attributes.style "box-shadow" "0px 0px 6px 2px hsla(0, 0%, 0%, 20%)"
- , Html.Attributes.style "transition" "height 1500ms 500ms"
- , Html.Attributes.style "height"
- (if show then
- "18rem"
-
- else
- "3rem"
- )
- ]
- |> List.singleton
- |> Html.div
- [ Html.Attributes.style "height" "3rem"
- , Html.Attributes.style "position" "relative"
- , Html.Attributes.style "z-index" "1"
- ]
+ { killed, saved } =
+ score outcomes
= in
- case Game.outcome game of
- Game.Won ->
- popup True
- [ 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" ]
- ]
-
- Game.Lost ->
- popup True
- [ 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" ]
- ]
-
- Game.InProgress ->
- popup False [ socreView outcomes ]
+ 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"
+ , Html.Attributes.style "background" "hsl(0, 0%, 86%)"
+ , Html.Attributes.style "box-shadow" "0px 0px 6px 2px hsla(0, 0%, 0%, 20%)"
+ , Html.Attributes.style "z-index" "1"
+ ]
+ [ [ "🐍 "
+ , saved |> String.fromInt
+ ]
+ |> List.map Html.text
+ |> Html.div
+ []
+ , [ "☠️ "
+ , killed |> String.fromInt
+ ]
+ |> List.map Html.text
+ |> Html.div
+ []
+ ]
=
=
-curtainView : Int -> Game.Model -> Html Msg
-curtainView countdown game =
+viewCurtain : Int -> Game.Model -> Html Msg
+viewCurtain countdown game =
= let
= overlay elements =
= elements
@@ -273,7 +171,6 @@ curtainView countdown game =
= , Html.Attributes.style "left" "0"
= , Html.Attributes.style "right" "0"
= , Html.Attributes.style "display" "flex"
- , Html.Attributes.style "padding-top" "20%"
= , Html.Attributes.style "justify-content" "center"
= , Html.Attributes.style "background" "hsla(0,0%,0%,80%)"
= , Html.Attributes.style "animation-name" "overlay"
@@ -297,8 +194,10 @@ curtainView countdown game =
= [ Html.Attributes.style "font-size" "10rem"
= , Html.Attributes.style "font-weight" "bold"
= , Html.Attributes.style "color" "white"
+ , Html.Attributes.style "align-self" "center"
= , Html.Attributes.style "animation-name" "countdown"
= , Html.Attributes.style "animation-duration" "1s"
+ , Html.Attributes.style "animation-delay" "0.1s"
= , Html.Attributes.style "animation-iteration-count" "infinite"
= ]
= |> List.singleton
@@ -310,17 +209,14 @@ curtainView countdown game =
= ]
= [ Html.text "☠️" ]
= , Html.p [] [ Html.text "Oh, no! Your snake is dead." ]
+ , playAgainButton
= ]
= |> Html.div
= [ Html.Attributes.style "font-size" "2rem"
= , Html.Attributes.style "font-weight" "bold"
= , Html.Attributes.style "color" "white"
- , Html.Attributes.style "animation-name" "outcome"
- , Html.Attributes.style "animation-duration" "1s"
- , Html.Attributes.style "animation-delay" "1s"
- , Html.Attributes.style "animation-iteration-count" "1"
- , Html.Attributes.style "animation-fill-mode" "both"
= , Html.Attributes.style "text-align" "center"
+ , Html.Attributes.style "align-self" "center"
= ]
= |> List.singleton
= |> overlay
@@ -331,46 +227,86 @@ curtainView countdown game =
= ]
= [ Html.text "🙏" ]
= , Html.p [] [ Html.text "Bravo! The snake is safe." ]
+ , playAgainButton
= ]
= |> Html.div
= [ Html.Attributes.style "font-size" "2rem"
= , Html.Attributes.style "font-weight" "bold"
= , Html.Attributes.style "color" "white"
- , Html.Attributes.style "animation-name" "outcome"
- , Html.Attributes.style "animation-duration" "1s"
- , Html.Attributes.style "animation-delay" "1s"
- , Html.Attributes.style "animation-iteration-count" "1"
- , Html.Attributes.style "animation-fill-mode" "both"
= , Html.Attributes.style "text-align" "center"
+ , Html.Attributes.style "align-self" "center"
= ]
= |> List.singleton
= |> overlay
=
=
-socreView : Outcomes -> Html msg
-socreView outcomes =
- let
- { killed, saved } =
- score outcomes
- in
+playAgainButton : Html Msg
+playAgainButton =
+ 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.Attributes.autofocus True
+ ]
+ [ Html.text "Play again" ]
+
+
+viewIntro : Html Msg
+viewIntro =
= 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"
+ [ Html.Attributes.style "text-align" "center"
+ , Html.Attributes.style "max-width" "640px"
+ , Html.Attributes.style "padding" "2rem"
+ , Html.Attributes.style "line-height" "150%"
= ]
- [ [ "🐍 "
- , saved |> String.fromInt
- ]
- |> List.map Html.text
- |> Html.div
- []
- , [ "☠️ "
- , killed |> String.fromInt
- ]
- |> List.map Html.text
- |> Html.div
- []
+ [ Html.div []
+ [ Html.h1 [] [ Html.text "Save the snake!" ]
+ , Html.p [] [ Html.text "Guess the password. Use W A S D (or swipe 👆) to move around and collect the letters to fill the missing word. Once the password is collected you will be able to escape the fire trap." ]
+ , Html.p [ Html.Attributes.style "font-size" "2rem" ] [ Html.text "🐍" ]
+ , Html.p [] [ Html.text "The most important: avoid fire and poop." ]
+ , Html.button
+ [ Html.Events.onClick StartButtonClicked
+ , 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 "Start" ]
+ ]
+ , Html.footer [ Html.Attributes.style "font-size" "0.8rem" ]
+ [ Html.p []
+ [ Html.text "Made by "
+ , Html.a
+ [ Html.Attributes.href "https://tad-lispy.com/" ]
+ [ Html.text "Tad Lispy" ]
+ , Html.text ". "
+ ]
+ , Html.p []
+ [ Html.text "This game is a "
+ , Html.strong []
+ [ Html.text "free software" ]
+ , Html.text ". You may copy, modify, redistribute it etc. under the terms of "
+ , Html.a
+ [ Html.Attributes.href "https://www.gnu.org/licenses/gpl-3.0.html"
+ , Html.Attributes.target "_blank"
+ ]
+ [ Html.text "the GNU General Public License v. 3.0" ]
+ , Html.text " or later. "
+ , Html.a [ Html.Attributes.href "https://gitlab.com/hornbook/word-snake/" ]
+ [ Html.text "See the source code" ]
+ , Html.text ". I respect your "
+ , Html.a
+ [ Html.Attributes.href "privacy.html"
+ , Html.Attributes.target "_blank"
+ ]
+ [ Html.text "privacy" ]
+ , Html.text ". Good luck, have fun!"
+ ]
+ ]
= ]
=
=index 76ad605..63b01db 100644
--- a/src/index.html
+++ b/src/index.html
@@ -31,18 +31,16 @@
=
=
= <style type="text/css" media="screen">
- html, body {
+ html, body, [data-elm-hot] {
= height: 100%;
+ width: 100%;
= margin: 0;
= padding: 0;
= font-family: sans-serif;
- background: hsl(0, 0%, 86%);
- }
-
- body, [data-elm-hot] {
- background: hsl(0, 0%, 86%);
- width: 100%;
= color: hsl(0, 0%, 26%);
+ background: hsl(0, 0%, 76%);
+ overflow: hidden;
+
= display: flex;
= flex-direction: column;
= align-items: center;1.1.0
On by
index 630b8cc..fb2428e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
={
= "name": "word-snake",
- "version": "1.0.5",
+ "version": "1.1.0",
= "lockfileVersion": 1,
= "requires": true,
= "dependencies": {index 6033381..b410692 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
={
= "name": "word-snake",
- "version": "1.0.5",
+ "version": "1.1.0",
= "description": "A word snake challenge for Hornbook - prototype",
= "main": "src/index.coffee",
= "scripts": {Make player's progress persistent
On by
Using jakearchibald/idb-keyval.
Also move all ports to the new Ports module. It's cleaner that way.
index 27987cb..52a669e 100644
--- a/node-packages.nix
+++ b/node-packages.nix
@@ -1687,13 +1687,13 @@ let
= sha512 = "bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==";
= };
= };
- "caniuse-lite-1.0.30001150" = {
+ "caniuse-lite-1.0.30001208" = {
= name = "caniuse-lite";
= packageName = "caniuse-lite";
- version = "1.0.30001150";
+ version = "1.0.30001208";
= src = fetchurl {
- url = "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001150.tgz";
- sha512 = "kiNKvihW0m36UhAFnl7bOAv0i1K1f6wpfVtTF5O5O82XzgtBnb05V0XeV3oZ968vfg2sRNChsHw8ASH2hDfoYQ==";
+ url = "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz";
+ sha512 = "OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==";
= };
= };
= "caseless-0.12.0" = {
@@ -3478,6 +3478,15 @@ let
= sha1 = "06ea6f83679a7749e386cfe1fe812ae5db223ded";
= };
= };
+ "idb-keyval-5.0.5" = {
+ name = "idb-keyval";
+ packageName = "idb-keyval";
+ version = "5.0.5";
+ src = fetchurl {
+ url = "https://registry.npmjs.org/idb-keyval/-/idb-keyval-5.0.5.tgz";
+ sha512 = "cqi65rrjhgPExI9vmSU7VcYEbHCUfIBY+9YUWxyr0PyGizptFgGFnvZQ0w+tqOXk1lUcGCZGVLfabf7QnR2S0g==";
+ };
+ };
= "ieee754-1.1.13" = {
= name = "ieee754";
= packageName = "ieee754";
@@ -7145,7 +7154,7 @@ let
= args = {
= name = "word-snake";
= packageName = "word-snake";
- version = "1.0.5";
+ version = "1.1.0";
= src = ./.;
= dependencies = [
= sources."@babel/code-frame-7.10.4"
@@ -7408,7 +7417,7 @@ let
= sources."caller-path-2.0.0"
= sources."callsites-2.0.0"
= sources."caniuse-api-3.0.0"
- sources."caniuse-lite-1.0.30001150"
+ sources."caniuse-lite-1.0.30001208"
= sources."caseless-0.12.0"
= sources."chalk-2.4.2"
= sources."chardet-0.7.0"
@@ -7707,6 +7716,7 @@ let
= sources."https-browserify-1.0.0"
= sources."iconv-lite-0.4.24"
= sources."icss-replace-symbols-1.1.0"
+ sources."idb-keyval-5.0.5"
= sources."ieee754-1.1.13"
= sources."import-fresh-2.0.0"
= sources."indexes-of-1.0.1"index fb2428e..3c02512 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4026,6 +4026,11 @@
= "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=",
= "dev": true
= },
+ "idb-keyval": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-5.0.5.tgz",
+ "integrity": "sha512-cqi65rrjhgPExI9vmSU7VcYEbHCUfIBY+9YUWxyr0PyGizptFgGFnvZQ0w+tqOXk1lUcGCZGVLfabf7QnR2S0g=="
+ },
= "ieee754": {
= "version": "1.1.13",
= "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",index b410692..e9e903f 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
= },
= "dependencies": {
= "@capacitor/android": "^2.4.2",
- "@capacitor/core": "^2.4.2"
+ "@capacitor/core": "^2.4.2",
+ "idb-keyval": "^5.0.5"
= }
=}index 860d82b..76f5a65 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -1,4 +1,4 @@
-port module Main exposing (Model, Msg, init, main, subscriptions, update, view)
+module Main exposing (Model, Msg, init, main, subscriptions, update, view)
=
=import Browser
=import Dict exposing (Dict)
@@ -10,17 +10,12 @@ import Http
=import Level exposing (Level)
=import Parser
=import Parser.Custom as Parser
+import Ports
=import Random
=import RemoteData exposing (WebData)
=import Time
=
=
-port fullscreen : () -> Cmd msg
-
-
-port goalTracking : ( String, Int ) -> Cmd msg
-
-
=main : Program Flags Model Msg
=main =
= Browser.element
@@ -49,6 +44,7 @@ type alias Outcomes =
=
=type alias Flags =
= { levels_url : String
+ , outcomes : List ( String, Bool )
= }
=
=
@@ -56,7 +52,17 @@ init : Flags -> ( Model, Cmd Msg )
=init flags =
= ( { game = Nothing
= , levels = RemoteData.Loading
- , outcomes = Dict.empty
+ , outcomes =
+ flags.outcomes
+ |> Dict.fromList
+ |> Dict.map
+ (\_ won ->
+ if won then
+ Game.Won
+
+ else
+ Game.Lost
+ )
= , countdown = 0
= }
= , fetchLevels GotLevelsResponse flags.levels_url
@@ -327,14 +333,19 @@ update : Msg -> Model -> ( Model, Cmd Msg )
=update msg model =
= case msg of
= GotLevelsResponse response ->
- ( { model | levels = RemoteData.fromResult response }
+ ( { model
+ | levels =
+ response
+ |> RemoteData.fromResult
+ |> RemoteData.map (rescheduleLevels model.outcomes)
+ }
= , Cmd.none
= )
=
= StartButtonClicked ->
= ( model
= , Cmd.batch
- [ fullscreen ()
+ [ Ports.fullscreen ()
= , model.levels
= |> RemoteData.map (generateRandomGameFlags GotRandomGameFlags)
= |> RemoteData.withDefault Cmd.none
@@ -369,10 +380,10 @@ update msg model =
= case outcome of
= Game.Won ->
= -- TODO: Extract Fathom analytics to own module. Have a union type for goal ids.
- goalTracking ( "MNUMD96E", 0 )
+ Ports.goalTracking ( "MNUMD96E", 0 )
=
= Game.Lost ->
- goalTracking ( "QHPNWQCC", 0 )
+ Ports.goalTracking ( "QHPNWQCC", 0 )
=
= Game.InProgress ->
= -- TODO: Report an error
@@ -384,7 +395,7 @@ update msg model =
= ( model
= , [ generateRandomGameFlags GotRandomGameFlags rescheduledLevels
= , trackGoal
- , fullscreen ()
+ , Ports.fullscreen ()
= ]
= |> Cmd.batch
= )
@@ -398,16 +409,22 @@ update msg model =
= let
= ( updatedGame, gameCmd ) =
= Game.update gameMsg game
+
+ outcome =
+ Game.outcome game
= in
= ( { model
= | game = Just updatedGame
= , outcomes =
= Dict.insert
= (Level.serialize game.level)
- (Game.outcome game)
+ outcome
= model.outcomes
= }
- , Cmd.map GotGameMsg gameCmd
+ , [ Cmd.map GotGameMsg gameCmd
+ , Ports.storeOutcome game.level outcome
+ ]
+ |> Cmd.batch
= )
=
= GotRandomGameFlags levels randomness ->
@@ -514,6 +531,25 @@ scheduleLevel push outcome levels =
= ]
=
=
+rescheduleLevels : Outcomes -> List Level -> List Level
+rescheduleLevels outcomes levels =
+ levels
+ |> List.indexedMap
+ (\index level ->
+ case Dict.get (Level.serialize level) outcomes of
+ Just Game.Won ->
+ ( index + List.length levels, level )
+
+ Just Game.Lost ->
+ ( index + 5, level )
+
+ _ ->
+ ( index, level )
+ )
+ |> List.sortBy Tuple.first
+ |> List.map Tuple.second
+
+
=generateRandomGameFlags : (List Level -> Random.Seed -> msg) -> List Level -> Cmd msg
=generateRandomGameFlags tag levels =
= Random.map2 tagnew file mode 100644
index 0000000..6d35f26
--- /dev/null
+++ b/src/Ports.elm
@@ -0,0 +1,32 @@
+port module Ports exposing (fullscreen, goalTracking, storeOutcome)
+
+import Game
+import Level exposing (Level)
+
+
+port fullscreen : () -> Cmd msg
+
+
+port goalTracking : ( String, Int ) -> Cmd msg
+
+
+port outcomes : ( String, Bool ) -> Cmd msg
+
+
+storeOutcome : Level -> Game.Outcome -> Cmd msg
+storeOutcome level outcome =
+ case outcome of
+ Game.InProgress ->
+ Cmd.none
+
+ Game.Won ->
+ outcomes
+ ( Level.serialize level
+ , True
+ )
+
+ Game.Lost ->
+ outcomes
+ ( Level.serialize level
+ , False
+ )index 22d139c..9f3e888 100644
--- a/src/index.coffee
+++ b/src/index.coffee
@@ -1,4 +1,5 @@
=import { Elm } from "./Main.elm"
+import * as outcomes_db from "idb-keyval"
=
=levels =
= location
@@ -15,21 +16,31 @@ levels_url =
= else
= "https://word-snake.software.garden/levels/default.snake"
=
-node = document.getElementById "app"
-flags = { levels_url }
+outcomes_db
+.entries()
+.then (outcomes) =>
+ node = document.getElementById "app"
=
-game = Elm.Main.init { node, flags }
+ flags = {
+ levels_url
+ outcomes
+ }
=
-document.addEventListener "keydown", (event) =>
- if event.key in [ "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" ]
- do event.preventDefault
- game.ports.keydown.send event
+ game = Elm.Main.init { node, flags }
=
-game.ports.fullscreen.subscribe () =>
- document.body.requestFullscreen? {}
+ document.addEventListener "keydown", (event) =>
+ if event.key in [ "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" ]
+ do event.preventDefault
+ game.ports.keydown.send event
=
-game.ports.goalTracking.subscribe ([goal, value]) =>
- if process.env.NODE_ENV is "production"
- fathom.trackGoal goal, value
- else
- console.debug "Tracking goals disabled in development environment."
+ game.ports.fullscreen.subscribe () =>
+ document.body.requestFullscreen? {}
+
+ game.ports.goalTracking.subscribe ([goal, value]) =>
+ if process.env.NODE_ENV is "production"
+ fathom.trackGoal goal, value
+ else
+ console.debug "Tracking goals disabled in development environment."
+
+ game.ports.outcomes.subscribe ([level, won]) =>
+ outcomes_db.set level, wonMerge branch 'persistent-progress' into 'master'
On by
Make player's progress persistent
See merge request software-garden/word-snake!19
Fix grammar in a level
On by
index d2e9706..ff053a9 100644
--- a/static/levels/default.snake
+++ b/static/levels/default.snake
@@ -9,7 +9,7 @@ Pokemon Go is a location-based augmented { REALITY } game developed by company c
=In 1960 Michael Woodruff { PERFORMED } the first successful kidney transplant in the United Kingdom.
=To bypass US Munitions Export Laws, the creator of the PGP { PUBLISHED } all the source code in book form.
=Munich is a { GERMAN } city located on the River Isar.
-The world's oldest still operational space launch facility located in { KAZAKHSTAN }.
+The world's oldest still operational space launch facility is located in { KAZAKHSTAN }.
=Thirty six divided by four is { NINE }.
=Fifty six is { EIGHT } times seven.
=A group of six healthy people typically have { SIX } pairs of ears between them.Order levels by length
On by
Fana and I think that the difficulty of each level is proportional to the length. The more player has to read and process the more difficult it is to guess the answer.
For reference, this is how to sort lines by length:
cat static/levels/default.snake \
| tail --lines +3 \
| awk '{ print length, $0 }' \
| sort --human-numeric-sort \
| cut --delimiter=' ' --fields=2-index ff053a9..ee87bf4 100644
--- a/static/levels/default.snake
+++ b/static/levels/default.snake
@@ -1,35 +1,34 @@
=A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
=
-Tibia is a name of one of the { BONES } in the leg.
=Dr. Dre is an American { RAPPER }.
-Somewhat surprisingly, a person can get { SUNBURNED } on a cloudy day.
-Wales is a country inside the United Kingdom that does not appear on its flag, the Union { JACK }.
-The Artful Dodger is a nickname of Jack Dawkins, a character in "Oliver Twist", a novel by Charles { DICKENS }.
-Pokemon Go is a location-based augmented { REALITY } game developed by company called Niantic.
-In 1960 Michael Woodruff { PERFORMED } the first successful kidney transplant in the United Kingdom.
-To bypass US Munitions Export Laws, the creator of the PGP { PUBLISHED } all the source code in book form.
-Munich is a { GERMAN } city located on the River Isar.
-The world's oldest still operational space launch facility is located in { KAZAKHSTAN }.
-Thirty six divided by four is { NINE }.
=Fifty six is { EIGHT } times seven.
-A group of six healthy people typically have { SIX } pairs of ears between them.
-The first phase of the Marmaray project opened in 2013 with an { UNDERSEA } rail tunnel across the Bosphorus strait.
-In the context of spaceflight, a { SATELLITE } is an object that has been intentionally placed into orbit.
-Mercury rotates in a way that is { UNIQUE } in the Solar System.
-In Virgil's Aeneid, Mercury reminds Aeneas of his mission to found the { CITY } of Rome.
-X-bracing was used in the construction of the 1908 Singer Building, then the { TALLEST } building in the world.
-South America is bordered on the west by the { PACIFIC } Ocean and on the north and east by the Atlantic Ocean.
-{ BOLIVIA } is a landlocked country located in western-central South America.
+Thirty six divided by four is { NINE }.
+The { OPPOSITE } of "literally" is "figuratively".
+Tibia is a name of one of the { BONES } in the leg.
+Munich is a { GERMAN } city located on the River Isar.
+Bali is an island located 8 { DEGREES } south of the equator.
=Babylon was built along both { BANKS } of the Euphrates river.
-The Norte Chico civilization, which flourished between the fourth and second { MILLENNIA } BC is the oldest known civilization in the Americas.
+The number of dwarf planets in the Solar System is { UNKNOWN }.
+Mercury rotates in a way that is { UNIQUE } in the Solar System.
+Somewhat surprisingly, a person can get { SUNBURNED } on a cloudy day.
=The Andes are the longest continental mountain { RANGE } in the world.
+A metaphor can help to identify { HIDDEN } similarities between two ideas.
+{ BOLIVIA } is a landlocked country located in western-central South America.
+A group of six healthy people typically have { SIX } pairs of ears between them.
=Mountain ranges are usually segmented by highlands or mountain passes and { VALLEYS }.
-Doom Mons is the name of a mountain range and its eponymous peak on Titan, the largest { MOON } of Saturn.
-The number of dwarf planets in the Solar System is { UNKNOWN }.
+In Virgil's Aeneid, Mercury reminds Aeneas of his mission to found the { CITY } of Rome.
+The world's oldest still operational space launch facility is located in { KAZAKHSTAN }.
+Pokemon Go is a location-based augmented { REALITY } game developed by company called Niantic.
+Wales is a country inside the United Kingdom that does not appear on its flag, the Union { JACK }.
=If the ellipse is { ROTATED } about its major axis, the result is a prolate - an elongated spheroid
+In 1960 Michael Woodruff { PERFORMED } the first successful kidney transplant in the United Kingdom.
+Doom Mons is the name of a mountain range and its eponymous peak on Titan, the largest { MOON } of Saturn.
+In the context of spaceflight, a { SATELLITE } is an object that has been intentionally placed into orbit.
+To bypass US Munitions Export Laws, the creator of the PGP { PUBLISHED } all the source code in book form.
+South America is bordered on the west by the { PACIFIC } Ocean and on the north and east by the Atlantic Ocean.
+The Artful Dodger is a nickname of Jack Dawkins, a character in "Oliver Twist", a novel by Charles { DICKENS }.
+X-bracing was used in the construction of the 1908 Singer Building, then the { TALLEST } building in the world.
=The Earth is often approximated by an oblate spheroid, known as the reference ellipsoid, instead of a { SPHERE }.
-The { OPPOSITE } of "literally" is "figuratively".
-A metaphor can help to identify { HIDDEN } similarities between two ideas.
+The first phase of the Marmaray project opened in 2013 with an { UNDERSEA } rail tunnel across the Bosphorus strait.
+The Norte Chico civilization, which flourished between the fourth and second { MILLENNIA } BC is the oldest known civilization in the Americas.
=The metropolitan area of Cairo, with a { POPULATION } of over 20 million, is the largest in Africa, the Arab world, and the Middle East, and the 6th-largest in the world.
-Bali is an island located 8 { DEGREES } south of the equator.
-Once a {SNAKE} always a snake.