├── src ├── static │ ├── styles │ │ ├── vars.scss │ │ ├── components │ │ │ ├── counter.scss │ │ │ ├── hangman.scss │ │ │ ├── index.scss │ │ │ ├── box.scss │ │ │ ├── header.scss │ │ │ ├── keyboard.scss │ │ │ ├── lost.scss │ │ │ └── won.scss │ │ └── main.scss │ ├── index.js │ └── index.html ├── favicon.ico └── elm │ ├── Actions.elm │ ├── Main.elm │ ├── Components │ ├── BoxList.elm │ ├── Header.elm │ ├── Keyboard.elm │ └── Hangman.elm │ ├── Model.elm │ ├── SentencesBank.elm │ ├── View.elm │ └── Update.elm ├── .gitignore ├── README.md ├── elm-package.json ├── package.json └── webpack.config.js /src/static/styles/vars.scss: -------------------------------------------------------------------------------- 1 | $primary: #00ca25; -------------------------------------------------------------------------------- /src/static/styles/components/counter.scss: -------------------------------------------------------------------------------- 1 | .counter{ 2 | 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules 3 | tmp/ 4 | dist/ 5 | yarn.lock 6 | 7 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puemos/elm-hangman/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/static/styles/components/hangman.scss: -------------------------------------------------------------------------------- 1 | .hangman{ 2 | padding: 20px; 3 | zoom: 2; 4 | } -------------------------------------------------------------------------------- /src/elm/Actions.elm: -------------------------------------------------------------------------------- 1 | module Actions exposing (Action(..)) 2 | 3 | type Action 4 | = Init 5 | | Press Int 6 | | Next 7 | | Prev 8 | | Noop -------------------------------------------------------------------------------- /src/static/styles/components/index.scss: -------------------------------------------------------------------------------- 1 | @import './header'; 2 | @import './keyboard'; 3 | @import './box'; 4 | @import './counter'; 5 | @import './hangman'; 6 | @import './lost'; 7 | @import './won'; -------------------------------------------------------------------------------- /src/static/styles/components/box.scss: -------------------------------------------------------------------------------- 1 | .box { 2 | & + .box { 3 | margin-left: 15px; 4 | } 5 | } 6 | .boxWord { 7 | & + .boxWord { 8 | margin-left: 40px; 9 | } 10 | } 11 | .boxList { 12 | padding: 0 20px; 13 | } 14 | -------------------------------------------------------------------------------- /src/static/styles/components/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 10px 20px; 3 | } 4 | 5 | .header-nav { 6 | button { 7 | font-family: 'Press Start 2P'; 8 | color: white; 9 | border: none; 10 | background: transparent; 11 | margin: 0 20px; 12 | outline: none; 13 | &:active{ 14 | opacity: 0.3; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/static/index.js: -------------------------------------------------------------------------------- 1 | // pull in desired CSS/SASS files 2 | require( './styles/main.scss' ); 3 | var $ = jQuery = require( '../../node_modules/jquery/dist/jquery.js' ); // <--- remove if jQuery not needed 4 | require( '../../node_modules/bootstrap-sass/assets/javascripts/bootstrap.js' ); // <--- remove if Bootstrap's JS not needed 5 | 6 | // inject bundled Elm app into div#main 7 | var Elm = require( '../elm/Main' ); 8 | Elm.Main.embed( document.getElementById( 'main' ) ); 9 | -------------------------------------------------------------------------------- /src/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Elm Hangman 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/static/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Press+Start+2P'); 2 | @import url('https://rawcdn.githack.com/kristoferjoseph/flexboxgrid/7.0/flexboxgrid.min.css'); 3 | @import './vars'; 4 | @import './components/index'; 5 | 6 | html{ 7 | font-family: 'Press Start 2P'; 8 | } 9 | 10 | .app{ 11 | background-color: rgb(120,120,120); 12 | color: white; 13 | height: 100vh; 14 | align-items: space-between; 15 | } 16 | 17 | .github-star{ 18 | position: fixed; 19 | bottom: 0; 20 | left: 0; 21 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Hangman in Elm 🕴️ - [Try it now!](http://puemos.github.io/elm-hangman) 3 | 4 | ![screen](https://user-images.githubusercontent.com/13174025/28454442-935883ac-6dfb-11e7-9798-bc0f6e7ff64e.gif) 5 | 6 | >Hangman is a paper and pencil guessing game for two or more players. One player thinks of a word, phrase or sentence and the other tries to guess it by suggesting letters or numbers, within a certain number of guesses. 7 | 8 | You can use the keyboard or just click the letters 9 | 10 | ## License 11 | 12 | MIT @ [Shy Alter](https://github.com/puemos) 13 | -------------------------------------------------------------------------------- /src/elm/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Model exposing (Model) 4 | import Update 5 | import View 6 | import Keyboard exposing (KeyCode) 7 | import Html 8 | import Actions exposing (Action(..)) 9 | import Task exposing (Task) 10 | 11 | 12 | subscriptions : Model -> Sub Action 13 | subscriptions model = 14 | Sub.batch 15 | [ Keyboard.downs Press 16 | ] 17 | 18 | 19 | main : Program Never Model Action 20 | main = 21 | Html.program 22 | { init = ( Model.initial, Task.perform (always Init) (Task.succeed 0) ) 23 | , update = Update.update 24 | , view = View.view 25 | , subscriptions = subscriptions 26 | } 27 | -------------------------------------------------------------------------------- /src/elm/Components/BoxList.elm: -------------------------------------------------------------------------------- 1 | module Components.BoxList exposing (boxList) 2 | 3 | import Html exposing (div, Html, text, button) 4 | import Html.Attributes exposing (style, class) 5 | import Actions exposing (..) 6 | import Char 7 | 8 | 9 | box : Char -> Html Action 10 | box letter = 11 | div 12 | [ class "box" ] 13 | [ text (String.fromChar (Char.toUpper letter)) ] 14 | 15 | 16 | boxWord : String -> Html Action 17 | boxWord word = 18 | div 19 | [ class "boxWord row" ] 20 | (List.map 21 | box 22 | (String.toList word) 23 | ) 24 | 25 | 26 | boxList : String -> Html Action 27 | boxList sentencePos = 28 | div 29 | [ class "boxList row center middle last-sm" ] 30 | (List.map 31 | boxWord 32 | (String.words sentencePos) 33 | ) 34 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Example using `elm-webpack-loader`.", 4 | "repository": "https://github.com/moarwick/elm-webpack-starter.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | "src/elm" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/animation-frame": "1.0.1 <= v < 2.0.0", 12 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 13 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 14 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 15 | "elm-lang/keyboard": "1.0.1 <= v < 2.0.0", 16 | "elm-lang/mouse": "1.0.1 <= v < 2.0.0", 17 | "elm-lang/svg": "2.0.0 <= v < 3.0.0", 18 | "evancz/elm-graphics": "1.0.1 <= v < 2.0.0", 19 | "evancz/elm-markdown": "3.0.1 <= v < 4.0.0" 20 | }, 21 | "elm-version": "0.18.0 <= v < 0.19.0" 22 | } 23 | -------------------------------------------------------------------------------- /src/static/styles/components/keyboard.scss: -------------------------------------------------------------------------------- 1 | .keyboard { 2 | padding-bottom: 20px; 3 | } 4 | .keyboardButton { 5 | position: relative; 6 | color: rgb(0, 0, 0); 7 | user-select: none; 8 | touch-action: pan-y; 9 | -webkit-user-drag: none; 10 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 11 | text-align: center; 12 | margin: 5px; 13 | width: 20px; 14 | height: 20px; 15 | line-height: 20px; 16 | 17 | cursor: pointer; 18 | } 19 | 20 | .keyboardButton__disabled { 21 | &::before, 22 | &::after { 23 | position: absolute; 24 | content: ''; 25 | background: red; 26 | display: block; 27 | width: 100%; 28 | height: 2px; 29 | -webkit-transform: rotate(-45deg); 30 | transform: rotate(-45deg); 31 | left: 0; 32 | right: 0; 33 | top: 0; 34 | bottom: 0; 35 | margin: auto; 36 | } 37 | &::after { 38 | -webkit-transform: rotate(45deg); 39 | transform: rotate(45deg); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/elm/Components/Header.elm: -------------------------------------------------------------------------------- 1 | module Components.Header exposing (header) 2 | 3 | import Html exposing (div, Html, text, button) 4 | import Html.Events exposing (onClick) 5 | import Html.Attributes exposing (style, class) 6 | import Actions exposing (..) 7 | 8 | 9 | counter : Int -> Html Action 10 | counter strikes = 11 | div [ class "counter" ] 12 | [ text ("strikes: " ++ (toString strikes)) 13 | ] 14 | 15 | 16 | header : Int -> Int -> Int -> Html Action 17 | header currPos maxPos strikes = 18 | div [ class "header row between" ] 19 | [ text "Hangman" 20 | , div [ class "header-nav row between middle"] 21 | [ button [ onClick Prev ] [ text "<" ] 22 | , div [] 23 | [ text 24 | ((toString currPos) ++ "/" ++ (toString maxPos)) 25 | ] 26 | , button [ onClick Next ] [ text ">" ] 27 | ] 28 | , counter strikes 29 | ] 30 | -------------------------------------------------------------------------------- /src/elm/Model.elm: -------------------------------------------------------------------------------- 1 | module Model exposing (Status(..), Model, initial) 2 | 3 | import Set exposing (Set) 4 | import Char exposing (..) 5 | import Array exposing (Array, fromList) 6 | import SentencesBank exposing (bank) 7 | 8 | 9 | type Status 10 | = Play 11 | | Won 12 | | Lost 13 | 14 | 15 | type alias Model = 16 | { pressed : Set KeyCode 17 | , sentencePos : Int 18 | , sentencePossBank : Array String 19 | , answer : String 20 | , letters : List ( KeyCode, Bool ) 21 | , strikes : Int 22 | , status : Status 23 | } 24 | 25 | 26 | abc : List Char 27 | abc = 28 | (String.toList "abcdefghijklmnopqrstuvwxyz") 29 | 30 | 31 | toUpperCode : Char -> KeyCode 32 | toUpperCode char = 33 | Char.toCode (Char.toUpper char) 34 | 35 | 36 | initial : Model 37 | initial = 38 | { pressed = Set.empty 39 | , sentencePos = 0 40 | , sentencePossBank = fromList bank 41 | , answer = "" 42 | , letters = List.map (\c -> ( (toUpperCode c), False )) abc 43 | , strikes = 0 44 | , status = Play 45 | } 46 | -------------------------------------------------------------------------------- /src/elm/SentencesBank.elm: -------------------------------------------------------------------------------- 1 | module SentencesBank exposing (bank) 2 | 3 | 4 | bank : List String 5 | bank = 6 | [ "Awkward" 7 | , "Bagpipes" 8 | , "Banjo" 9 | , "Bungler" 10 | , "Croquet" 11 | , "Crypt" 12 | , "Dwarves" 13 | , "Fervid" 14 | , "Fishhook" 15 | , "Fjord" 16 | , "Gazebo" 17 | , "Gypsy" 18 | , "Haiku" 19 | , "Haphazard" 20 | , "Hyphen" 21 | , "Ivory" 22 | , "Jazzy" 23 | , "Jiffy" 24 | , "Jinx" 25 | , "Jukebox" 26 | , "Kayak" 27 | , "Kiosk" 28 | , "Klutz" 29 | , "Memento" 30 | , "Mystify" 31 | , "Numbskull" 32 | , "Ostracize" 33 | , "Oxygen" 34 | , "Pajama" 35 | , "Phlegm" 36 | , "Pixel" 37 | , "Polka" 38 | , "Quad" 39 | , "Quip" 40 | , "Rhythmic" 41 | , "Rogue" 42 | , "Sphinx" 43 | , "Squawk" 44 | , "Swivel" 45 | , "Toady" 46 | , "Twelfth" 47 | , "Unzip" 48 | , "Waxy" 49 | , "Wildebeest" 50 | , "Yacht" 51 | , "Zealous" 52 | , "Zigzag" 53 | , "Zippy" 54 | , "Zombie" 55 | ] 56 | -------------------------------------------------------------------------------- /src/static/styles/components/lost.scss: -------------------------------------------------------------------------------- 1 | .lost { 2 | position: fixed; 3 | z-index: 1; 4 | width: 100vw; 5 | height: 100vh; 6 | transition: 1s cubic-bezier(0.215, 0.610, 0.355, 1); 7 | transition-property: opacity; 8 | opacity: 1; 9 | background: rgba(255, 138, 138, 0.94); 10 | 11 | &.transparent { 12 | z-index: -1; 13 | opacity: 0; 14 | } 15 | } 16 | 17 | .lost_caption { 18 | margin-bottom: 100px; 19 | font-family: 'Press Start 2P'; 20 | font-size: 27px; 21 | color: white; 22 | } 23 | 24 | .lost_actions { 25 | button { 26 | padding: 10px 20px; 27 | font-family: 'Press Start 2P'; 28 | font-size: 10px; 29 | transition: 1s cubic-bezier(0.215, 0.610, 0.355, 1); 30 | transition-property: background color; 31 | color: white; 32 | border: 1px solid white; 33 | outline: none; 34 | background: transparent; 35 | &:hover { 36 | color: rgba(255, 138, 138, 1); 37 | background: white; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/static/styles/components/won.scss: -------------------------------------------------------------------------------- 1 | .won { 2 | position: fixed; 3 | z-index: 1; 4 | width: 100vw; 5 | height: 100vh; 6 | transition: 1s cubic-bezier(0.215, 0.610, 0.355, 1); 7 | transition-property: opacity; 8 | opacity: 1; 9 | background: rgba(105, 251, 130, 0.94); 10 | &.transparent { 11 | z-index: -1; 12 | width: 0; 13 | opacity: 0; 14 | } 15 | } 16 | .won_caption { 17 | margin-bottom: 100px; 18 | font-family: 'Press Start 2P'; 19 | font-size: 27px; 20 | color: white; 21 | } 22 | .won_actions { 23 | button { 24 | padding: 10px 20px; 25 | font-family: 'Press Start 2P'; 26 | font-size: 10px; 27 | transition: 1s cubic-bezier(0.215, 0.610, 0.355, 1); 28 | transition-property: background color; 29 | color: white; 30 | border: 1px solid white; 31 | outline: none; 32 | background: transparent; 33 | & + button { 34 | margin-left: 40px; 35 | } 36 | &:hover { 37 | color: rgba(105, 251, 130, 1); 38 | background: white; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/elm/Components/Keyboard.elm: -------------------------------------------------------------------------------- 1 | module Components.Keyboard exposing (keyboard) 2 | 3 | import Html exposing (div, Html, text, button) 4 | import Html.Events exposing (onClick) 5 | import Html.Attributes exposing (style, class) 6 | import Actions exposing (..) 7 | import Char 8 | 9 | 10 | keyboardButton : Bool -> ( Int, Bool ) -> Html Action 11 | keyboardButton disable ( charCode, isPreesed ) = 12 | let 13 | className = 14 | if isPreesed then 15 | "keyboardButton__disabled" 16 | else 17 | "" 18 | 19 | letter = 20 | (String.fromChar (Char.toUpper (Char.fromCode charCode))) 21 | 22 | action = 23 | if (disable) then 24 | Noop 25 | else 26 | (Press charCode) 27 | in 28 | div 29 | [ class ("keyboardButton " ++ className), onClick action ] 30 | [ text letter ] 31 | 32 | 33 | keyboard : Bool -> List ( Int, Bool ) -> Html Action 34 | keyboard disable letters = 35 | div 36 | [ class "keyboard row center" ] 37 | (List.map 38 | (keyboardButton disable) 39 | letters 40 | ) 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-hangman", 3 | "description": "The game of Hangman written in Elm.", 4 | "version": "0.2.0", 5 | "license": "MIT", 6 | "author": "Shy Alter", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/puemos/elm-hangman" 10 | }, 11 | "keywords": [ 12 | "elm", 13 | "hangman", 14 | "webpack", 15 | "game" 16 | ], 17 | "scripts": { 18 | "start": "webpack-dev-server --hot --inline", 19 | "prebuild": "rimraf dist", 20 | "build": "webpack", 21 | "reinstall": "npm i rimraf && rimraf node_modules && npm uninstall -g elm && npm i -g elm && npm i && elm package install" 22 | }, 23 | "devDependencies": { 24 | "autoprefixer": "^6.7.7", 25 | "bootstrap-sass": "^3.4.1", 26 | "copy-webpack-plugin": "^4.0.1", 27 | "css-loader": "^0.27.3", 28 | "elm": "^0.18.0", 29 | "elm-webpack-loader": "^4.3.0", 30 | "extract-text-webpack-plugin": "^2.1.0", 31 | "file-loader": "^0.10.1", 32 | "html-webpack-plugin": "^2.28.0", 33 | "jquery": "^3.5.0", 34 | "node-sass": "^7.0.0", 35 | "postcss-loader": "^1.3.3", 36 | "rimraf": "^2.6.1", 37 | "sass-loader": "^6.0.3", 38 | "style-loader": "^0.16.0", 39 | "url-loader": "^0.5.8", 40 | "webpack": "^2.3.1", 41 | "webpack-dev-server": "^5.0.4", 42 | "webpack-merge": "^4.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/elm/View.elm: -------------------------------------------------------------------------------- 1 | module View exposing (view) 2 | 3 | import Html exposing (div, Html, text, button, iframe) 4 | import Html.Attributes exposing (..) 5 | import Model exposing (Model, Status) 6 | import Components.Hangman exposing (hangman) 7 | import Components.Header exposing (header) 8 | import Components.Keyboard exposing (keyboard) 9 | import Components.BoxList exposing (boxList) 10 | import Html.Events exposing (onClick) 11 | import Actions exposing (..) 12 | import Array 13 | 14 | 15 | (=>) : a -> b -> ( a, b ) 16 | (=>) = 17 | (,) 18 | 19 | 20 | renderLostScreen : Bool -> Html Action 21 | renderLostScreen lost = 22 | let 23 | className = 24 | if lost then 25 | "lost column middle center" 26 | else 27 | "lost transparent column middle center" 28 | in 29 | div [ class className ] 30 | [ div [ class "lost_caption" ] [ text "You died" ] 31 | , div [ class "lost_actions" ] 32 | [ button [ onClick Init ] [ text "Replay" ] 33 | ] 34 | ] 35 | 36 | 37 | renderWonScreen : Bool -> Html Action 38 | renderWonScreen won = 39 | let 40 | className = 41 | if won then 42 | "won column middle center" 43 | else 44 | "won transparent column middle center" 45 | in 46 | div [ class className ] 47 | [ div [ class "won_caption" ] [ text "Saved!" ] 48 | , div [ class "won_actions row" ] 49 | [ button [ onClick Init ] [ text "Replay" ] 50 | , button [ onClick Next ] [ text "Next" ] 51 | ] 52 | ] 53 | 54 | 55 | view : Model -> Html Action 56 | view model = 57 | let 58 | sentencePosBankLength = 59 | Array.length model.sentencePossBank 60 | in 61 | div 62 | [ class "app column between" ] 63 | [ header model.sentencePos sentencePosBankLength model.strikes 64 | , renderLostScreen (model.status == Model.Lost) 65 | , renderWonScreen (model.status == Model.Won) 66 | , div [ class "main column around" ] 67 | [ div [ class "hangman row middle center" ] 68 | [ hangman model.strikes ] 69 | , boxList model.answer 70 | ] 71 | , keyboard (model.status == Model.Lost) model.letters 72 | , iframe [ class "github", src "https://ghbtns.com/github-btn.html?user=puemos&repo=elm-hangman&type=star&v=2", attribute "frameborder" "0", attribute "scrolling" "0", attribute "width" "170px", attribute "height" "20px" ] [] 73 | ] 74 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var merge = require('webpack-merge'); 4 | var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | var autoprefixer = require('autoprefixer'); 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | var CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | 9 | 10 | const prod = 'production'; 11 | const dev = 'development'; 12 | 13 | // determine build env 14 | const TARGET_ENV = process.env.npm_lifecycle_event === 'build' ? prod : dev; 15 | const isDev = TARGET_ENV == dev; 16 | const isProd = TARGET_ENV == prod; 17 | 18 | // entry and output path/filename variables 19 | const entryPath = path.join(__dirname, 'src/static/index.js'); 20 | const outputPath = path.join(__dirname, 'dist'); 21 | const outputFilename = isProd ? '[name]-[hash].js' : '[name].js' 22 | 23 | console.log('WEBPACK GO! Building for ' + TARGET_ENV); 24 | 25 | // common webpack config (valid for dev and prod) 26 | var commonConfig = { 27 | output: { 28 | path: outputPath, 29 | filename: `static/js/${outputFilename}`, 30 | }, 31 | resolve: { 32 | extensions: ['.js', '.elm'], 33 | modules: ['node_modules'] 34 | }, 35 | module: { 36 | noParse: /\.elm$/, 37 | rules: [{ 38 | test: /\.(eot|ttf|woff|woff2|svg)$/, 39 | use: 'file-loader?publicPath=../../&name=static/css/[hash].[ext]' 40 | }] 41 | }, 42 | plugins: [ 43 | new webpack.LoaderOptionsPlugin({ 44 | options: { 45 | postcss: [autoprefixer()] 46 | } 47 | }), 48 | new HtmlWebpackPlugin({ 49 | template: 'src/static/index.html', 50 | inject: 'body', 51 | favicon: 'src/favicon.ico', 52 | filename: 'index.html' 53 | }) 54 | ] 55 | } 56 | 57 | // additional webpack settings for local env (when invoked by 'npm start') 58 | if (isDev === true) { 59 | module.exports = merge(commonConfig, { 60 | entry: [ 61 | 'webpack-dev-server/client?http://localhost:8080', 62 | entryPath 63 | ], 64 | devServer: { 65 | // serve index.html in place of 404 responses 66 | historyApiFallback: true, 67 | contentBase: './src', 68 | hot: true 69 | }, 70 | module: { 71 | rules: [{ 72 | test: /\.elm$/, 73 | exclude: [/elm-stuff/, /node_modules/], 74 | use: [{ 75 | loader: 'elm-webpack-loader', 76 | options: { 77 | verbose: true, 78 | warn: true, 79 | debug: true 80 | } 81 | }] 82 | },{ 83 | test: /\.sc?ss$/, 84 | use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'] 85 | }] 86 | } 87 | }); 88 | } 89 | 90 | // additional webpack settings for prod env (when invoked via 'npm run build') 91 | if (isProd === true) { 92 | module.exports = merge(commonConfig, { 93 | entry: entryPath, 94 | module: { 95 | rules: [{ 96 | test: /\.elm$/, 97 | exclude: [/elm-stuff/, /node_modules/], 98 | use: 'elm-webpack-loader' 99 | }, { 100 | test: /\.sc?ss$/, 101 | use: ExtractTextPlugin.extract({ 102 | fallback: 'style-loader', 103 | use: ['css-loader', 'postcss-loader', 'sass-loader'] 104 | }) 105 | }] 106 | }, 107 | plugins: [ 108 | new ExtractTextPlugin({ 109 | filename: 'static/css/[name]-[hash].css', 110 | allChunks: true, 111 | }), 112 | new CopyWebpackPlugin([{ 113 | from: 'src/static/img/', 114 | to: 'static/img/' 115 | }, { 116 | from: 'src/favicon.ico' 117 | }]), 118 | 119 | // extract CSS into a separate file 120 | // minify & mangle JS/CSS 121 | new webpack.optimize.UglifyJsPlugin({ 122 | minimize: true, 123 | compressor: { 124 | warnings: false 125 | } 126 | // mangle: true 127 | }) 128 | ] 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /src/elm/Components/Hangman.elm: -------------------------------------------------------------------------------- 1 | module Components.Hangman exposing (hangman) 2 | 3 | import Actions exposing (..) 4 | import Svg exposing (..) 5 | import Svg.Attributes exposing (..) 6 | 7 | 8 | toShow : Int -> Int -> String 9 | toShow n strikes = 10 | if n > strikes then 11 | "hidden" 12 | else 13 | "" 14 | 15 | 16 | hangman : Int -> Svg Action 17 | hangman strikes = 18 | svg [ height "135", width "81" ] 19 | [ node "title" 20 | [] 21 | [ text "Layer 1" ] 22 | , g [ id "base", visibility (toShow 1 strikes) ] 23 | [ node "line" 24 | [ fill "none", id "svg_1", stroke "#000", strokeWidth "5", x1 "11.1835", x2 "70.00333", y1 "123.35251", y2 "123.35251" ] 25 | [] 26 | , text " " 27 | ] 28 | , g [ id "poll", visibility (toShow 2 strikes) ] 29 | [ node "line" 30 | [ fill "none", id "svg_5", stroke "#000", strokeWidth "5", transform "rotate(90 56.118484497070305,64.44848632812501) ", x1 "-3.88497", x2 "116.12197", y1 "64.44849", y2 "64.44849" ] 31 | [] 32 | , text " " 33 | ] 34 | , g [ id "support", visibility (toShow 3 strikes) ] 35 | [ node "line" 36 | [ fill "none", id "svg_6", stroke "#000", strokeWidth "5", transform "rotate(-141 44.18540954589844,15.687820434570312) ", x1 "29.20883", x2 "59.162", y1 "15.68777", y2 "15.68777" ] 37 | [] 38 | , text " " 39 | ] 40 | , g [ id "above", visibility (toShow 4 strikes) ] 41 | [ node "line" 42 | [ fill "none", id "svg_7", stroke "#000", strokeWidth "5", x1 "16.55079", x2 "57.15915", y1 "6.96102", y2 "6.96102" ] 43 | [] 44 | , text " " 45 | ] 46 | , g [ id "rope", visibility (toShow 5 strikes) ] 47 | [ node "rect" 48 | [ fill "#999999", height "2.46019", id "svg_25", strokeWidth "5", transform "rotate(90 21.00877380371094,29.422645568847656) ", width "40.77304", x "0.62226", y "28.19256" ] 49 | [] 50 | , text " " 51 | , node "rect" 52 | [ fill "#999999", height "2.46019", id "svg_13", strokeWidth "5", width "11.59903", x "12.57304", y "48.2277" ] 53 | [] 54 | , text " " 55 | ] 56 | , g [ id "head", visibility (toShow 6 strikes) ] 57 | [ node "rect" 58 | [ fill "#000000", height "11.59903", id "svg_8", strokeWidth "5", width "11.59903", x "12.57304", y "36.6284" ] 59 | [] 60 | , text " " 61 | , node "rect" 62 | [ fill "#ffffff", height "3.69068", id "svg_26", strokeWidth "5", width "3.69068", x "12.44894", y "36.59328" ] 63 | [] 64 | , text " " 65 | , node "rect" 66 | [ fill "#ffffff", height "3.69068", id "svg_27", strokeWidth "5", width "3.69068", x "20.5333", y "36.59328" ] 67 | [] 68 | , text " " 69 | ] 70 | , g [ id "body", visibility (toShow 7 strikes) ] 71 | [ node "rect" 72 | [ fill "#000000", height "23.90158", id "svg_14", strokeWidth "5", width "18.80492", x "9.10975", y "50.65303" ] 73 | [] 74 | , text " " 75 | ] 76 | , g [ id "left-arm", visibility (toShow 8 strikes) ] 77 | [ node "rect" 78 | [ fill "#000000", height "3.86643", id "svg_17", strokeWidth "5", width "3.86643", x "5.24331", y "74.20312" ] 79 | [] 80 | , text " " 81 | ] 82 | , g [ id "right-arm", visibility (toShow 9 strikes) ] 83 | [ node "rect" 84 | [ fill "#000000", height "3.86643", id "svg_18", strokeWidth "5", width "3.86643", x "27.73892", y "74.55462" ] 85 | [] 86 | , text " " 87 | ] 88 | , g [ id "left-foot1", visibility (toShow 10 strikes) ] 89 | [ node "rect" 90 | [ fill "#000000", height "3.33919", id "svg_19", strokeWidth "5", width "12.12654", x "12.44893", y "74.73036" ] 91 | [] 92 | , text " " 93 | , node "rect" 94 | [ fill "#000000", height "3.33919", id "svg_21", strokeWidth "5", transform "rotate(90 14.11854553222655,87.38415527343751) ", width "18.62917", x "4.80395", y "85.71457" ] 95 | [] 96 | , text " " 97 | , node "rect" 98 | [ fill "#000000", height "3.86643", id "svg_23", strokeWidth "5", width "3.86643", x "8.75825", y "96.52298" ] 99 | [] 100 | , text " " 101 | ] 102 | , g [ id "right-foot", visibility (toShow 11 strikes) ] 103 | [ node "rect" 104 | [ fill "#000000", height "3.33919", id "svg_22", strokeWidth "5", transform "rotate(90 22.905868530273427,87.38415527343751) ", width "18.62917", x "13.59128", y "85.71456" ] 105 | [] 106 | , text " " 107 | , node "rect" 108 | [ fill "#000000", height "3.86643", id "svg_24", strokeWidth "5", width "3.86643", x "24.57547", y "96.69873" ] 109 | [] 110 | , text " " 111 | ] 112 | ] 113 | -------------------------------------------------------------------------------- /src/elm/Update.elm: -------------------------------------------------------------------------------- 1 | module Update exposing (update) 2 | 3 | import Set exposing (..) 4 | import Model exposing (..) 5 | import Actions exposing (..) 6 | import Char exposing (..) 7 | import Array exposing (Array) 8 | 9 | 10 | nextKeyCode : Char.KeyCode 11 | nextKeyCode = 12 | 39 13 | 14 | 15 | prevKeyCode : Char.KeyCode 16 | prevKeyCode = 17 | 37 18 | 19 | 20 | maxStrikes : Int 21 | maxStrikes = 22 | 11 23 | 24 | 25 | update : Action -> Model -> ( Model, Cmd Action ) 26 | update action model = 27 | case action of 28 | Init -> 29 | ( loadSentence model.sentencePos model, Cmd.none ) 30 | 31 | Next -> 32 | ( onNext model, Cmd.none ) 33 | 34 | Prev -> 35 | ( onPrev model, Cmd.none ) 36 | 37 | Press char -> 38 | let 39 | normalizeChar = 40 | Char.toCode (Char.toUpper (Char.fromCode char)) 41 | 42 | maybeSentence = 43 | Array.get model.sentencePos model.sentencePossBank 44 | 45 | sentence = 46 | case maybeSentence of 47 | Just sentenceString -> 48 | sentenceString 49 | 50 | Nothing -> 51 | ("") 52 | 53 | nextPressed = 54 | onKeyPress char model.pressed 55 | 56 | nextStrikes = 57 | calcStrikes nextPressed sentence 58 | 59 | nextAnswer = 60 | checkSentence nextPressed sentence 61 | 62 | nextLetters = 63 | (calcLetters nextPressed model.letters) 64 | 65 | nextStatus = 66 | if (nextStrikes >= maxStrikes) then 67 | Lost 68 | else if (nextAnswer == sentence) then 69 | Won 70 | else 71 | model.status 72 | in 73 | if (char == nextKeyCode) then 74 | ( onNext model, Cmd.none ) 75 | else if (char == prevKeyCode) then 76 | ( onPrev model, Cmd.none ) 77 | else if (model.status == Play) then 78 | ( { model 79 | | pressed = nextPressed 80 | , strikes = nextStrikes 81 | , answer = nextAnswer 82 | , letters = nextLetters 83 | , status = nextStatus 84 | } 85 | , Cmd.none 86 | ) 87 | else 88 | ( model, Cmd.none ) 89 | 90 | Noop -> 91 | ( model, Cmd.none ) 92 | 93 | 94 | onKeyPress : KeyCode -> Set KeyCode -> Set KeyCode 95 | onKeyPress current pressed = 96 | let 97 | char = 98 | (Char.fromCode current) 99 | in 100 | if (Char.isLower char || Char.isUpper char) then 101 | Set.insert current pressed 102 | else 103 | pressed 104 | 105 | 106 | loadSentence : Int -> Model -> Model 107 | loadSentence pos model = 108 | let 109 | intiModel = 110 | Model.initial 111 | 112 | maybeSentence = 113 | Array.get pos model.sentencePossBank 114 | 115 | sentencePos = 116 | case maybeSentence of 117 | Just sentencePosString -> 118 | sentencePosString 119 | 120 | Nothing -> 121 | ("") 122 | 123 | nextPressed = 124 | onKeyPress 0 intiModel.pressed 125 | 126 | nextAnswer = 127 | checkSentence nextPressed sentencePos 128 | in 129 | ({ intiModel 130 | | answer = nextAnswer 131 | , sentencePos = pos 132 | } 133 | ) 134 | 135 | 136 | onNext : Model -> Model 137 | onNext model = 138 | let 139 | sentencePosBankLength = 140 | Array.length model.sentencePossBank 141 | in 142 | (loadSentence (min (model.sentencePos + 1) (sentencePosBankLength - 1))) model 143 | 144 | 145 | onPrev : Model -> Model 146 | onPrev model = 147 | loadSentence (max (model.sentencePos - 1) 0) model 148 | 149 | 150 | calcLetters : Set KeyCode -> List ( KeyCode, Bool ) -> List ( KeyCode, Bool ) 151 | calcLetters pressed letters = 152 | let 153 | checkFn = 154 | (didPressed pressed) 155 | in 156 | List.map (\( c, _ ) -> ( c, checkFn (Char.fromCode c) )) letters 157 | 158 | 159 | calcStrikes : Set Int -> String -> Int 160 | calcStrikes pressed sentence = 161 | let 162 | checkFn = 163 | (didPressed pressed) 164 | 165 | sentenceCharList = 166 | (String.toList sentence) 167 | 168 | sentenceCharListNoSpace = 169 | (List.filter (\c -> c /= ' ') sentenceCharList) 170 | 171 | numOfCorrect = 172 | (Set.fromList (List.filter checkFn sentenceCharListNoSpace)) 173 | in 174 | (Set.size pressed) - (Set.size numOfCorrect) 175 | 176 | 177 | didPressed : Set KeyCode -> Char -> Bool 178 | didPressed preesed letter = 179 | let 180 | char = 181 | (Char.toCode (Char.toUpper letter)) 182 | in 183 | Set.member char preesed 184 | 185 | 186 | swapUnpresses : (Char -> Bool) -> Char -> Char 187 | swapUnpresses checkFn letter = 188 | if not (Char.isUpper (Char.toUpper letter) ) then 189 | letter 190 | else if (checkFn letter) then 191 | letter 192 | else 193 | '_' 194 | 195 | 196 | checkSentence : Set KeyCode -> String -> String 197 | checkSentence pressed sentencePos = 198 | let 199 | checkFn = 200 | (didPressed pressed) 201 | 202 | sentencePosCharList = 203 | (String.toList sentencePos) 204 | in 205 | String.fromList (List.map (swapUnpresses checkFn) sentencePosCharList) 206 | --------------------------------------------------------------------------------