diff --git a/README.md b/README.md index 316832a..4081f48 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,6 @@ -# elm-webpack-starter +# LADY BOGGLE -A simple Webpack setup for writing [Elm](http://elm-lang.org/) apps: - -* Dev server with live reloading, HMR -* Support for CSS/SCSS (with Autoprefixer), image assets -* Bootstrap 3.3+ (Sass version) -* Bundling and minification for deployment -* Basic app scaffold, using `Html.beginnerProgram` -* A snippet of example code to get you started! +Boggle written in elm by ladies. ### Install: @@ -49,39 +42,3 @@ npm run build * Files are saved into the `/dist` folder * To check it, open `dist/index.html` - -### Changelog - -**Ver 0.8.0** -* Update to Elm 0.18, use `debug=true` on webpack loader (PR by [douglascorrea](https://github.com/moarwick/elm-webpack-starter/pull/33)) -* Add a script for one-step installs -* Update to latest packages - -**Ver 0.7.1** -* Fix favicon issues, per [Issue 30](https://github.com/moarwick/elm-webpack-starter/issues/30) - -**Ver 0.7.0** -* Modify project structure, per [Issue 26](https://github.com/moarwick/elm-webpack-starter/issues/26) -* Include Bootstrap JS, per [Issue 28](https://github.com/moarwick/elm-webpack-starter/issues/28) -* More helpful install steps in README, per [Issue 29](https://github.com/moarwick/elm-webpack-starter/issues/29) -* Update to latest packages - -**Ver 0.6.2** -* Use `copy-webpack-plugin` instead of `cp` to copy files (Windows compatible) - -**Ver 0.6.0** -* `elm-hot-loader` is back (no Elm code changes required!) -* Switch to [bootstrap-sass](https://www.npmjs.com/package/bootstrap-sass) to demo CSS - -**Ver 0.5.0** -* Update to Elm 0.17.0 (and other latest modules) -* Upgrade starter code per [upgrade-docs](https://github.com/elm-lang/elm-platform/blob/master/upgrade-docs/0.17.md) -* Remove `elm-hot-loader` (for now) - -**Ver 0.4.0** -* Add [elm-hot-loader](https://github.com/fluxxu/elm-hot-loader) for HMR support (PR by [fluxxu](https://github.com/fluxxu)) - -**Ver 0.3.0** -* Use `html-webpack-plugin` to generate `index.html` -* Apply hash filenames for bundled JS and CSS (prevents caching) -* Image and favicon assets copied to `dist/` diff --git a/elm-package.json b/elm-package.json index 2fdb466..49cc74a 100644 --- a/elm-package.json +++ b/elm-package.json @@ -9,8 +9,10 @@ "exposed-modules": [], "dependencies": { "elm-lang/core": "5.0.0 <= v < 6.0.0", + "elm-lang/dom": "1.1.1 <= v < 2.0.0", "elm-lang/html": "2.0.0 <= v < 3.0.0", "elm-lang/http": "1.0.0 <= v < 2.0.0", + "elm-lang/keyboard": "1.0.1 <= v < 2.0.0", "evancz/elm-markdown": "3.0.1 <= v < 4.0.0" }, "elm-version": "0.18.0 <= v < 0.19.0" diff --git a/package.json b/package.json index ce77c82..a05fbb3 100644 --- a/package.json +++ b/package.json @@ -34,5 +34,8 @@ "webpack": "^1.13.1", "webpack-dev-server": "^1.14.1", "webpack-merge": "^0.16.0" + }, + "dependencies": { + "glob": "^7.1.1" } } diff --git a/src/elm/BoardRandomizer.elm b/src/elm/BoardRandomizer.elm new file mode 100644 index 0000000..ce208a4 --- /dev/null +++ b/src/elm/BoardRandomizer.elm @@ -0,0 +1,131 @@ +module BoardRandomizer exposing (createRandomBoard) + +import Html +import Random exposing (int, initialSeed, generate, step) + + +createRandomBoard : Int -> Int -> List (List String) +createRandomBoard width seed = + List.map + (\row -> List.map decodeNumberToLetter row) + ((groupInto width + (randomSequence width seed) + ) + ) + + +randomSequence : Int -> Int -> List Int +randomSequence width seed = + let + gen = + Random.list (width * width) (Random.int 0 150) + + s = + initialSeed seed + + ( res, ns ) = + step gen s + in + res + + +groupInto : Int -> List a -> List (List a) +groupInto groups initial = + let + len = + List.length initial + + n = + len // groups + in + List.repeat groups [] + |> List.indexedMap + (\i _ -> + List.take n (List.drop (n * i) initial) + ) + + +decodeNumberToLetter : Int -> String +decodeNumberToLetter num = + if num < 19 then + "e" + else if num < 32 then + "t" + else if num < 44 then + "a" + else if num < 56 then + "r" + else if num < 67 then + "i" + else if num < 78 then + "n" + else if num < 89 then + "o" + else if num < 98 then + "s" + else if num < 104 then + "d" + else if num < 109 then + "c" + else if num < 114 then + "h" + else if num < 119 then + "l" + else if num < 123 then + "f" + else if num < 127 then + "m" + else if num < 131 then + "p" + else if num < 135 then + "u" + else if num < 138 then + "g" + else if num < 141 then + "y" + else if num < 143 then + "w" + else if num < 144 then + "b" + else if num < 145 then + "j" + else if num < 146 then + "k" + else if num < 147 then + "q" + else if num < 148 then + "v" + else if num < 149 then + "x" + else + "z" + + + +--frequencies +-- 19 e +-- 13 t 32 +-- 12 a 44 +-- 12 r 56 +-- 11 i 67 +-- 11 n 78 +-- 11 o 89 +-- 9 s 98 +-- 6 d 104 +-- 5 c 109 +-- 5 h 114 +-- 5 l 119 +-- 4 f 123 +-- 4 m 127 +-- 4 p 131 +-- 4 u 135 +-- 3 g 138 +-- 3 y 141 +-- 2 w 143 +-- 1 b 144 +-- 1 j 145 +-- 1 k 146 +-- 1 q 147 +-- 1 v 148 +-- 1 x 149 +-- 1 z 150 diff --git a/src/elm/Main.elm b/src/elm/Main.elm index 5b655ca..500b5d5 100644 --- a/src/elm/Main.elm +++ b/src/elm/Main.elm @@ -1,59 +1,101 @@ module Main exposing (..) +import Task exposing (Task) import Html exposing (..) import Html.Attributes exposing (placeholder, value, classList, class) -import Html.Events exposing (onClick, onInput) +import Html.Events exposing (onClick, onInput, on) import Dict exposing (Dict) -import Set exposing (Set) +import Dom exposing (focus, Error) +import Http +import Json.Decode as Decode +import Keyboard exposing (presses) +import Char exposing (fromCode) +import Time +import BoardRandomizer +import PathFinder exposing (findPaths) +import Types exposing (..) -- APP -main : Program Never Model Msg +main : Program Flags Model Msg main = - Html.beginnerProgram { model = model, view = view, update = update } + Html.programWithFlags { init = init, subscriptions = subscriptions, view = view, update = update } --- MODEL - +-- SUBSCRIPTIONS -type alias Model = - { board : BoardDict, score : Int, currentGuess : String } +subscriptions : Model -> Sub Msg +subscriptions model = + Sub.batch + [ Keyboard.presses + (\code -> Presses (Char.fromCode code)) + , Time.every Time.second Tick + ] -type alias Board = - List Row -type alias Tile = - { letter : String, match : Bool } +-- MODEL -type alias Point = - ( Int, Int ) +type alias Model = + { board : BoardDict + , score : Int + , currentGuess : String + , foundWords : List String + , hasMatch : Bool + , guessed : Bool + , correct : Maybe Bool + , definition : Maybe String + , ticks : Int + } -type alias Row = - List Tile +type alias Flags = + { startTime : Int + } model : Model model = - { board = board, score = 0, currentGuess = "" } + { board = getBoardDict <| [ [ { letter = "", match = False } ] ] + , score = 0 + , currentGuess = "" + , foundWords = [] + , hasMatch = False + , guessed = False + , correct = Nothing + , definition = Nothing + , ticks = 0 + } boardWidth : Int boardWidth = - 3 + 5 + + + +-- INIT + + +init : Flags -> ( Model, Cmd Msg ) +init flags = + ( { model + | board = createBoard flags.startTime + } + , Cmd.none + ) -board : BoardDict -board = +createBoard : Int -> BoardDict +createBoard seed = let letters = - [ [ "a", "b", "a" ], [ "a", "d", "k" ], [ "p", "w", "z" ] ] + BoardRandomizer.createRandomBoard boardWidth seed tilesForRow : List String -> Row tilesForRow row = @@ -73,81 +115,114 @@ board = type Msg = NoOp | ScoreWord - | UpdateGuess String + | UpdateGuessWord String + | Presses Char + | DefineWord (Result Http.Error String) + | Tick Time.Time -update : Msg -> Model -> Model +update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = case msg of + Presses code -> + if code == '\x0D' then + (update ScoreWord model) + else + (update NoOp model) + + Tick time -> + ( { model + | ticks = model.ticks + 1 + } + , Cmd.none + ) + NoOp -> - model + ( model, Cmd.none ) ScoreWord -> - { model - | score = model.score + (String.length model.currentGuess) + let + isValidGuess = + model.hasMatch && not (List.member model.currentGuess model.foundWords) + + isCorrect = + if isValidGuess then + Just True + else + Nothing + in + ( { model + | guessed = True + , currentGuess = + if isValidGuess then + model.currentGuess + else + "" + , correct = isCorrect + } + , if isValidGuess then + lookUpWord model.currentGuess + else + Cmd.none + ) + + DefineWord (Ok definition) -> + ( { model + | definition = Just (definition) , currentGuess = "" - } + , correct = Just True + , score = model.score + (String.length model.currentGuess) + , foundWords = + if not (List.member model.currentGuess model.foundWords) then + model.currentGuess :: model.foundWords + else + model.foundWords + } + , Task.attempt (always NoOp) (Dom.focus "guess-input") + ) + + DefineWord (Err _) -> + ( { model + | definition = Nothing + , currentGuess = "" + , correct = Just False + } + , Task.attempt (always NoOp) (Dom.focus "guess-input") + ) - UpdateGuess guess -> + UpdateGuessWord guess -> let - firstLetter : String -> String - firstLetter string = - String.slice 0 1 string - - matchFirstLetter : Point -> Tile -> Tile - matchFirstLetter point tile = - checkTile (firstLetter guess) point tile - - isMatching : Point -> Tile -> Bool - isMatching _ tile = - tile.match - - neighborList : List Point - neighborList = - List.concatMap getNeighbors <| - Dict.keys <| - Dict.filter isMatching <| - Dict.map matchFirstLetter model.board - - mappingThingy : Point -> Tile -> Tile - mappingThingy point tile = - if List.member point neighborList then + findMatches : Point -> Tile -> Tile + findMatches point tile = + if List.member point (Maybe.withDefault [] (List.head <| findPaths boardWidth model.board guess)) then { tile | match = True } else tile newDict = - Dict.map mappingThingy model.board + Dict.map findMatches <| clearBoard model.board in - { model - | currentGuess = guess + ( { model + | currentGuess = + guess , board = newDict - } - - -getNeighbors : Point -> List Point -getNeighbors ( x, y ) = - Set.toList <| - Set.remove ( x, y ) <| - Set.fromList - [ ( max (x - 1) 0, max (y - 1) 0 ) - , ( max (x - 1) 0, y ) - , ( max (x - 1) 0, min (y + 1) boardWidth ) - , ( x, max (y - 1) 0 ) - , ( x, min (y + 1) boardWidth ) - , ( min (x + 1) boardWidth, max (y - 1) 0 ) - , ( min (x + 1) boardWidth, y ) - , ( min (x + 1) boardWidth, min (y + 1) boardWidth ) - ] + , hasMatch = not <| Dict.isEmpty (Dict.filter (\key tile -> tile.match == True) newDict) + , guessed = False + } + , Cmd.none + ) -checkTile : String -> Point -> Tile -> Tile -checkTile guessLetter location tile = - { tile | match = (guessLetter == tile.letter) } - - -type alias BoardDict = - Dict Point Tile +clearBoard : BoardDict -> BoardDict +clearBoard board = + let + clearTile : Point -> Tile -> Tile + clearTile point tile = + { tile + | match = False + } + in + Dict.map clearTile board getBoardDict : Board -> BoardDict @@ -176,13 +251,69 @@ view model = makeTile tile = span [ classList [ ( "letter", True ), ( "letter--highlighted", tile.match ) ] ] [ text tile.letter ] + + makeFoundWord word = + div [ class "foundWord" ] [ text word ] in - div [] - [ h2 [] [ text <| toString model.score ] - , div [ class "boardContainer" ] (List.map makeTile <| Dict.values model.board) - , div [] - [ input [ placeholder "Guess away!", onInput UpdateGuess, value model.currentGuess ] [] - , button [ onClick ScoreWord ] [ text "Check" ] + div [ class "gameRoot" ] + [ div [ class "header" ] + [ h2 [ class "timer" ] [ text <| "" ++ toString (model.ticks) ] + , h1 [ class "title" ] + [ a [ Html.Attributes.href "http://elm-lang.org/" ] [ text "Elm" ] + , text " Boggle" + ] + ] + , div [ classList [ ( "game", True ), ( "guessed", model.guessed ), ( "pending", model.correct == Nothing ), ( "correct", model.correct == Just True ) ] ] + [ div [] + [ h2 [] [ text <| "Score: " ++ toString model.score ] + , div [ class "boardContainer" ] (List.map makeTile <| Dict.values model.board) + , div [ class "controls" ] + [ input + [ class "guesser" + , placeholder "Guess away!" + , onInput UpdateGuessWord + , value model.currentGuess + , Html.Attributes.autofocus True + ] + [] + , button [ class "checker", onClick ScoreWord ] [ text "Check" ] + ] + ] + , div [ class "foundWordContainer" ] <| [ h2 [] [ text "Found Words" ] ] ++ (List.map makeFoundWord model.foundWords) + ] + , div [ class "footer" ] + [ a [ Html.Attributes.href "https://github.com/jeanettehead/lady-boggle" ] [ text "See the code on GitHub" ] + , a [ Html.Attributes.href "http://iamjea.net" ] [ text "My Website" ] ] - , div [] [ text <| toString model.board ] ] + + + +-- HTTP + + +authenticatedGet : String -> Decode.Decoder String -> Http.Request String +authenticatedGet url decoder = + Http.request + { method = "GET" + , headers = [ Http.header "X-Mashape-Key" "7RhzLqB7x0mshfh5YE2afEP7Ngkxp1xigQqjsnKy6oDuQ7CfkC" ] + , body = Http.emptyBody + , url = url + , expect = Http.expectJson decoder + , timeout = Nothing + , withCredentials = False + } + + +lookUpWord : String -> Cmd Msg +lookUpWord word = + let + url = + "https://wordsapiv1.p.mashape.com/words/" ++ word + in + Http.send DefineWord (authenticatedGet url decodeResponse) + + +decodeResponse : Decode.Decoder String +decodeResponse = + Decode.at [ "word" ] Decode.string diff --git a/src/elm/PathFinder.elm b/src/elm/PathFinder.elm new file mode 100644 index 0000000..d2d1d39 --- /dev/null +++ b/src/elm/PathFinder.elm @@ -0,0 +1,104 @@ +module PathFinder exposing (findPaths) + +import Types exposing (..) +import Dict exposing (Dict) +import Array exposing (Array) +import Set exposing (Set) + + +findPaths : Int -> BoardDict -> String -> List Path +findPaths boardWidth board guess = + List.filter (\path -> List.length path == String.length guess) <| + List.concat <| + (List.map + (\point -> (explorePath boardWidth board [ point ] (shortenedWord guess))) + <| + Dict.keys <| + firstLetterMatches guess board + ) + + +firstLetterMatches : String -> BoardDict -> BoardDict +firstLetterMatches guess board = + let + matchFirstLetter : Point -> Tile -> Tile + matchFirstLetter point tile = + checkTile (firstLetter guess) tile + in + Dict.filter isMatching <| + Dict.map matchFirstLetter board + + +firstLetter : String -> String +firstLetter string = + String.slice 0 1 string + + +shortenedWord : String -> String +shortenedWord word = + String.dropLeft 1 word + + +checkTile : String -> Tile -> Tile +checkTile guessLetter tile = + { tile | match = (guessLetter == tile.letter) } + + +isMatching : Point -> Tile -> Bool +isMatching _ tile = + tile.match + + +explorePath : Int -> BoardDict -> Path -> String -> List Path +explorePath boardWidth board path word = + let + lastPoint : Path -> Maybe Point + lastPoint path = + Array.get 0 (Array.fromList path) + + travel : BoardDict -> String -> Path -> List Path + travel board word path = + List.map + (\match -> + if (not <| List.member match path) then + List.append [ match ] path + else + [] + ) + (matchingNeighbors boardWidth board (lastPoint path) (firstLetter word)) + in + if String.length word > 0 then + List.concatMap (\aPath -> (explorePath boardWidth board aPath <| shortenedWord word)) (travel board word path) + else + [ path ] + + +matchingNeighbors : Int -> BoardDict -> Maybe Point -> String -> List Point +matchingNeighbors boardWidth board point letter = + let + isMatch : Point -> Bool + isMatch point = + (Maybe.withDefault { letter = "", match = False } <| Dict.get point board).letter == letter + in + case point of + Just value -> + List.filter isMatch (getNeighbors boardWidth value) + + Nothing -> + [] + + +getNeighbors : Int -> Point -> List Point +getNeighbors boardWidth ( x, y ) = + Set.toList <| + Set.remove ( x, y ) <| + Set.fromList + [ ( max (x - 1) 0, max (y - 1) 0 ) + , ( max (x - 1) 0, y ) + , ( max (x - 1) 0, min (y + 1) boardWidth ) + , ( x, max (y - 1) 0 ) + , ( x, min (y + 1) boardWidth ) + , ( min (x + 1) boardWidth, max (y - 1) 0 ) + , ( min (x + 1) boardWidth, y ) + , ( min (x + 1) boardWidth, min (y + 1) boardWidth ) + ] diff --git a/src/elm/Types.elm b/src/elm/Types.elm new file mode 100644 index 0000000..23d116f --- /dev/null +++ b/src/elm/Types.elm @@ -0,0 +1,35 @@ +module Types exposing (..) + +import Dict exposing (Dict) + + +type alias Board = + List Row + + +type alias Tile = + { letter : String, match : Bool } + + +type alias Point = + ( Int, Int ) + + +type alias Row = + List Tile + + +type alias StartingPoints = + List Point + + +type alias Path = + List Point + + +type alias Paths = + List Path + + +type alias BoardDict = + Dict Point Tile diff --git a/src/favicon.ico b/src/favicon.ico index ad6e719..df8564f 100644 Binary files a/src/favicon.ico and b/src/favicon.ico differ diff --git a/src/static/index.html b/src/static/index.html index 0a666cc..73ea69e 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -3,9 +3,9 @@ - elm-webpack-starter - - + Boggle + + diff --git a/src/static/index.js b/src/static/index.js index 0d1a0ad..e57fd1c 100644 --- a/src/static/index.js +++ b/src/static/index.js @@ -5,4 +5,4 @@ require( '../../node_modules/bootstrap-sass/assets/javascripts/bootstrap.js' ); // inject bundled Elm app into div#main var Elm = require( '../elm/Main' ); -Elm.Main.embed( document.getElementById( 'main' ) ); +var app = Elm.Main.fullscreen({startTime: Date.now()}) diff --git a/src/static/styles/main.scss b/src/static/styles/main.scss index 6e7b25c..067e438 100644 --- a/src/static/styles/main.scss +++ b/src/static/styles/main.scss @@ -3,9 +3,82 @@ $icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/'; @import '~bootstrap-sass/assets/stylesheets/_bootstrap.scss'; // can add Boostrap overrides, additional Sass/CSS below... +.footer { + display: flex; + flex-direction: column; + font-size: 18px; + width: 100%; + border-top: 2px solid #ddd; + margin-top: 50px; + padding-top: 10px; +} + +.controls { + width: 340px; + margin-left: 5px; + display: flex; + flex-direction: column; +} + +.guesser { + height: 50px; + width: 100%; + font-size: 24px; +} + +.checker { + height: 50px; + width: 100px; + font-size: 24px; + margin-top: 5px; + align-self: flex-end; +} + +.gameRoot { + padding-left: 30px; + padding-right: 30px; +} +.game { + display: flex; +} + +.header { + display: flex; +} + +.title { + color: #1c818e; + position: absolute; + margin-left: 150px; + a { + color: #1c818e; + } + a:hover { + color: #125a63; + text-decoration: none; + } +} + + +.timer { + margin-top: 50px; + margin-bottom: 0; +} + +.guessed .letter--highlighted { + background-color: #f2ac9f; +} + +.correct.guessed .letter--highlighted { + background-color: #8cc174; +} + +.pending.guessed .letter--highlighted { + background-color: #aa1ed8; +} .boardContainer { - width: 200px; + width: 350px; } .letter { display: inline-block; @@ -15,7 +88,16 @@ $icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/'; text-transform: uppercase; border: 1px solid blue; background-color: #EEE; + height: 60px; + width: 60px; } .letter--highlighted { background-color: #cc33ff; } + +.foundWordContainer { + font-size: 20px; + padding-left: 50px; + max-height: 500px; + overflow: scroll; +}