├── .formatter.exs ├── .github └── workflows │ └── test-phoenix.yml ├── .gitignore ├── .tool-versions ├── CREDITS.md ├── Procfile ├── README.md ├── assets ├── .babelrc ├── css │ ├── animations.scss │ ├── app.scss │ └── phoenix.css ├── elm │ ├── elm.json │ ├── src │ │ ├── Adventure.elm │ │ ├── Adventure │ │ │ ├── Character.elm │ │ │ ├── Screen.elm │ │ │ ├── SvgView.elm │ │ │ ├── WebGLView.elm │ │ │ └── Window.elm │ │ ├── App.elm │ │ ├── Breakout.elm │ │ ├── Breakout │ │ │ ├── Ball.elm │ │ │ ├── Brick.elm │ │ │ ├── Paddle.elm │ │ │ └── Window.elm │ │ ├── Landing.elm │ │ ├── Main.elm │ │ ├── Mario.elm │ │ ├── NotFound.elm │ │ ├── Pong.elm │ │ ├── Pong │ │ │ ├── Ball.elm │ │ │ ├── Game.elm │ │ │ ├── Paddle.elm │ │ │ └── Window.elm │ │ ├── Route.elm │ │ └── Util │ │ │ ├── Fps.elm │ │ │ ├── Icon.elm │ │ │ ├── Keyboard.elm │ │ │ ├── List.elm │ │ │ ├── Ports.elm │ │ │ ├── Sound.elm │ │ │ ├── Vector.elm │ │ │ └── View.elm │ └── static │ │ └── screens │ │ └── 01 ├── js │ ├── app.js │ ├── beta-hook.ts │ └── elm-hook.js ├── package-lock.json ├── package.json ├── static │ ├── favicon.ico │ ├── images │ │ ├── pixel-ball.png │ │ └── pixel-paddle.png │ ├── robots.txt │ └── sounds │ │ ├── beep.wav │ │ ├── boop.wav │ │ └── music.wav ├── tsconfig.json └── webpack.config.js ├── config ├── config.exs ├── dev.exs ├── prod.exs ├── prod.secret.exs └── test.exs ├── elixir_buildpack.config ├── lib ├── games.ex ├── games │ ├── application.ex │ └── repo.ex ├── games_web.ex └── games_web │ ├── channels │ └── user_socket.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── live │ ├── beta_live.ex │ └── page_live.ex │ ├── router.ex │ ├── telemetry.ex │ ├── templates │ └── layout │ │ ├── app.html.eex │ │ ├── live.html.leex │ │ └── root.html.leex │ └── views │ ├── error_helpers.ex │ ├── error_view.ex │ └── layout_view.ex ├── mix.exs ├── mix.lock ├── phoenix_static_buildpack.config ├── priv ├── gettext │ ├── en │ │ └── LC_MESSAGES │ │ │ └── errors.po │ └── errors.pot └── repo │ ├── migrations │ └── .formatter.exs │ └── seeds.exs └── test ├── games_web ├── live │ └── page_live_test.exs └── views │ ├── error_view_test.exs │ └── layout_view_test.exs ├── support ├── channel_case.ex ├── conn_case.ex └── data_case.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto, :phoenix], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /.github/workflows/test-phoenix.yml: -------------------------------------------------------------------------------- 1 | name: Test Phoenix 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | otp: [23.0.2] 16 | elixir: [1.10.4] 17 | node: [14.5.0] 18 | 19 | services: 20 | db: 21 | image: postgres:12 22 | env: 23 | POSTGRES_USER: postgres 24 | POSTGRES_PASSWORD: postgres 25 | POSTGRES_DB: games_test 26 | ports: ['5432:5432'] 27 | options: >- 28 | --health-cmd pg_isready 29 | --health-interval 10s 30 | --health-timeout 5s 31 | --health-retries 5 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - name: Set up Elixir 37 | uses: erlef/setup-beam@v1 38 | with: 39 | otp-version: ${{ matrix.otp }} 40 | elixir-version: ${{ matrix.elixir }} 41 | 42 | - name: Set up Node 43 | uses: actions/setup-node@v1 44 | with: 45 | node-version: ${{ matrix.node }} 46 | 47 | - name: Install Dependencies 48 | run: | 49 | mix deps.get 50 | npm install --prefix assets 51 | 52 | - name: Run Tests 53 | run: | 54 | mix format --check-formatted 55 | mix test 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | games-*.tar 24 | 25 | # If NPM crashes, it generates a log, let's ignore it too. 26 | npm-debug.log 27 | 28 | # The directory NPM downloads your dependencies sources to. 29 | /assets/node_modules/ 30 | 31 | # Since we are building assets from assets/, 32 | # we ignore priv/static. You may want to comment 33 | # this depending on your deployment strategy. 34 | /priv/static/ 35 | 36 | # Custom 37 | .cache 38 | .DS_Store 39 | elm.js 40 | elm-stuff 41 | node_modules 42 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | erlang 23.0.2 2 | elixir 1.10.4-otp-23 3 | elm 0.19.1 4 | nodejs 14.5.0 5 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Music 4 | 5 | ### Breakout 6 | 7 | Music: "Star Way", from PlayOnLoop.com 8 | Licensed under [Creative Commons by Attribution 4.0](https://creativecommons.org/licenses/by/4.0/) 9 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | release: MIX_ENV=prod mix ecto.migrate 2 | web: MIX_ENV=prod mix phx.server 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create with Games 2 | 3 | [![Test Phoenix](https://github.com/create-with/games/workflows/Test%20Phoenix/badge.svg)](https://github.com/create-with/games/actions?query=workflow%3A%22Test+Phoenix%22) 4 | 5 | ## Phoenix Back-end 6 | 7 | 🚀 Learn more about the Phoenix framework with the 8 | [up and running guide](https://hexdocs.pm/phoenix/up_and_running.html)! 9 | 10 | ## Elm Front-end 11 | 12 | 😍 Check out the [`/assets/elm/src`](/assets/elm/src) folder to see the Elm 13 | front-end application. 14 | -------------------------------------------------------------------------------- /assets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /assets/css/animations.scss: -------------------------------------------------------------------------------- 1 | @keyframes bounceInDown { 2 | from, 3 | 60%, 4 | 75%, 5 | 90%, 6 | to { 7 | animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); 8 | } 9 | 10 | 0% { 11 | opacity: 0; 12 | transform: translate3d(0, -3000px, 0) scaleY(3); 13 | } 14 | 15 | 60% { 16 | opacity: 1; 17 | transform: translate3d(0, 25px, 0) scaleY(0.9); 18 | } 19 | 20 | 75% { 21 | transform: translate3d(0, -10px, 0) scaleY(0.95); 22 | } 23 | 24 | 90% { 25 | transform: translate3d(0, 5px, 0) scaleY(0.985); 26 | } 27 | 28 | to { 29 | transform: translate3d(0, 0, 0); 30 | } 31 | } 32 | 33 | .bounce-in-down { 34 | animation: bounceInDown 1.2s; 35 | } 36 | -------------------------------------------------------------------------------- /assets/css/app.scss: -------------------------------------------------------------------------------- 1 | /* This file is for your main application css. */ 2 | // @import "./phoenix.css"; 3 | @import "../node_modules/nprogress/nprogress.css"; 4 | 5 | /* Animations */ 6 | @import "./animations.scss"; 7 | 8 | /* Fonts */ 9 | .font-retro { 10 | font-family: 'Press Start 2P', cursive; 11 | } 12 | 13 | /* LiveView specific classes for your customizations */ 14 | .phx-no-feedback.invalid-feedback, 15 | .phx-no-feedback .invalid-feedback { 16 | display: none; 17 | } 18 | 19 | .phx-click-loading { 20 | opacity: 0.5; 21 | transition: opacity 1s ease-out; 22 | } 23 | 24 | .phx-disconnected{ 25 | cursor: wait; 26 | } 27 | .phx-disconnected *{ 28 | pointer-events: none; 29 | } 30 | 31 | .phx-modal { 32 | opacity: 1!important; 33 | position: fixed; 34 | z-index: 1; 35 | left: 0; 36 | top: 0; 37 | width: 100%; 38 | height: 100%; 39 | overflow: auto; 40 | background-color: rgb(0,0,0); 41 | background-color: rgba(0,0,0,0.4); 42 | } 43 | 44 | .phx-modal-content { 45 | background-color: #fefefe; 46 | margin: 15% auto; 47 | padding: 20px; 48 | border: 1px solid #888; 49 | width: 80%; 50 | } 51 | 52 | .phx-modal-close { 53 | color: #aaa; 54 | float: right; 55 | font-size: 28px; 56 | font-weight: bold; 57 | } 58 | 59 | .phx-modal-close:hover, 60 | .phx-modal-close:focus { 61 | color: black; 62 | text-decoration: none; 63 | cursor: pointer; 64 | } 65 | 66 | 67 | /* Alerts and form errors */ 68 | .alert { 69 | padding: 15px; 70 | margin-bottom: 20px; 71 | border: 1px solid transparent; 72 | border-radius: 4px; 73 | } 74 | .alert-info { 75 | color: #31708f; 76 | background-color: #d9edf7; 77 | border-color: #bce8f1; 78 | } 79 | .alert-warning { 80 | color: #8a6d3b; 81 | background-color: #fcf8e3; 82 | border-color: #faebcc; 83 | } 84 | .alert-danger { 85 | color: #a94442; 86 | background-color: #f2dede; 87 | border-color: #ebccd1; 88 | } 89 | .alert p { 90 | margin-bottom: 0; 91 | } 92 | .alert:empty { 93 | display: none; 94 | } 95 | .invalid-feedback { 96 | color: #a94442; 97 | display: block; 98 | margin: -1rem 0 2rem; 99 | } 100 | -------------------------------------------------------------------------------- /assets/css/phoenix.css: -------------------------------------------------------------------------------- 1 | /* Includes some default style for the starter application. 2 | * This can be safely deleted to start fresh. 3 | */ 4 | 5 | /* Milligram v1.3.0 https://milligram.github.io 6 | * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license 7 | */ 8 | 9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} 10 | 11 | /* General style */ 12 | h1{font-size: 3.6rem; line-height: 1.25} 13 | h2{font-size: 2.8rem; line-height: 1.3} 14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35} 15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5} 16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4} 17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2} 18 | pre{padding: 1em;} 19 | 20 | .container{ 21 | margin: 0 auto; 22 | max-width: 80.0rem; 23 | padding: 0 2.0rem; 24 | position: relative; 25 | width: 100% 26 | } 27 | select { 28 | width: auto; 29 | } 30 | 31 | /* Phoenix promo and logo */ 32 | .phx-hero { 33 | text-align: center; 34 | border-bottom: 1px solid #e3e3e3; 35 | background: #eee; 36 | border-radius: 6px; 37 | padding: 3em 3em 1em; 38 | margin-bottom: 3rem; 39 | font-weight: 200; 40 | font-size: 120%; 41 | } 42 | .phx-hero input { 43 | background: #ffffff; 44 | } 45 | .phx-logo { 46 | min-width: 300px; 47 | margin: 1rem; 48 | display: block; 49 | } 50 | .phx-logo img { 51 | width: auto; 52 | display: block; 53 | } 54 | 55 | /* Headers */ 56 | header { 57 | width: 100%; 58 | background: #fdfdfd; 59 | border-bottom: 1px solid #eaeaea; 60 | margin-bottom: 2rem; 61 | } 62 | header section { 63 | align-items: center; 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: space-between; 67 | } 68 | header section :first-child { 69 | order: 2; 70 | } 71 | header section :last-child { 72 | order: 1; 73 | } 74 | header nav ul, 75 | header nav li { 76 | margin: 0; 77 | padding: 0; 78 | display: block; 79 | text-align: right; 80 | white-space: nowrap; 81 | } 82 | header nav ul { 83 | margin: 1rem; 84 | margin-top: 0; 85 | } 86 | header nav a { 87 | display: block; 88 | } 89 | 90 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */ 91 | header section { 92 | flex-direction: row; 93 | } 94 | header nav ul { 95 | margin: 1rem; 96 | } 97 | .phx-logo { 98 | flex-basis: 527px; 99 | margin: 2rem 1rem; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /assets/elm/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "BrianHicks/elm-particle": "1.3.1", 10 | "avh4/elm-color": "1.0.0", 11 | "elm/browser": "1.0.2", 12 | "elm/core": "1.0.5", 13 | "elm/html": "1.0.0", 14 | "elm/json": "1.1.3", 15 | "elm/random": "1.0.0", 16 | "elm/svg": "1.0.1", 17 | "elm/url": "1.0.0", 18 | "elm-community/random-extra": "3.1.0", 19 | "elm-explorations/linear-algebra": "1.0.3", 20 | "elm-explorations/webgl": "1.1.2", 21 | "ianmackenzie/elm-3d-camera": "3.1.0", 22 | "ianmackenzie/elm-3d-scene": "1.0.1", 23 | "ianmackenzie/elm-geometry": "3.7.0", 24 | "ianmackenzie/elm-triangular-mesh": "1.1.0", 25 | "ianmackenzie/elm-units": "2.7.0" 26 | }, 27 | "indirect": { 28 | "elm/time": "1.0.0", 29 | "elm/virtual-dom": "1.0.2", 30 | "ianmackenzie/elm-1d-parameter": "1.0.1", 31 | "ianmackenzie/elm-float-extra": "1.1.0", 32 | "ianmackenzie/elm-geometry-linear-algebra-interop": "2.0.2", 33 | "ianmackenzie/elm-interval": "2.0.0", 34 | "ianmackenzie/elm-units-interval": "2.1.0", 35 | "owanturist/elm-union-find": "1.0.0" 36 | } 37 | }, 38 | "test-dependencies": { 39 | "direct": {}, 40 | "indirect": {} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /assets/elm/src/Adventure.elm: -------------------------------------------------------------------------------- 1 | module Adventure exposing 2 | ( Model 3 | , Msg(..) 4 | , init 5 | , subscriptions 6 | , update 7 | , view 8 | ) 9 | 10 | -- IMPORTS 11 | 12 | import Adventure.Character exposing (Character) 13 | import Adventure.Screen exposing (Screen) 14 | import Adventure.SvgView 15 | import Adventure.WebGLView 16 | import Adventure.Window exposing (Window) 17 | import Browser exposing (Document) 18 | import Browser.Events 19 | import Html exposing (Html) 20 | import Html.Attributes 21 | import Json.Decode 22 | import Set 23 | import Util.Fps exposing (Time) 24 | import Util.Keyboard exposing (Controls) 25 | import Util.View 26 | 27 | 28 | 29 | -- MODEL 30 | 31 | 32 | type alias Model = 33 | { character : Character 34 | , deltaTime : Time 35 | , playerKeyPress : Controls 36 | , screen : Screen 37 | , window : Window 38 | } 39 | 40 | 41 | 42 | -- INIT 43 | 44 | 45 | initialModel : Model 46 | initialModel = 47 | { character = Adventure.Character.initialCharacter 48 | , deltaTime = 0.0 49 | , playerKeyPress = Util.Keyboard.initialKeys 50 | , screen = Adventure.Screen.screen01 51 | , window = Adventure.Window.initialWindow 52 | } 53 | 54 | 55 | initialCommand : Cmd Msg 56 | initialCommand = 57 | Cmd.none 58 | 59 | 60 | init : () -> ( Model, Cmd Msg ) 61 | init _ = 62 | ( initialModel, initialCommand ) 63 | 64 | 65 | 66 | -- UPDATE 67 | 68 | 69 | type Msg 70 | = BrowserAdvancedAnimationFrame Time 71 | | PlayerPressedKeyDown String 72 | | PlayerReleasedKey String 73 | 74 | 75 | update : Msg -> Model -> ( Model, Cmd Msg ) 76 | update msg model = 77 | case msg of 78 | BrowserAdvancedAnimationFrame deltaTime -> 79 | ( { model | character = updateCharacter model.playerKeyPress deltaTime model.character } 80 | , Cmd.none 81 | ) 82 | 83 | PlayerPressedKeyDown key -> 84 | ( updateKeyPress key model, Cmd.none ) 85 | 86 | PlayerReleasedKey _ -> 87 | ( { model | playerKeyPress = Set.empty }, Cmd.none ) 88 | 89 | 90 | updateCharacter : Controls -> Time -> Character -> Character 91 | updateCharacter playerKeyPress deltaTime character = 92 | if Util.Keyboard.playerPressedArrowUpKey playerKeyPress then 93 | { character | y = character.y - character.vy * deltaTime } 94 | 95 | else if Util.Keyboard.playerPressedArrowDownKey playerKeyPress then 96 | { character | y = character.y + character.vy * deltaTime } 97 | 98 | else if Util.Keyboard.playerPressedArrowLeftKey playerKeyPress then 99 | { character | x = character.x - character.vx * deltaTime } 100 | 101 | else if Util.Keyboard.playerPressedArrowRightKey playerKeyPress then 102 | { character | x = character.x + character.vx * deltaTime } 103 | 104 | else 105 | character 106 | 107 | 108 | updateKeyPress : String -> Model -> Model 109 | updateKeyPress key model = 110 | if Set.member key Util.Keyboard.validKeys then 111 | { model | playerKeyPress = Set.insert key model.playerKeyPress } 112 | 113 | else 114 | model 115 | 116 | 117 | 118 | -- SUBSCRIPTIONS 119 | 120 | 121 | subscriptions : Model -> Sub Msg 122 | subscriptions _ = 123 | Sub.batch 124 | [ browserAnimationSubscription 125 | , keyDownSubscription 126 | , keyUpSubscription 127 | ] 128 | 129 | 130 | browserAnimationSubscription : Sub Msg 131 | browserAnimationSubscription = 132 | Browser.Events.onAnimationFrameDelta <| handleAnimationFrames 133 | 134 | 135 | handleAnimationFrames : Time -> Msg 136 | handleAnimationFrames milliseconds = 137 | BrowserAdvancedAnimationFrame <| milliseconds / 1000 138 | 139 | 140 | keyDownSubscription : Sub Msg 141 | keyDownSubscription = 142 | Browser.Events.onKeyDown <| Json.Decode.map PlayerPressedKeyDown <| Util.Keyboard.keyDecoder 143 | 144 | 145 | keyUpSubscription : Sub Msg 146 | keyUpSubscription = 147 | Browser.Events.onKeyUp <| Json.Decode.map PlayerReleasedKey <| Util.Keyboard.keyDecoder 148 | 149 | 150 | 151 | -- VIEW 152 | 153 | 154 | view : (Msg -> msg) -> Model -> Document msg 155 | view msg model = 156 | { title = "⚔️ Adventure" 157 | , body = List.map (Html.map msg) [ viewMain model, Util.View.footer ] 158 | } 159 | 160 | 161 | viewMain : Model -> Html Msg 162 | viewMain model = 163 | Html.main_ 164 | [ Html.Attributes.class "h-full p-8" 165 | , Html.Attributes.style "background-color" "lightgray" 166 | ] 167 | [ viewHeader 168 | , viewGame model 169 | ] 170 | 171 | 172 | viewHeader : Html msg 173 | viewHeader = 174 | Html.header [ Html.Attributes.class "flex justify-center" ] 175 | [ Html.h1 [ Html.Attributes.class "font-black text-5xl" ] 176 | [ Html.text "Adventure" ] 177 | ] 178 | 179 | 180 | viewGame : Model -> Html Msg 181 | viewGame model = 182 | Html.section [ Html.Attributes.class "flex flex-row my-4" ] 183 | [ Adventure.SvgView.view model.window model.screen model.character 184 | , Adventure.WebGLView.view model.window 185 | ] 186 | -------------------------------------------------------------------------------- /assets/elm/src/Adventure/Character.elm: -------------------------------------------------------------------------------- 1 | module Adventure.Character exposing 2 | ( Character 3 | , initialCharacter 4 | , view 5 | ) 6 | 7 | -- IMPORTS 8 | 9 | import Svg exposing (Svg) 10 | import Svg.Attributes 11 | 12 | 13 | 14 | -- MODEL 15 | 16 | 17 | type alias Character = 18 | { color : String 19 | , x : Float 20 | , y : Float 21 | , vx : Float 22 | , vy : Float 23 | , width : Float 24 | , height : Float 25 | } 26 | 27 | 28 | initialCharacter : Character 29 | initialCharacter = 30 | { color = "yellow" 31 | , x = 395.0 32 | , y = 500.0 33 | , vx = 350.0 34 | , vy = 350.0 35 | , width = 10.0 36 | , height = 10.0 37 | } 38 | 39 | 40 | 41 | -- VIEW 42 | 43 | 44 | view : Character -> Svg a 45 | view character = 46 | Svg.rect 47 | [ Svg.Attributes.fill <| character.color 48 | , Svg.Attributes.x <| String.fromFloat character.x 49 | , Svg.Attributes.y <| String.fromFloat character.y 50 | , Svg.Attributes.width <| String.fromFloat character.width 51 | , Svg.Attributes.height <| String.fromFloat character.height 52 | ] 53 | [] 54 | -------------------------------------------------------------------------------- /assets/elm/src/Adventure/SvgView.elm: -------------------------------------------------------------------------------- 1 | module Adventure.SvgView exposing (view) 2 | 3 | -- IMPORTS 4 | 5 | import Adventure.Character exposing (Character) 6 | import Adventure.Screen exposing (Screen) 7 | import Adventure.Window exposing (Window) 8 | import Svg exposing (Svg) 9 | import Svg.Attributes 10 | 11 | 12 | 13 | -- 2D VIEW 14 | 15 | 16 | view : Window -> Screen -> Character -> Svg a 17 | view window screen character = 18 | let 19 | viewBoxString = 20 | [ window.x 21 | , window.y 22 | , window.width 23 | , window.height 24 | ] 25 | |> List.map String.fromFloat 26 | |> String.join " " 27 | in 28 | Svg.svg 29 | [ Svg.Attributes.viewBox viewBoxString 30 | , Svg.Attributes.width <| String.fromFloat window.width 31 | , Svg.Attributes.height <| String.fromFloat window.height 32 | ] 33 | [ Adventure.Window.view window 34 | , Adventure.Screen.view screen 35 | , Adventure.Character.view character 36 | ] 37 | -------------------------------------------------------------------------------- /assets/elm/src/Adventure/WebGLView.elm: -------------------------------------------------------------------------------- 1 | module Adventure.WebGLView exposing (view) 2 | 3 | -- IMPORTS 4 | 5 | import Adventure.Window exposing (Window) 6 | import Html exposing (Html) 7 | import Html.Attributes 8 | import Math.Matrix4 exposing (Mat4) 9 | import Math.Vector3 exposing (Vec3) 10 | import WebGL exposing (Entity, Mesh, Shader) 11 | import WebGL.Settings.StencilTest 12 | 13 | 14 | 15 | -- CONFIG 16 | 17 | 18 | type alias Config = 19 | { fps : Int 20 | , tps : Int 21 | , max : Int 22 | , spriteSize : Int 23 | } 24 | 25 | 26 | config : Config 27 | config = 28 | { fps = 60 29 | , tps = 5 30 | , max = 10 31 | , spriteSize = 20 32 | } 33 | 34 | 35 | 36 | -- VIEW 37 | 38 | 39 | view : Window -> Html a 40 | view window = 41 | let 42 | ratio = 43 | window.width / window.height 44 | in 45 | Html.div [] 46 | [ WebGL.toHtmlWith 47 | [ WebGL.alpha True 48 | , WebGL.antialias 49 | , WebGL.depth 1 50 | , WebGL.stencil 0 51 | ] 52 | [ Html.Attributes.height <| round window.height 53 | , Html.Attributes.width <| round window.width 54 | ] 55 | [ wallsView ratio ] 56 | ] 57 | 58 | 59 | wallsView : Float -> Entity 60 | wallsView ratio = 61 | WebGL.entityWith 62 | [ WebGL.Settings.StencilTest.test 63 | { ref = 1 64 | , mask = 0xFF 65 | , test = WebGL.Settings.StencilTest.always 66 | , fail = WebGL.Settings.StencilTest.keep 67 | , zfail = WebGL.Settings.StencilTest.keep 68 | , zpass = WebGL.Settings.StencilTest.replace 69 | , writeMask = 0xFF 70 | } 71 | ] 72 | vertexShader 73 | fragmentShader 74 | square 75 | (Uniforms (Math.Vector3.vec3 0.4 0.2 0.3) 76 | (Math.Vector3.vec3 0 0 0) 77 | (Math.Matrix4.makeScale3 (toFloat config.max + 1) (toFloat config.max + 1) 1 78 | |> Math.Matrix4.mul (Math.Matrix4.makeTranslate3 (toFloat config.max / 2) (toFloat config.max / 2) 0) 79 | |> Math.Matrix4.mul (camera ratio) 80 | ) 81 | light 82 | ) 83 | 84 | 85 | camera : Float -> Mat4 86 | camera ratio = 87 | let 88 | c = 89 | toFloat config.max / 2 90 | 91 | eye = 92 | Math.Vector3.vec3 c -c 15 93 | 94 | center = 95 | Math.Vector3.vec3 c c 0 96 | in 97 | Math.Matrix4.mul (Math.Matrix4.makePerspective 45 ratio 0.01 100) 98 | (Math.Matrix4.makeLookAt eye center Math.Vector3.j) 99 | 100 | 101 | light : Vec3 102 | light = 103 | Math.Vector3.vec3 -1 1 3 104 | |> Math.Vector3.normalize 105 | 106 | 107 | 108 | -- SHADERS 109 | 110 | 111 | type alias Uniforms = 112 | { color : Vec3 113 | , offset : Vec3 114 | , camera : Mat4 115 | , light : Vec3 116 | } 117 | 118 | 119 | type alias Varying = 120 | { vlighting : Float 121 | } 122 | 123 | 124 | vertexShader : Shader Attributes Uniforms Varying 125 | vertexShader = 126 | [glsl| 127 | attribute vec3 position; 128 | attribute vec3 normal; 129 | uniform vec3 offset; 130 | uniform mat4 camera; 131 | uniform vec3 light; 132 | varying highp float vlighting; 133 | void main () { 134 | highp float ambientLight = 0.5; 135 | highp float directionalLight = 0.5; 136 | gl_Position = camera * vec4(position + offset, 1.0); 137 | vlighting = ambientLight + max(dot(normal, light), 0.0) * directionalLight; 138 | } 139 | |] 140 | 141 | 142 | fragmentShader : Shader {} Uniforms Varying 143 | fragmentShader = 144 | [glsl| 145 | precision mediump float; 146 | varying highp float vlighting; 147 | uniform vec3 color; 148 | void main () { 149 | gl_FragColor = vec4(color * vlighting, 1.0); 150 | } 151 | |] 152 | 153 | 154 | 155 | -- GEOMETRIES 156 | 157 | 158 | type alias Attributes = 159 | { position : Vec3 160 | , normal : Vec3 161 | } 162 | 163 | 164 | attributes : Vec3 -> Vec3 -> Vec3 -> ( Attributes, Attributes, Attributes ) 165 | attributes p1 p2 p3 = 166 | let 167 | normal = 168 | Math.Vector3.sub p1 p3 169 | |> Math.Vector3.cross (Math.Vector3.sub p1 p2) 170 | |> Math.Vector3.normalize 171 | 172 | attributesFor : Vec3 -> Attributes 173 | attributesFor p = 174 | Attributes p normal 175 | in 176 | ( attributesFor p1, attributesFor p2, attributesFor p3 ) 177 | 178 | 179 | square : Mesh Attributes 180 | square = 181 | WebGL.triangles 182 | [ attributes (Math.Vector3.vec3 -0.5 0.5 0) (Math.Vector3.vec3 -0.5 -0.5 0) (Math.Vector3.vec3 0.5 0.5 0) 183 | , attributes (Math.Vector3.vec3 -0.5 -0.5 0) (Math.Vector3.vec3 0.5 -0.5 0) (Math.Vector3.vec3 0.5 0.5 0) 184 | ] 185 | -------------------------------------------------------------------------------- /assets/elm/src/Adventure/Window.elm: -------------------------------------------------------------------------------- 1 | module Adventure.Window exposing 2 | ( Window 3 | , initialWindow 4 | , view 5 | ) 6 | 7 | -- IMPORTS 8 | 9 | import Svg exposing (Svg) 10 | import Svg.Attributes 11 | 12 | 13 | 14 | -- MODEL 15 | 16 | 17 | type alias Window = 18 | { backgroundColor : String 19 | , x : Float 20 | , y : Float 21 | , width : Float 22 | , height : Float 23 | } 24 | 25 | 26 | initialWindow : Window 27 | initialWindow = 28 | { backgroundColor = "lightgray" 29 | , x = 0.0 30 | , y = 0.0 31 | , width = 800.0 32 | , height = 600.0 33 | } 34 | 35 | 36 | 37 | -- VIEW 38 | 39 | 40 | view : Window -> Svg msg 41 | view window = 42 | Svg.rect 43 | [ Svg.Attributes.fill <| window.backgroundColor 44 | , Svg.Attributes.x <| String.fromFloat window.x 45 | , Svg.Attributes.y <| String.fromFloat window.y 46 | , Svg.Attributes.width <| String.fromFloat window.width 47 | , Svg.Attributes.height <| String.fromFloat window.height 48 | ] 49 | [] 50 | -------------------------------------------------------------------------------- /assets/elm/src/App.elm: -------------------------------------------------------------------------------- 1 | module App exposing 2 | ( Flags 3 | , Model 4 | , Msg(..) 5 | , init 6 | , subscriptions 7 | , update 8 | , view 9 | ) 10 | 11 | -- IMPORTS 12 | 13 | import Adventure 14 | import Breakout 15 | import Browser 16 | import Browser.Navigation 17 | import Landing 18 | import Mario 19 | import NotFound 20 | import Pong 21 | import Route 22 | import Url 23 | import Url.Parser 24 | import Util.Sound 25 | 26 | 27 | 28 | -- MODEL 29 | 30 | 31 | type alias Flags = 32 | () 33 | 34 | 35 | type alias Model = 36 | { flags : Flags 37 | , key : Browser.Navigation.Key 38 | , route : Route.Route 39 | , url : Url.Url 40 | } 41 | 42 | 43 | init : Flags -> Url.Url -> Browser.Navigation.Key -> ( Model, Cmd Msg ) 44 | init flags url key = 45 | changedUrl url <| 46 | { flags = flags 47 | , key = key 48 | , route = Route.Landing 49 | , url = url 50 | } 51 | 52 | 53 | 54 | -- UPDATE 55 | 56 | 57 | type Msg 58 | = ChangedUrl Url.Url 59 | | ClickedUrl Browser.UrlRequest 60 | | ReceivedAdventureMsg Adventure.Msg 61 | | ReceivedBreakoutMsg Breakout.Msg 62 | | ReceivedMarioMsg Mario.Msg 63 | | ReceivedPongMsg Pong.Msg 64 | 65 | 66 | update : Msg -> Model -> ( Model, Cmd Msg ) 67 | update msg model = 68 | case msg of 69 | ChangedUrl url -> 70 | changedUrl url model 71 | 72 | ClickedUrl urlRequest -> 73 | clickedUrl urlRequest model 74 | 75 | ReceivedAdventureMsg pageMsg -> 76 | case model.route of 77 | Route.Adventure pageModel -> 78 | changeToPage model Route.Adventure ReceivedAdventureMsg <| Adventure.update pageMsg pageModel 79 | 80 | _ -> 81 | ( model, Cmd.none ) 82 | 83 | ReceivedBreakoutMsg pageMsg -> 84 | case model.route of 85 | Route.Breakout pageModel -> 86 | changeToPage model Route.Breakout ReceivedBreakoutMsg <| Breakout.update pageMsg pageModel 87 | 88 | _ -> 89 | ( model, Cmd.none ) 90 | 91 | ReceivedMarioMsg pageMsg -> 92 | case model.route of 93 | Route.Mario pageModel -> 94 | changeToPage model Route.Mario ReceivedMarioMsg <| Mario.update pageMsg pageModel 95 | 96 | _ -> 97 | ( model, Cmd.none ) 98 | 99 | ReceivedPongMsg pageMsg -> 100 | case model.route of 101 | Route.Pong pageModel -> 102 | changeToPage model Route.Pong ReceivedPongMsg <| Pong.update pageMsg pageModel 103 | 104 | _ -> 105 | ( model, Cmd.none ) 106 | 107 | 108 | changedUrl : Url.Url -> Model -> ( Model, Cmd Msg ) 109 | changedUrl url model = 110 | case Url.Parser.parse (urlParser model) url of 111 | Just route -> 112 | route 113 | 114 | Nothing -> 115 | ( { model | route = Route.NotFound }, Cmd.none ) 116 | 117 | 118 | clickedUrl : Browser.UrlRequest -> Model -> ( Model, Cmd Msg ) 119 | clickedUrl urlRequest model = 120 | case urlRequest of 121 | Browser.Internal url -> 122 | ( model, Browser.Navigation.pushUrl model.key <| Url.toString url ) 123 | 124 | Browser.External href -> 125 | ( model, Browser.Navigation.load href ) 126 | 127 | 128 | 129 | -- SUBSCRIPTIONS 130 | 131 | 132 | subscriptions : Model -> Sub Msg 133 | subscriptions { route } = 134 | case route of 135 | Route.Adventure pageModel -> 136 | Sub.map ReceivedAdventureMsg <| Adventure.subscriptions pageModel 137 | 138 | Route.Breakout pageModel -> 139 | Sub.map ReceivedBreakoutMsg <| Breakout.subscriptions pageModel 140 | 141 | Route.Mario pageModel -> 142 | Sub.map ReceivedMarioMsg <| Mario.subscriptions pageModel 143 | 144 | Route.Pong pageModel -> 145 | Sub.map ReceivedPongMsg <| Pong.subscriptions pageModel 146 | 147 | _ -> 148 | Sub.none 149 | 150 | 151 | 152 | -- VIEW 153 | 154 | 155 | view : Model -> Browser.Document Msg 156 | view { route } = 157 | case route of 158 | Route.Adventure pageModel -> 159 | Adventure.view ReceivedAdventureMsg pageModel 160 | 161 | Route.Breakout pageModel -> 162 | Breakout.view ReceivedBreakoutMsg pageModel 163 | 164 | Route.Landing -> 165 | Landing.view 166 | 167 | Route.Mario pageModel -> 168 | Mario.view ReceivedMarioMsg pageModel 169 | 170 | Route.NotFound -> 171 | NotFound.view 172 | 173 | Route.Pong pageModel -> 174 | Pong.view ReceivedPongMsg pageModel 175 | 176 | 177 | 178 | -- ROUTING 179 | 180 | 181 | urlParser : Model -> Url.Parser.Parser (( Model, Cmd Msg ) -> b) b 182 | urlParser model = 183 | Url.Parser.oneOf 184 | [ landingPageParser model 185 | , pageParser model "adventure" Route.Adventure ReceivedAdventureMsg <| Adventure.init model.flags 186 | , pageParser model "breakout" Route.Breakout ReceivedBreakoutMsg <| Breakout.init model.flags 187 | , pageParser model "mario" Route.Mario ReceivedMarioMsg <| Mario.init model.flags 188 | , pageParser model "pong" Route.Pong ReceivedPongMsg <| Pong.init model.flags 189 | ] 190 | 191 | 192 | 193 | -- PARSERS 194 | 195 | 196 | pageParser : Model -> String -> (pageModel -> Route.Route) -> (pageMsg -> Msg) -> ( pageModel, Cmd pageMsg ) -> Url.Parser.Parser (( Model, Cmd Msg ) -> b) b 197 | pageParser model urlString pageRoute pageMsg ( pageModel, pageCommand ) = 198 | urlString 199 | |> Url.Parser.s 200 | |> Url.Parser.map (changeToPage model pageRoute pageMsg ( pageModel, pageCommand )) 201 | 202 | 203 | landingPageParser : Model -> Url.Parser.Parser (( Model, Cmd Msg ) -> b) b 204 | landingPageParser model = 205 | Url.Parser.top 206 | |> Url.Parser.map (changeToLandingPage model) 207 | 208 | 209 | 210 | -- PAGE CHANGES 211 | 212 | 213 | changeToPage : Model -> (pageModel -> Route.Route) -> (pageMsg -> Msg) -> ( pageModel, Cmd pageMsg ) -> ( Model, Cmd Msg ) 214 | changeToPage model pageRoute pageMsg ( pageModel, pageCommand ) = 215 | ( { model | route = pageRoute pageModel }, Cmd.map pageMsg pageCommand ) 216 | 217 | 218 | changeToLandingPage : Model -> ( Model, Cmd Msg ) 219 | changeToLandingPage model = 220 | ( { model | route = Route.Landing }, Util.Sound.stopMusic ) 221 | -------------------------------------------------------------------------------- /assets/elm/src/Breakout.elm: -------------------------------------------------------------------------------- 1 | module Breakout exposing 2 | ( Model 3 | , Msg(..) 4 | , init 5 | , subscriptions 6 | , update 7 | , view 8 | ) 9 | 10 | -- IMPORTS 11 | 12 | import Breakout.Ball exposing (Ball, BallPath, ShowBallPath) 13 | import Breakout.Brick exposing (Brick, Bricks) 14 | import Breakout.Paddle exposing (Direction, Paddle) 15 | import Breakout.Window exposing (Window, WindowEdge) 16 | import Browser exposing (Document) 17 | import Browser.Events 18 | import Dict 19 | import Html exposing (Html) 20 | import Html.Attributes 21 | import Json.Decode 22 | import Json.Encode 23 | import Particle exposing (Particle) 24 | import Particle.System exposing (System) 25 | import Process 26 | import Random exposing (Generator) 27 | import Random.Extra 28 | import Random.Float 29 | import Set 30 | import Svg exposing (Svg) 31 | import Svg.Attributes 32 | import Svg.Events 33 | import Task 34 | import Util.Fps exposing (ShowFps, Time) 35 | import Util.Keyboard exposing (Controls) 36 | import Util.Ports 37 | import Util.Sound exposing (PlayMusic) 38 | import Util.Vector 39 | import Util.View 40 | 41 | 42 | 43 | -- MODEL 44 | 45 | 46 | type GameState 47 | = StartingScreen 48 | | PlayingScreen 49 | | PauseScreen 50 | | EndingScreen 51 | 52 | 53 | type Confetti 54 | = Dot 55 | { color : Color 56 | , rotations : Float 57 | , rotationOffset : Float 58 | } 59 | 60 | 61 | type Color 62 | = Red 63 | | Pink 64 | | Yellow 65 | | Green 66 | | Blue 67 | 68 | 69 | type alias Model = 70 | { ball : Ball 71 | , ballPath : BallPath 72 | , bricks : Bricks 73 | , currentBrick : Maybe Brick 74 | , deltaTimes : List Time 75 | , gameState : GameState 76 | , paddle : Paddle 77 | , particleSystem : System Confetti 78 | , playerKeyPress : Controls 79 | , playMusic : PlayMusic 80 | , showBallPath : ShowBallPath 81 | , showFps : ShowFps 82 | , window : Window 83 | } 84 | 85 | 86 | 87 | -- INIT 88 | 89 | 90 | initialModel : Model 91 | initialModel = 92 | { ball = Breakout.Ball.initialBall 93 | , ballPath = Breakout.Ball.initialBallPath 94 | , bricks = Breakout.Brick.initialBricks 95 | , currentBrick = Nothing 96 | , deltaTimes = Util.Fps.initialDeltaTimes 97 | , gameState = StartingScreen 98 | , paddle = Breakout.Paddle.initialPaddle 99 | , particleSystem = Particle.System.init <| Random.initialSeed 0 100 | , playerKeyPress = Util.Keyboard.initialKeys 101 | , playMusic = Util.Sound.initialPlayMusic 102 | , showBallPath = Breakout.Ball.initialShowBallPath 103 | , showFps = Util.Fps.initialShowFps 104 | , window = Breakout.Window.initialWindow 105 | } 106 | 107 | 108 | initialCommand : Cmd Msg 109 | initialCommand = 110 | playMusicCommand Util.Sound.initialPlayMusic "music.wav" 111 | 112 | 113 | init : () -> ( Model, Cmd Msg ) 114 | init _ = 115 | ( initialModel, initialCommand ) 116 | 117 | 118 | 119 | -- UPDATE 120 | 121 | 122 | type Msg 123 | = BrowserAdvancedAnimationFrame Time 124 | | CollisionGeneratedRandomWindowShakePositions ( Float, Float ) 125 | | Particles 126 | | ParticleMsg (Particle.System.Msg Confetti) 127 | | PlayerClickedPlayMusicRadioButton PlayMusic 128 | | PlayerClickedShowBallPathRadioButton ShowBallPath 129 | | PlayerClickedShowFpsRadioButton ShowFps 130 | | PlayerClickedWindow 131 | | PlayerPressedKeyDown String 132 | | PlayerReleasedKey String 133 | | WindowShakeCompleted 134 | 135 | 136 | update : Msg -> Model -> ( Model, Cmd Msg ) 137 | update msg model = 138 | case msg of 139 | BrowserAdvancedAnimationFrame deltaTime -> 140 | let 141 | brickHitByBall = 142 | Breakout.Brick.getBrickHitByBall model.ball model.bricks 143 | 144 | paddleDirection = 145 | Breakout.Paddle.playerKeyPressToDirection model.playerKeyPress 146 | 147 | paddleHitByBall = 148 | Breakout.Paddle.ballHitPaddle model.ball model.paddle 149 | 150 | windowEdgeHitByBall = 151 | Breakout.Window.getWindowEdgeHitByBall model.ball model.window 152 | in 153 | ( { model 154 | | ball = updateBall deltaTime brickHitByBall paddleHitByBall windowEdgeHitByBall model.ball 155 | , ballPath = updateBallPath model.showBallPath windowEdgeHitByBall model.ball model.ballPath 156 | , bricks = updateBricks model.ball model.bricks 157 | , deltaTimes = updateDeltaTimes model.showFps deltaTime model.deltaTimes 158 | , gameState = updateGameState model.gameState model 159 | , paddle = updatePaddle paddleDirection brickHitByBall windowEdgeHitByBall model.window deltaTime model.paddle 160 | } 161 | , commands brickHitByBall 162 | ) 163 | 164 | CollisionGeneratedRandomWindowShakePositions ( randomX, randomY ) -> 165 | ( { model | window = Breakout.Window.shake randomX randomY model.window }, completeWindowShake ) 166 | 167 | Particles -> 168 | ( { model | particleSystem = Particle.System.burst (particlesGenerator 10 model.ball.position) model.particleSystem } 169 | , generateRandomWindowShake 170 | ) 171 | 172 | ParticleMsg particleMsg -> 173 | ( { model | particleSystem = Particle.System.update particleMsg model.particleSystem }, Cmd.none ) 174 | 175 | PlayerClickedPlayMusicRadioButton playMusicValue -> 176 | ( { model | playMusic = playMusicValue }, playMusicCommand playMusicValue "music.wav" ) 177 | 178 | PlayerClickedShowBallPathRadioButton showBallPathValue -> 179 | ( { model | showBallPath = showBallPathValue }, Cmd.none ) 180 | 181 | PlayerClickedShowFpsRadioButton showFpsValue -> 182 | ( { model | showFps = showFpsValue }, Cmd.none ) 183 | 184 | PlayerClickedWindow -> 185 | ( model, Process.sleep 10 |> Task.perform (\_ -> Particles) ) 186 | 187 | PlayerPressedKeyDown key -> 188 | handlePlayerKeyPress key model 189 | 190 | PlayerReleasedKey _ -> 191 | ( { model | playerKeyPress = Set.empty }, Cmd.none ) 192 | 193 | WindowShakeCompleted -> 194 | ( { model | window = Breakout.Window.initialWindow }, Cmd.none ) 195 | 196 | 197 | 198 | -- HANDLE KEYBOARD INPUT 199 | 200 | 201 | handlePlayerKeyPress : String -> Model -> ( Model, Cmd Msg ) 202 | handlePlayerKeyPress key model = 203 | case key of 204 | " " -> 205 | case model.gameState of 206 | StartingScreen -> 207 | ( { model | gameState = updateGameState PlayingScreen model }, Cmd.none ) 208 | 209 | PlayingScreen -> 210 | ( { model | ball = resetBallVelocity model.ball }, Cmd.none ) 211 | 212 | PauseScreen -> 213 | ( { model 214 | | gameState = updateGameState PlayingScreen model 215 | , playMusic = Util.Sound.On 216 | } 217 | , playMusicCommand Util.Sound.On "music.wav" 218 | ) 219 | 220 | EndingScreen -> 221 | ( initialModel, Cmd.none ) 222 | 223 | "Escape" -> 224 | ( { model 225 | | gameState = updateGameState PauseScreen model 226 | , playMusic = Util.Sound.Off 227 | } 228 | , playMusicCommand Util.Sound.Off "music.wav" 229 | ) 230 | 231 | _ -> 232 | ( updateKeyPress key model, Cmd.none ) 233 | 234 | 235 | updateKeyPress : String -> Model -> Model 236 | updateKeyPress key model = 237 | if Set.member key Util.Keyboard.validKeys then 238 | { model | playerKeyPress = Set.insert key model.playerKeyPress } 239 | 240 | else 241 | model 242 | 243 | 244 | 245 | -- UPDATES 246 | 247 | 248 | resetBallVelocity : Ball -> Ball 249 | resetBallVelocity ball = 250 | { ball | velocity = initialModel.ball.velocity } 251 | 252 | 253 | updateBall : Time -> Maybe Brick -> Maybe Paddle -> Maybe WindowEdge -> Ball -> Ball 254 | updateBall deltaTime maybeBrick maybePaddle maybeWindowEdge ball = 255 | ball 256 | |> updateBallWithBrickCollision maybeBrick 257 | |> updateBallWithPaddleCollision maybePaddle 258 | |> updateBallWithWindowCollision maybeWindowEdge 259 | |> updateBallPosition deltaTime 260 | 261 | 262 | updateBallWithBrickCollision : Maybe Brick -> Ball -> Ball 263 | updateBallWithBrickCollision maybeBrick ball = 264 | case maybeBrick of 265 | Just _ -> 266 | -- NAIVE VELOCITY CHANGE 267 | { ball 268 | | velocity = 269 | ( Util.Vector.getX ball.velocity 270 | , negate <| Util.Vector.getY ball.velocity 271 | ) 272 | } 273 | 274 | Nothing -> 275 | ball 276 | 277 | 278 | updateBallWithPaddleCollision : Maybe Paddle -> Ball -> Ball 279 | updateBallWithPaddleCollision maybePaddle ball = 280 | let 281 | amountToChangeBallAngle = 282 | 18.0 283 | 284 | amountToChangeBallSpeed = 285 | 5.0 286 | in 287 | case maybePaddle of 288 | Just paddle -> 289 | { ball 290 | | velocity = 291 | ( Breakout.Paddle.getPaddleHitByBallDistanceFromCenter amountToChangeBallAngle ball paddle 292 | , negate <| Util.Vector.getY ball.velocity + amountToChangeBallSpeed 293 | ) 294 | } 295 | 296 | Nothing -> 297 | ball 298 | 299 | 300 | updateBallWithWindowCollision : Maybe WindowEdge -> Ball -> Ball 301 | updateBallWithWindowCollision maybeWindowEdge ball = 302 | let 303 | ( x, y ) = 304 | ball.position 305 | 306 | ( vx, vy ) = 307 | ball.velocity 308 | in 309 | case maybeWindowEdge of 310 | Just Breakout.Window.Bottom -> 311 | { ball 312 | | position = initialModel.ball.position 313 | , velocity = ( 0, 0 ) 314 | } 315 | 316 | Just Breakout.Window.Left -> 317 | { ball 318 | | position = ( x + ball.width / 2, y ) 319 | , velocity = ( negate vx, vy ) 320 | } 321 | 322 | Just Breakout.Window.Right -> 323 | { ball 324 | | position = ( x - ball.width / 2, y ) 325 | , velocity = ( negate vx, vy ) 326 | } 327 | 328 | Just Breakout.Window.Top -> 329 | { ball 330 | | position = ( x, y + ball.height / 2 ) 331 | , velocity = ( vx, negate vy ) 332 | } 333 | 334 | Nothing -> 335 | ball 336 | 337 | 338 | updateBallPosition : Time -> Ball -> Ball 339 | updateBallPosition deltaTime ball = 340 | { ball 341 | | position = 342 | ball.velocity 343 | |> Util.Vector.scale deltaTime 344 | |> Util.Vector.add ball.position 345 | } 346 | 347 | 348 | updateBallPath : ShowBallPath -> Maybe WindowEdge -> Ball -> BallPath -> BallPath 349 | updateBallPath showBallPath maybeWindowEdge ball ballPath = 350 | case showBallPath of 351 | Breakout.Ball.Off -> 352 | [] 353 | 354 | Breakout.Ball.On -> 355 | case maybeWindowEdge of 356 | Just Breakout.Window.Left -> 357 | [] 358 | 359 | Just Breakout.Window.Right -> 360 | [] 361 | 362 | _ -> 363 | if ball.position /= Breakout.Ball.initialBall.position then 364 | List.take 40 <| ball :: ballPath 365 | 366 | else 367 | [] 368 | 369 | 370 | updateBricks : Ball -> Bricks -> Bricks 371 | updateBricks ball bricks = 372 | bricks 373 | |> Breakout.Brick.filterDestroyedBricks 374 | |> Dict.map (Breakout.Brick.incrementBrickHitCount ball) 375 | 376 | 377 | updateDeltaTimes : ShowFps -> Time -> List Time -> List Time 378 | updateDeltaTimes showFps deltaTime deltaTimes = 379 | case showFps of 380 | Util.Fps.Off -> 381 | deltaTimes 382 | 383 | Util.Fps.On -> 384 | List.take 50 (deltaTime :: deltaTimes) 385 | 386 | 387 | updateGameState : GameState -> Model -> GameState 388 | updateGameState gameState model = 389 | if (gameState == PlayingScreen && model.paddle.lives == 0) || Dict.isEmpty model.bricks then 390 | EndingScreen 391 | 392 | else 393 | gameState 394 | 395 | 396 | updatePaddle : Maybe Direction -> Maybe Brick -> Maybe WindowEdge -> Window -> Time -> Paddle -> Paddle 397 | updatePaddle maybeDirection maybeBrick maybeWindowEdge window deltaTime paddle = 398 | paddle 399 | |> Breakout.Paddle.updatePaddle maybeDirection deltaTime 400 | |> Breakout.Paddle.keepPaddleWithinWindow window 401 | |> Breakout.Paddle.updateScore maybeBrick 402 | |> Breakout.Paddle.updateLives maybeWindowEdge 403 | 404 | 405 | 406 | -- COMMANDS 407 | 408 | 409 | commands : Maybe Brick -> Cmd Msg 410 | commands brickHitByBall = 411 | case brickHitByBall of 412 | Just _ -> 413 | Process.sleep 10 |> Task.perform (\_ -> Particles) 414 | 415 | Nothing -> 416 | Cmd.none 417 | 418 | 419 | completeWindowShake : Cmd Msg 420 | completeWindowShake = 421 | Process.sleep 10 422 | |> Task.perform (\_ -> WindowShakeCompleted) 423 | 424 | 425 | generateRandomWindowShake : Cmd Msg 426 | generateRandomWindowShake = 427 | Random.generate CollisionGeneratedRandomWindowShakePositions randomWindowShakePairGenerator 428 | 429 | 430 | playMusicCommand : PlayMusic -> String -> Cmd Msg 431 | playMusicCommand playMusic soundFile = 432 | Util.Ports.playMusic <| 433 | Json.Encode.object 434 | [ ( "play", Json.Encode.bool <| Util.Sound.playMusicToBool playMusic ) 435 | , ( "soundFile", Json.Encode.string soundFile ) 436 | ] 437 | 438 | 439 | 440 | -- GENERATORS 441 | 442 | 443 | confettiGenerator : Generator Confetti 444 | confettiGenerator = 445 | Random.Extra.frequency ( 5 / 8, dotGenerator ) [] 446 | 447 | 448 | dotGenerator : Generator Confetti 449 | dotGenerator = 450 | Random.map3 451 | (\color rotations rotationOffset -> 452 | Dot 453 | { color = color 454 | , rotations = rotations 455 | , rotationOffset = rotationOffset 456 | } 457 | ) 458 | (Random.weighted 459 | ( 1 / 5, Red ) 460 | [ ( 1 / 5, Pink ) 461 | , ( 1 / 5, Yellow ) 462 | , ( 2 / 5, Green ) 463 | , ( 2 / 5, Blue ) 464 | ] 465 | ) 466 | (Random.Float.normal 1 1) 467 | (Random.float 0 1) 468 | 469 | 470 | particleAt : Float -> Float -> Generator (Particle Confetti) 471 | particleAt x y = 472 | Particle.init confettiGenerator 473 | |> Particle.withLifetime (Random.Float.normal 1.5 0.25) 474 | |> Particle.withLocation (Random.constant { x = x, y = y }) 475 | |> Particle.withDirection (Random.Float.normal (degrees 345) (degrees 15)) 476 | |> Particle.withSpeed (Random.Float.normal 600 100) 477 | |> Particle.withGravity 980 478 | |> Particle.withDrag 479 | (\_ -> 480 | { density = 0.001226 481 | , coefficient = 1.15 482 | , area = 1 483 | } 484 | ) 485 | 486 | 487 | particlesGenerator : Int -> ( Float, Float ) -> Generator (List (Particle Confetti)) 488 | particlesGenerator numberOfParticles ( x, y ) = 489 | Random.list numberOfParticles <| 490 | particleAt x y 491 | 492 | 493 | randomWindowShakePairGenerator : Generator ( Float, Float ) 494 | randomWindowShakePairGenerator = 495 | Random.pair randomWindowShakeGenerator randomWindowShakeGenerator 496 | 497 | 498 | randomWindowShakeGenerator : Generator Float 499 | randomWindowShakeGenerator = 500 | Random.pair (Random.uniform 1 [ -1 ]) (Random.float 5 16) 501 | |> Random.map (\( sign, value ) -> sign * value) 502 | 503 | 504 | 505 | -- SUBSCRIPTIONS 506 | 507 | 508 | subscriptions : Model -> Sub Msg 509 | subscriptions model = 510 | Sub.batch 511 | [ browserAnimationSubscription model.gameState 512 | , keyDownSubscription 513 | , keyUpSubscription 514 | , particleSystemSubscription model.particleSystem 515 | ] 516 | 517 | 518 | browserAnimationSubscription : GameState -> Sub Msg 519 | browserAnimationSubscription gameState = 520 | case gameState of 521 | PlayingScreen -> 522 | Browser.Events.onAnimationFrameDelta <| handleAnimationFrames 523 | 524 | StartingScreen -> 525 | Sub.none 526 | 527 | PauseScreen -> 528 | Sub.none 529 | 530 | EndingScreen -> 531 | Sub.none 532 | 533 | 534 | handleAnimationFrames : Time -> Msg 535 | handleAnimationFrames milliseconds = 536 | BrowserAdvancedAnimationFrame <| milliseconds / 1000 537 | 538 | 539 | keyDownSubscription : Sub Msg 540 | keyDownSubscription = 541 | Browser.Events.onKeyDown <| Json.Decode.map PlayerPressedKeyDown <| Util.Keyboard.keyDecoder 542 | 543 | 544 | keyUpSubscription : Sub Msg 545 | keyUpSubscription = 546 | Browser.Events.onKeyUp <| Json.Decode.map PlayerReleasedKey <| Util.Keyboard.keyDecoder 547 | 548 | 549 | particleSystemSubscription : System Confetti -> Sub Msg 550 | particleSystemSubscription particleSystem = 551 | Particle.System.sub [] ParticleMsg particleSystem 552 | 553 | 554 | 555 | -- VIEW 556 | 557 | 558 | view : (Msg -> msg) -> Model -> Document msg 559 | view msg model = 560 | { title = "\u{1F6F8} Breakout" 561 | , body = List.map (Html.map msg) [ viewMain model, Util.View.footer ] 562 | } 563 | 564 | 565 | viewMain : Model -> Html Msg 566 | viewMain model = 567 | Html.main_ [ Html.Attributes.class "bg-blue-400 h-full p-8" ] 568 | [ viewHeader 569 | , viewGame model 570 | , viewInformation model 571 | ] 572 | 573 | 574 | viewParticles : Particle Confetti -> Svg msg 575 | viewParticles particle = 576 | let 577 | lifetime = 578 | Particle.lifetimePercent particle 579 | 580 | opacity = 581 | if lifetime < 0.1 then 582 | lifetime * 10 583 | 584 | else 585 | 1 586 | in 587 | case Particle.data particle of 588 | Dot { color, rotationOffset, rotations } -> 589 | Svg.rect 590 | [ Svg.Attributes.width "10px" 591 | , Svg.Attributes.height "10px" 592 | , Svg.Attributes.x "-5px" 593 | , Svg.Attributes.y "-5px" 594 | , Svg.Attributes.rx "2px" 595 | , Svg.Attributes.ry "2px" 596 | , Svg.Attributes.fill (fill color) 597 | , Svg.Attributes.stroke "white" 598 | , Svg.Attributes.strokeWidth "4px" 599 | , Svg.Attributes.opacity <| String.fromFloat opacity 600 | , Svg.Attributes.transform <| 601 | "rotate(" 602 | ++ String.fromFloat ((rotations * lifetime + rotationOffset) * 360) 603 | ++ ")" 604 | ] 605 | [] 606 | 607 | 608 | fill : Color -> String 609 | fill color = 610 | case color of 611 | Red -> 612 | "#D72D35" 613 | 614 | Pink -> 615 | "#F2298A" 616 | 617 | Yellow -> 618 | "#F2C618" 619 | 620 | Green -> 621 | "#2ACC42" 622 | 623 | Blue -> 624 | "#37CBE8" 625 | 626 | 627 | viewHeader : Html msg 628 | viewHeader = 629 | Html.header [ Html.Attributes.class "flex justify-center" ] 630 | [ Html.h1 [ Html.Attributes.class "font-black text-5xl" ] 631 | [ Html.text "Breakout" ] 632 | ] 633 | 634 | 635 | viewGame : Model -> Html Msg 636 | viewGame model = 637 | Html.section [ Html.Attributes.class "flex justify-center my-4" ] 638 | [ viewSvg model.window model ] 639 | 640 | 641 | viewSvg : Window -> Model -> Svg Msg 642 | viewSvg window model = 643 | let 644 | viewBoxString = 645 | [ window.x 646 | , window.y 647 | , window.width 648 | , window.height 649 | ] 650 | |> List.map String.fromFloat 651 | |> String.join " " 652 | in 653 | Svg.svg 654 | [ Svg.Attributes.viewBox viewBoxString 655 | , Svg.Attributes.width <| String.fromFloat window.width 656 | , Svg.Attributes.height <| String.fromFloat window.height 657 | , Svg.Events.onClick PlayerClickedWindow 658 | ] 659 | [ Breakout.Window.viewGameWindow window 660 | , Breakout.Brick.viewBricks model.bricks 661 | , Breakout.Paddle.viewPaddle model.paddle 662 | , Breakout.Paddle.viewPaddleScore model.paddle.score 663 | , Breakout.Paddle.viewLives model.paddle.lives 664 | , Breakout.Ball.viewBall model.ball 665 | , Breakout.Ball.viewBallPath model.ballPath |> Svg.g [] 666 | , Particle.System.view viewParticles [] model.particleSystem 667 | , Util.Fps.viewFps Util.Fps.On model.deltaTimes 668 | ] 669 | 670 | 671 | 672 | -- VIEW INFO 673 | 674 | 675 | viewInformation : Model -> Html Msg 676 | viewInformation model = 677 | Html.section [] 678 | [ viewEndingScreen model.gameState model.bricks 679 | , viewPauseScreen model.gameState 680 | , viewInstructions 681 | , viewOptions model.showBallPath model.showFps model.playMusic 682 | ] 683 | 684 | 685 | 686 | -- PAUSE SCREEN 687 | 688 | 689 | viewPauseScreen : GameState -> Html msg 690 | viewPauseScreen gameState = 691 | case gameState of 692 | PauseScreen -> 693 | Html.div [ Html.Attributes.class "pt-4 text-center" ] 694 | [ Html.h2 [ Html.Attributes.class "font-extrabold font-gray-800 pb-1 text-xl" ] 695 | [ Html.text "Game Paused" ] 696 | , Html.p [] 697 | [ Html.text "⏯ Press the SPACEBAR key to continue the game." ] 698 | ] 699 | 700 | StartingScreen -> 701 | Html.span [] [] 702 | 703 | PlayingScreen -> 704 | Html.span [] [] 705 | 706 | EndingScreen -> 707 | Html.span [] [] 708 | 709 | 710 | 711 | -- ENDING SCREEN 712 | 713 | 714 | viewEndingScreen : GameState -> Bricks -> Html msg 715 | viewEndingScreen gameState bricks = 716 | case gameState of 717 | EndingScreen -> 718 | Html.div [ Html.Attributes.class "pt-4 text-center" ] 719 | [ Html.h2 [ Html.Attributes.class "font-extrabold font-gray-800 pb-1 text-xl" ] 720 | [ if Dict.isEmpty bricks then 721 | Html.text "🎉 Congrats! You beat the game!" 722 | 723 | else 724 | Html.text "Game Over!" 725 | ] 726 | , Html.p [] 727 | [ Html.text "🔁 Press the SPACEBAR key to reset the game." ] 728 | ] 729 | 730 | StartingScreen -> 731 | Html.span [] [] 732 | 733 | PlayingScreen -> 734 | Html.span [] [] 735 | 736 | PauseScreen -> 737 | Html.span [] [] 738 | 739 | 740 | 741 | -- INSTRUCTIONS 742 | 743 | 744 | viewInstructions : Html msg 745 | viewInstructions = 746 | Html.div [ Html.Attributes.class "pt-4" ] 747 | [ Html.h2 [ Html.Attributes.class "font-extrabold font-gray-800 pb-1 text-center text-xl" ] 748 | [ Html.text "Instructions" ] 749 | , Html.div [ Html.Attributes.class "flex justify-center" ] 750 | [ Html.ul [ Html.Attributes.class "leading-relaxed list-disc list-inside mx-3" ] 751 | [ Html.li [] [ Html.text "\u{1F6F8} Press the SPACEBAR key to serve the ball." ] 752 | , Html.li [] [ Html.text "⌨️ Use the arrow keys to move the paddle." ] 753 | , Html.li [] [ Html.text "⏸ Press the ESCAPE key if you need to pause the game." ] 754 | , Html.li [] [ Html.text "🏆 Break all the bricks to win!" ] 755 | ] 756 | ] 757 | ] 758 | 759 | 760 | 761 | -- OPTIONS 762 | 763 | 764 | viewOptions : ShowBallPath -> ShowFps -> PlayMusic -> Html Msg 765 | viewOptions showBallPath showFps playMusic = 766 | Html.div [ Html.Attributes.class "pt-4" ] 767 | [ Html.h2 [ Html.Attributes.class "font-extrabold font-gray-800 pb-1 text-center text-xl" ] 768 | [ Html.text "Options" ] 769 | , Html.form [ Html.Attributes.class "flex justify-center" ] 770 | [ Html.ul [ Html.Attributes.class "leading-relaxed list-disc list-inside mx-3" ] 771 | [ Html.li [] [ viewShowBallPathOptions showBallPath ] 772 | , Html.li [] [ viewShowFpsOptions showFps ] 773 | , Html.li [] [ viewPlayMusicOptions playMusic ] 774 | ] 775 | ] 776 | ] 777 | 778 | 779 | viewShowBallPathOptions : ShowBallPath -> Html Msg 780 | viewShowBallPathOptions showBallPath = 781 | Html.fieldset [ Html.Attributes.class "inline" ] 782 | [ Html.span [ Html.Attributes.class "mr-3" ] 783 | [ Html.text "Show ball path history:" ] 784 | , Util.View.radioButton Breakout.Ball.Off showBallPath Breakout.Ball.showBallPathToString PlayerClickedShowBallPathRadioButton 785 | , Util.View.radioButton Breakout.Ball.On showBallPath Breakout.Ball.showBallPathToString PlayerClickedShowBallPathRadioButton 786 | ] 787 | 788 | 789 | viewShowFpsOptions : ShowFps -> Html Msg 790 | viewShowFpsOptions showFps = 791 | Html.fieldset [ Html.Attributes.class "inline" ] 792 | [ Html.span [ Html.Attributes.class "mr-3" ] 793 | [ Html.text "Show FPS meter:" ] 794 | , Util.View.radioButton Util.Fps.Off showFps Util.Fps.showFpsToString PlayerClickedShowFpsRadioButton 795 | , Util.View.radioButton Util.Fps.On showFps Util.Fps.showFpsToString PlayerClickedShowFpsRadioButton 796 | ] 797 | 798 | 799 | viewPlayMusicOptions : PlayMusic -> Html Msg 800 | viewPlayMusicOptions playMusic = 801 | Html.fieldset [ Html.Attributes.class "inline" ] 802 | [ Html.span [ Html.Attributes.class "mr-3" ] 803 | [ Html.text "Play Music:" ] 804 | , Util.View.radioButton Util.Sound.Off playMusic Util.Sound.playMusicToString PlayerClickedPlayMusicRadioButton 805 | , Util.View.radioButton Util.Sound.On playMusic Util.Sound.playMusicToString PlayerClickedPlayMusicRadioButton 806 | ] 807 | -------------------------------------------------------------------------------- /assets/elm/src/Breakout/Ball.elm: -------------------------------------------------------------------------------- 1 | module Breakout.Ball exposing 2 | ( Ball 3 | , BallPath 4 | , ShowBallPath(..) 5 | , initialBall 6 | , initialBallPath 7 | , initialShowBallPath 8 | , showBallPathToString 9 | , viewBall 10 | , viewBallPath 11 | ) 12 | 13 | -- IMPORTS 14 | 15 | import Svg exposing (Svg) 16 | import Svg.Attributes 17 | import Util.Vector exposing (Vector) 18 | 19 | 20 | 21 | -- MODEL 22 | 23 | 24 | type alias Ball = 25 | { position : Vector 26 | , velocity : Vector 27 | , width : Float 28 | , height : Float 29 | } 30 | 31 | 32 | type alias BallPath = 33 | List Ball 34 | 35 | 36 | type ShowBallPath 37 | = Off 38 | | On 39 | 40 | 41 | 42 | -- INIT 43 | 44 | 45 | initialBall : Ball 46 | initialBall = 47 | { position = ( 250.0, 250.0 ) 48 | , velocity = ( 400.0, 400.0 ) 49 | , width = 16.0 50 | , height = 16.0 51 | } 52 | 53 | 54 | initialBallPath : BallPath 55 | initialBallPath = 56 | [] 57 | 58 | 59 | initialShowBallPath : ShowBallPath 60 | initialShowBallPath = 61 | On 62 | 63 | 64 | 65 | -- HELPERS 66 | 67 | 68 | showBallPathToString : ShowBallPath -> String 69 | showBallPathToString showBallPath = 70 | case showBallPath of 71 | On -> 72 | "On" 73 | 74 | Off -> 75 | "Off" 76 | 77 | 78 | 79 | -- VIEW 80 | 81 | 82 | viewBall : Ball -> Svg msg 83 | viewBall ball = 84 | Svg.image 85 | [ Svg.Attributes.xlinkHref "/images/pixel-ball.png" 86 | , Svg.Attributes.x <| String.fromFloat <| Util.Vector.getX ball.position 87 | , Svg.Attributes.y <| String.fromFloat <| Util.Vector.getY ball.position 88 | , Svg.Attributes.width <| String.fromFloat ball.width 89 | , Svg.Attributes.height <| String.fromFloat ball.height 90 | ] 91 | [] 92 | 93 | 94 | viewBallPath : BallPath -> List (Svg msg) 95 | viewBallPath ballPath = 96 | List.indexedMap viewBallPathSegment ballPath 97 | 98 | 99 | viewBallPathSegment : Int -> Ball -> Svg msg 100 | viewBallPathSegment index ball = 101 | Svg.rect 102 | [ Svg.Attributes.fillOpacity <| String.fromFloat <| 0.01 * toFloat (25 - index) 103 | , Svg.Attributes.fill <| "lightyellow" 104 | , Svg.Attributes.x <| String.fromFloat <| Util.Vector.getX ball.position 105 | , Svg.Attributes.y <| String.fromFloat <| Util.Vector.getY ball.position 106 | , Svg.Attributes.width <| String.fromFloat ball.width 107 | , Svg.Attributes.height <| String.fromFloat ball.height 108 | ] 109 | [] 110 | -------------------------------------------------------------------------------- /assets/elm/src/Breakout/Brick.elm: -------------------------------------------------------------------------------- 1 | module Breakout.Brick exposing 2 | ( Brick 3 | , Bricks 4 | , ballHitBrick 5 | , filterDestroyedBricks 6 | , getBrickHitByBall 7 | , incrementBrickHitCount 8 | , initialBricks 9 | , viewBricks 10 | ) 11 | 12 | -- IMPORTS 13 | 14 | import Breakout.Ball exposing (Ball) 15 | import Dict exposing (Dict) 16 | import Svg exposing (Svg) 17 | import Svg.Attributes 18 | import Util.Vector exposing (Vector) 19 | 20 | 21 | 22 | -- MODEL 23 | 24 | 25 | type alias Brick = 26 | { color : String 27 | , height : Float 28 | , hitCount : Int 29 | , hitThreshold : Int 30 | , position : Vector 31 | , strokeColor : String 32 | , width : Float 33 | } 34 | 35 | 36 | type alias Bricks = 37 | Dict ( Int, Int ) Brick 38 | 39 | 40 | 41 | -- INIT 42 | 43 | 44 | defaultBrick : Brick 45 | defaultBrick = 46 | { color = "white" 47 | , height = 16 48 | , hitCount = 0 49 | , hitThreshold = 1 50 | , position = ( 0, 0 ) 51 | , strokeColor = "black" 52 | , width = 80 53 | } 54 | 55 | 56 | initialBricks : Bricks 57 | initialBricks = 58 | buildRow 1 "#f56565" "white" 59 | |> Dict.union (buildRow 2 "#ed8936" "black") 60 | |> Dict.union (buildRow 3 "#ecc94b" "black") 61 | |> Dict.union (buildRow 4 "#48bb78" "black") 62 | |> Dict.union (buildRow 5 "#4299e1" "black") 63 | |> Dict.union (buildRow 6 "#667eea" "black") 64 | |> setHardRow 1 65 | 66 | 67 | buildRow : Int -> String -> String -> Bricks 68 | buildRow rowNumber color strokeColor = 69 | List.range 1 10 70 | |> List.foldr (insertBrick rowNumber) Dict.empty 71 | |> setRowColors color strokeColor 72 | |> setRowPosition 73 | 74 | 75 | insertBrick : Int -> Int -> (Bricks -> Bricks) 76 | insertBrick rowNumber columnNumber = 77 | Dict.insert ( rowNumber, columnNumber ) defaultBrick 78 | 79 | 80 | setRowColors : String -> String -> Bricks -> Bricks 81 | setRowColors color strokeColor row = 82 | Dict.map (\_ brick -> { brick | color = color, strokeColor = strokeColor }) row 83 | 84 | 85 | setRowPosition : Bricks -> Bricks 86 | setRowPosition row = 87 | Dict.map setBrickPosition row 88 | 89 | 90 | setBrickPosition : ( Int, Int ) -> Brick -> Brick 91 | setBrickPosition ( rowNumber, columnNumber ) brick = 92 | let 93 | rowHeight = 94 | toFloat rowNumber * brick.height 95 | 96 | paddingForStroke = 97 | case rowNumber of 98 | 1 -> 99 | 0 100 | 101 | _ -> 102 | 2 103 | 104 | offsetHorizontalColumn = 105 | toFloat (columnNumber - 1) 106 | 107 | paddingFromTopOfWindow = 108 | toFloat 80 109 | in 110 | { brick 111 | | position = 112 | ( offsetHorizontalColumn * brick.width 113 | , paddingFromTopOfWindow + rowHeight + paddingForStroke 114 | ) 115 | } 116 | 117 | 118 | setHardRow : Int -> Bricks -> Bricks 119 | setHardRow rowNumber bricks = 120 | Dict.map (setHardBrick rowNumber) bricks 121 | 122 | 123 | setHardBrick : Int -> ( Int, Int ) -> Brick -> Brick 124 | setHardBrick targetRowNumber ( rowNumber, _ ) brick = 125 | if targetRowNumber == rowNumber then 126 | { brick | hitThreshold = 2 } 127 | 128 | else 129 | brick 130 | 131 | 132 | 133 | -- COLLISIONS 134 | 135 | 136 | ballHitBrick : Ball -> Brick -> Bool 137 | ballHitBrick ball brick = 138 | let 139 | ( ballX, ballY ) = 140 | ball.position 141 | 142 | ( brickX, brickY ) = 143 | brick.position 144 | in 145 | (brickX <= ballX && ballX <= brickX + brick.width) 146 | && (brickY <= ballY && ballY <= brickY + brick.height) 147 | 148 | 149 | getBrickHitByBall : Ball -> Bricks -> Maybe Brick 150 | getBrickHitByBall ball bricks = 151 | bricks 152 | |> Dict.filter (\_ brick -> ballHitBrick ball brick) 153 | |> Dict.values 154 | |> List.head 155 | 156 | 157 | brickHitCountPassedThreshold : ( Int, Int ) -> Brick -> Bool 158 | brickHitCountPassedThreshold _ brick = 159 | brick.hitCount < brick.hitThreshold 160 | 161 | 162 | incrementBrickHitCount : Ball -> ( Int, Int ) -> Brick -> Brick 163 | incrementBrickHitCount ball _ brick = 164 | if ballHitBrick ball brick then 165 | { brick | hitCount = brick.hitCount + 1 } 166 | 167 | else 168 | brick 169 | 170 | 171 | filterDestroyedBricks : Bricks -> Bricks 172 | filterDestroyedBricks bricks = 173 | Dict.filter brickHitCountPassedThreshold bricks 174 | 175 | 176 | 177 | -- VIEW 178 | 179 | 180 | viewBricks : Bricks -> Svg a 181 | viewBricks bricks = 182 | bricks 183 | |> Dict.map viewBrick 184 | |> Dict.values 185 | |> Svg.g [] 186 | 187 | 188 | viewBrick : ( Int, Int ) -> Brick -> Svg a 189 | viewBrick _ brick = 190 | Svg.rect 191 | [ Svg.Attributes.class "bounce-in-down" 192 | , Svg.Attributes.fill <| brick.color 193 | , Svg.Attributes.fillOpacity "1" 194 | , Svg.Attributes.x <| String.fromFloat <| Util.Vector.getX brick.position 195 | , Svg.Attributes.y <| String.fromFloat <| Util.Vector.getY brick.position 196 | , Svg.Attributes.width <| String.fromFloat brick.width 197 | , Svg.Attributes.height <| String.fromFloat brick.height 198 | , Svg.Attributes.stroke <| brick.strokeColor 199 | , Svg.Attributes.strokeWidth "2" 200 | , Svg.Attributes.strokeOpacity <| String.fromFloat <| brickStrokeOpacity brick 201 | ] 202 | [] 203 | 204 | 205 | brickStrokeOpacity : Brick -> Float 206 | brickStrokeOpacity brick = 207 | toFloat (brick.hitThreshold - brick.hitCount) / toFloat brick.hitThreshold 208 | -------------------------------------------------------------------------------- /assets/elm/src/Breakout/Paddle.elm: -------------------------------------------------------------------------------- 1 | module Breakout.Paddle exposing 2 | ( Direction(..) 3 | , Paddle 4 | , ballHitPaddle 5 | , getPaddleHitByBallDistanceFromCenter 6 | , initialPaddle 7 | , keepPaddleWithinWindow 8 | , playerKeyPressToDirection 9 | , updateLives 10 | , updatePaddle 11 | , updateScore 12 | , viewLives 13 | , viewPaddle 14 | , viewPaddleScore 15 | ) 16 | 17 | -- IMPORTS 18 | 19 | import Breakout.Ball exposing (Ball) 20 | import Breakout.Brick exposing (Brick) 21 | import Breakout.Window exposing (Window, WindowEdge) 22 | import Set exposing (Set) 23 | import Svg exposing (Svg) 24 | import Svg.Attributes 25 | import Util.Keyboard 26 | import Util.Vector exposing (Vector) 27 | 28 | 29 | 30 | -- MODEL 31 | 32 | 33 | type Direction 34 | = Left 35 | | Right 36 | 37 | 38 | type alias Paddle = 39 | { height : Float 40 | , lives : Int 41 | , position : Vector 42 | , score : Int 43 | , vx : Float 44 | , width : Float 45 | } 46 | 47 | 48 | 49 | -- INIT 50 | 51 | 52 | initialPaddle : Paddle 53 | initialPaddle = 54 | { height = 20.0 55 | , lives = 3 56 | , position = ( 380.0, 550.0 ) 57 | , score = 0 58 | , vx = 700.0 59 | , width = 80.0 60 | } 61 | 62 | 63 | 64 | -- UPDATE 65 | 66 | 67 | keepPaddleWithinWindow : Window -> Paddle -> Paddle 68 | keepPaddleWithinWindow window paddle = 69 | let 70 | ( x, y ) = 71 | paddle.position 72 | 73 | leftEdge = 74 | 0 75 | 76 | rightEdge = 77 | window.width - paddle.width 78 | in 79 | { paddle | position = ( clamp leftEdge rightEdge x, y ) } 80 | 81 | 82 | updateLives : Maybe WindowEdge -> Paddle -> Paddle 83 | updateLives maybeWindowEdge paddle = 84 | case maybeWindowEdge of 85 | Just Breakout.Window.Bottom -> 86 | { paddle | lives = clamp 0 paddle.lives <| paddle.lives - 1 } 87 | 88 | Just _ -> 89 | paddle 90 | 91 | Nothing -> 92 | paddle 93 | 94 | 95 | updatePaddle : Maybe Direction -> Float -> Paddle -> Paddle 96 | updatePaddle maybeDirection deltaTime paddle = 97 | let 98 | ( x, y ) = 99 | paddle.position 100 | in 101 | case maybeDirection of 102 | Just Left -> 103 | { paddle | position = ( x - paddle.vx * deltaTime, y ) } 104 | 105 | Just Right -> 106 | { paddle | position = ( x + paddle.vx * deltaTime, y ) } 107 | 108 | Nothing -> 109 | paddle 110 | 111 | 112 | updateScore : Maybe Brick -> Paddle -> Paddle 113 | updateScore maybeBrick paddle = 114 | case maybeBrick of 115 | Just _ -> 116 | { paddle | score = paddle.score + 100 } 117 | 118 | Nothing -> 119 | paddle 120 | 121 | 122 | 123 | -- COLLISIONS 124 | 125 | 126 | ballHitPaddle : Ball -> Paddle -> Maybe Paddle 127 | ballHitPaddle ball paddle = 128 | let 129 | ( ballX, ballY ) = 130 | ball.position 131 | 132 | ( _, ballVy ) = 133 | ball.velocity 134 | 135 | ( paddleX, paddleY ) = 136 | paddle.position 137 | 138 | ballCollidedWithPaddle = 139 | (paddleY <= ballY + ball.height) 140 | && (paddleX <= ballX && ballX <= paddleX + paddle.width) 141 | && (ballVy > 0) 142 | in 143 | if ballCollidedWithPaddle then 144 | Just paddle 145 | 146 | else 147 | Nothing 148 | 149 | 150 | getPaddleHitByBallDistanceFromCenter : Float -> Ball -> Paddle -> Float 151 | getPaddleHitByBallDistanceFromCenter scale ball paddle = 152 | case ballHitPaddle ball paddle of 153 | Just paddle_ -> 154 | let 155 | ballCenter = 156 | Util.Vector.getX ball.position + ball.width / 2 157 | 158 | paddleCenter = 159 | Util.Vector.getX paddle.position + paddle_.width / 2 160 | in 161 | (ballCenter - paddleCenter) * scale 162 | 163 | Nothing -> 164 | 0 165 | 166 | 167 | 168 | -- HELPERS 169 | 170 | 171 | playerKeyPressToDirection : Set String -> Maybe Direction 172 | playerKeyPressToDirection playerKeyPress = 173 | if Util.Keyboard.playerPressedArrowLeftKey playerKeyPress then 174 | Just Left 175 | 176 | else if Util.Keyboard.playerPressedArrowRightKey playerKeyPress then 177 | Just Right 178 | 179 | else 180 | Nothing 181 | 182 | 183 | 184 | -- VIEW 185 | 186 | 187 | viewPaddle : Paddle -> Svg msg 188 | viewPaddle paddle = 189 | Svg.image 190 | [ Svg.Attributes.xlinkHref "/images/pixel-paddle.png" 191 | , Svg.Attributes.x <| String.fromFloat <| Util.Vector.getX paddle.position 192 | , Svg.Attributes.y <| String.fromFloat <| Util.Vector.getY paddle.position 193 | , Svg.Attributes.width <| String.fromFloat paddle.width 194 | , Svg.Attributes.height <| String.fromFloat paddle.height 195 | ] 196 | [] 197 | 198 | 199 | viewPaddleScore : Int -> Svg msg 200 | viewPaddleScore score = 201 | Svg.text_ 202 | [ Svg.Attributes.class "font-retro" 203 | , Svg.Attributes.fill "white" 204 | , Svg.Attributes.fontSize "12" 205 | , Svg.Attributes.fontWeight "bold" 206 | , Svg.Attributes.textAnchor "middle" 207 | , Svg.Attributes.x "50%" 208 | , Svg.Attributes.y "22" 209 | ] 210 | [ Svg.text <| String.toUpper <| "Points " ++ String.fromInt score ] 211 | 212 | 213 | viewLives : Int -> Svg msg 214 | viewLives lives = 215 | let 216 | ( width, height ) = 217 | ( 60, 10 ) 218 | 219 | offset = 220 | 44 221 | in 222 | List.range 0 (lives - 1) 223 | |> List.map 224 | (\index -> 225 | Svg.image 226 | [ Svg.Attributes.xlinkHref "/images/pixel-paddle.png" 227 | , Svg.Attributes.x <| String.fromInt <| index * offset 228 | , Svg.Attributes.y <| String.fromFloat <| Breakout.Window.initialWindow.height - 20 229 | , Svg.Attributes.width <| String.fromInt width 230 | , Svg.Attributes.height <| String.fromInt height 231 | ] 232 | [] 233 | ) 234 | |> Svg.g [] 235 | -------------------------------------------------------------------------------- /assets/elm/src/Breakout/Window.elm: -------------------------------------------------------------------------------- 1 | module Breakout.Window exposing 2 | ( Window 3 | , WindowEdge(..) 4 | , getWindowEdgeHitByBall 5 | , initialWindow 6 | , shake 7 | , viewGameWindow 8 | ) 9 | 10 | -- IMPORTS 11 | 12 | import Breakout.Ball exposing (Ball) 13 | import Svg exposing (Svg) 14 | import Svg.Attributes 15 | 16 | 17 | 18 | -- MODEL 19 | 20 | 21 | type alias Window = 22 | { backgroundColor : String 23 | , x : Float 24 | , y : Float 25 | , width : Float 26 | , height : Float 27 | } 28 | 29 | 30 | type WindowEdge 31 | = Bottom 32 | | Left 33 | | Right 34 | | Top 35 | 36 | 37 | 38 | -- INIT 39 | 40 | 41 | initialWindow : Window 42 | initialWindow = 43 | { backgroundColor = "black" 44 | , x = 0.0 45 | , y = 0.0 46 | , width = 800.0 47 | , height = 600.0 48 | } 49 | 50 | 51 | 52 | -- UPDATE 53 | 54 | 55 | shake : Float -> Float -> Window -> Window 56 | shake x y window = 57 | { window 58 | | x = x 59 | , y = y 60 | } 61 | 62 | 63 | 64 | -- COLLISIONS 65 | 66 | 67 | getWindowEdgeHitByBall : Ball -> Window -> Maybe WindowEdge 68 | getWindowEdgeHitByBall ball window = 69 | let 70 | ( x, y ) = 71 | ball.position 72 | in 73 | if y + ball.height >= window.height then 74 | Just Bottom 75 | 76 | else if x <= window.x then 77 | Just Left 78 | 79 | else if x + ball.width >= window.width then 80 | Just Right 81 | 82 | else if y <= window.y then 83 | Just Top 84 | 85 | else 86 | Nothing 87 | 88 | 89 | 90 | -- VIEW 91 | 92 | 93 | viewGameWindow : Window -> Svg msg 94 | viewGameWindow window = 95 | Svg.rect 96 | [ Svg.Attributes.fill <| window.backgroundColor 97 | , Svg.Attributes.x <| String.fromFloat window.x 98 | , Svg.Attributes.y <| String.fromFloat window.y 99 | , Svg.Attributes.width <| String.fromFloat window.width 100 | , Svg.Attributes.height <| String.fromFloat window.height 101 | ] 102 | [] 103 | -------------------------------------------------------------------------------- /assets/elm/src/Landing.elm: -------------------------------------------------------------------------------- 1 | module Landing exposing (view) 2 | 3 | -- IMPORTS 4 | 5 | import Browser 6 | import Html exposing (Html) 7 | import Html.Attributes 8 | import Util.View 9 | 10 | 11 | 12 | -- MODEL 13 | 14 | 15 | type alias Game = 16 | { color : String 17 | , emoji : String 18 | , slug : String 19 | , state : State 20 | , title : String 21 | } 22 | 23 | 24 | type State 25 | = Available 26 | | Unavailable 27 | 28 | 29 | allGames : List Game 30 | allGames = 31 | [ { color = "yellow" 32 | , emoji = "🏓" 33 | , slug = "pong" 34 | , state = Available 35 | , title = "Pong" 36 | } 37 | , { color = "blue" 38 | , emoji = "🛸" 39 | , slug = "breakout" 40 | , state = Available 41 | , title = "Breakout" 42 | } 43 | , { color = "gray" 44 | , emoji = "⚔️" 45 | , slug = "adventure" 46 | , state = Unavailable 47 | , title = "Adventure" 48 | } 49 | , { color = "red" 50 | , emoji = "🍄" 51 | , slug = "mario" 52 | , state = Unavailable 53 | , title = "Mario" 54 | } 55 | ] 56 | 57 | 58 | 59 | -- VIEW 60 | 61 | 62 | view : Browser.Document msg 63 | view = 64 | { title = "🕹 Games" 65 | , body = 66 | [ Html.main_ [ Html.Attributes.class "flex flex-col min-h-screen" ] 67 | [ viewGames allGames 68 | , Util.View.footer 69 | ] 70 | ] 71 | } 72 | 73 | 74 | viewGames : List Game -> Html msg 75 | viewGames games = 76 | Html.section [ Html.Attributes.class "flex-grow" ] 77 | [ games 78 | |> List.filter isAvailable 79 | |> List.map viewGame 80 | |> Html.ul [ Html.Attributes.class "px-16 py-8" ] 81 | ] 82 | 83 | 84 | viewGame : Game -> Html msg 85 | viewGame { color, emoji, slug, title } = 86 | Html.a [ Html.Attributes.href slug ] 87 | [ Html.li 88 | [ Html.Attributes.class <| colorToBorderClass color 89 | , Html.Attributes.class <| colorToColorClass color 90 | , Html.Attributes.class "hover:shadow-lg max-w-sm my-4 px-6 py-3 rounded-md shadow" 91 | ] 92 | [ Html.span [ Html.Attributes.class "mr-2" ] 93 | [ Html.text emoji ] 94 | , Html.strong [] 95 | [ Html.text title ] 96 | ] 97 | ] 98 | 99 | 100 | colorToBorderClass : String -> String 101 | colorToBorderClass color = 102 | "border-b-2 border-solid border-" ++ color ++ "-700" 103 | 104 | 105 | colorToColorClass : String -> String 106 | colorToColorClass color = 107 | "bg-" ++ color ++ "-200" 108 | 109 | 110 | isAvailable : Game -> Bool 111 | isAvailable { state } = 112 | state == Available 113 | -------------------------------------------------------------------------------- /assets/elm/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | -- IMPORTS 4 | 5 | import App 6 | import Browser 7 | 8 | 9 | 10 | -- MAIN 11 | 12 | 13 | main : Program App.Flags App.Model App.Msg 14 | main = 15 | Browser.application 16 | { init = App.init 17 | , onUrlChange = App.ChangedUrl 18 | , onUrlRequest = App.ClickedUrl 19 | , subscriptions = App.subscriptions 20 | , update = App.update 21 | , view = App.view 22 | } 23 | -------------------------------------------------------------------------------- /assets/elm/src/Mario.elm: -------------------------------------------------------------------------------- 1 | module Mario exposing 2 | ( Model 3 | , Msg(..) 4 | , init 5 | , subscriptions 6 | , update 7 | , view 8 | ) 9 | 10 | -- IMPORTS 11 | 12 | import Browser exposing (Document) 13 | import Html exposing (Html) 14 | import Html.Attributes 15 | import Util.View 16 | 17 | 18 | 19 | -- MODEL 20 | 21 | 22 | type alias Model = 23 | { mario : ( Int, Int ) } 24 | 25 | 26 | 27 | -- INIT 28 | 29 | 30 | init : () -> ( Model, Cmd Msg ) 31 | init _ = 32 | ( { mario = ( 0, 0 ) }, Cmd.none ) 33 | 34 | 35 | 36 | -- UPDATE 37 | 38 | 39 | type Msg 40 | = NoOp 41 | 42 | 43 | update : Msg -> Model -> ( Model, Cmd Msg ) 44 | update msg model = 45 | case msg of 46 | NoOp -> 47 | ( model, Cmd.none ) 48 | 49 | 50 | 51 | -- SUBSCRIPTIONS 52 | 53 | 54 | subscriptions : Model -> Sub Msg 55 | subscriptions _ = 56 | Sub.none 57 | 58 | 59 | 60 | -- VIEW 61 | 62 | 63 | view : (Msg -> msg) -> Model -> Document msg 64 | view msg model = 65 | { title = "🍄 Mario" 66 | , body = List.map (Html.map msg) [ viewMain model, Util.View.footer ] 67 | } 68 | 69 | 70 | viewMain : Model -> Html Msg 71 | viewMain _ = 72 | Html.main_ [ Html.Attributes.class "bg-red-400 h-full p-8" ] 73 | [-- viewHeader 74 | -- , viewGame model 75 | -- , viewInformation model 76 | ] 77 | -------------------------------------------------------------------------------- /assets/elm/src/NotFound.elm: -------------------------------------------------------------------------------- 1 | module NotFound exposing (view) 2 | 3 | -- IMPORTS 4 | 5 | import Browser 6 | import Html 7 | import Html.Attributes 8 | import Util.View 9 | 10 | 11 | 12 | -- VIEW 13 | 14 | 15 | view : Browser.Document msg 16 | view = 17 | { title = "Page Not Found" 18 | , body = 19 | [ Html.main_ [ Html.Attributes.class "flex flex-col min-h-screen" ] 20 | [ Html.p [ Html.Attributes.class "flex-grow font-bold my-64 text-center" ] 21 | [ Html.text "Page not found. Return from whence ye came! ⚔️" ] 22 | , Util.View.footer 23 | ] 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /assets/elm/src/Pong.elm: -------------------------------------------------------------------------------- 1 | module Pong exposing 2 | ( Model 3 | , Msg(..) 4 | , init 5 | , subscriptions 6 | , update 7 | , view 8 | ) 9 | 10 | -- IMPORTS 11 | 12 | import Browser exposing (Document) 13 | import Browser.Events 14 | import Html exposing (Html) 15 | import Html.Attributes 16 | import Json.Decode 17 | import Json.Encode 18 | import Pong.Ball exposing (Ball, BallPath, ShowBallPath) 19 | import Pong.Game exposing (State, Winner, WinningScore) 20 | import Pong.Paddle exposing (Direction, Paddle) 21 | import Pong.Window exposing (Window, WindowEdge) 22 | import Random exposing (Generator) 23 | import Set 24 | import Svg exposing (Svg) 25 | import Svg.Attributes 26 | import Util.Fps exposing (ShowFps, Time) 27 | import Util.Keyboard exposing (Controls) 28 | import Util.Ports 29 | import Util.View 30 | 31 | 32 | 33 | -- MODEL 34 | 35 | 36 | type alias Model = 37 | { ball : Ball 38 | , ballPath : BallPath 39 | , deltaTimes : List Time 40 | , gameState : State 41 | , leftPaddle : Paddle 42 | , playerKeyPress : Controls 43 | , rightPaddle : Paddle 44 | , showBallPath : ShowBallPath 45 | , showFps : ShowFps 46 | , winner : Winner 47 | , winningScore : WinningScore 48 | } 49 | 50 | 51 | 52 | -- INIT 53 | 54 | 55 | initialModel : Model 56 | initialModel = 57 | { ball = Pong.Ball.initialBall 58 | , ballPath = Pong.Ball.initialBallPath 59 | , deltaTimes = Util.Fps.initialDeltaTimes 60 | , gameState = Pong.Game.initialState 61 | , leftPaddle = Pong.Paddle.initialLeftPaddle 62 | , playerKeyPress = Util.Keyboard.initialKeys 63 | , rightPaddle = Pong.Paddle.initialRightPaddle 64 | , showBallPath = Pong.Ball.initialShowBallPath 65 | , showFps = Util.Fps.initialShowFps 66 | , winner = Pong.Game.initialWinner 67 | , winningScore = Pong.Game.initialWinningScore 68 | } 69 | 70 | 71 | initialCommand : Cmd Msg 72 | initialCommand = 73 | Cmd.none 74 | 75 | 76 | init : () -> ( Model, Cmd Msg ) 77 | init _ = 78 | ( initialModel, initialCommand ) 79 | 80 | 81 | 82 | -- UPDATE 83 | 84 | 85 | type Msg 86 | = BrowserAdvancedAnimationFrame Time 87 | | CollisionGeneratedRandomBallYPositionAndYVelocity ( Float, Float ) 88 | | PlayerClickedShowBallPathRadioButton ShowBallPath 89 | | PlayerClickedShowFpsRadioButton ShowFps 90 | | PlayerClickedWinningScoreRadioButton WinningScore 91 | | PlayerPressedKeyDown String 92 | | PlayerReleasedKey String 93 | 94 | 95 | update : Msg -> Model -> ( Model, Cmd Msg ) 96 | update msg model = 97 | case msg of 98 | BrowserAdvancedAnimationFrame deltaTime -> 99 | let 100 | windowEdgeHitByBall = 101 | Pong.Window.getWindowEdgeHitByBall model.ball Pong.Window.globalWindow 102 | 103 | paddleHitByBall = 104 | Pong.Paddle.getPaddleHitByBall model.ball model.leftPaddle model.rightPaddle 105 | 106 | winner = 107 | Pong.Game.getWinner model.leftPaddle model.rightPaddle model.winningScore 108 | 109 | leftPaddleDirection = 110 | Pong.Paddle.playerKeyPressToDirection model.playerKeyPress 111 | in 112 | model 113 | |> updateBall model.ball paddleHitByBall windowEdgeHitByBall deltaTime 114 | |> updateBallPath model.ball model.ballPath windowEdgeHitByBall 115 | |> updateDeltaTimes model.showFps deltaTime 116 | |> updatePaddle model.leftPaddle leftPaddleDirection model.ball Pong.Window.globalWindow deltaTime 117 | |> updatePaddle model.rightPaddle Nothing model.ball Pong.Window.globalWindow deltaTime 118 | |> updatePaddleScores windowEdgeHitByBall 119 | |> updateWinner winner 120 | |> updateGameState model.gameState winner 121 | |> addCommand paddleHitByBall windowEdgeHitByBall 122 | 123 | CollisionGeneratedRandomBallYPositionAndYVelocity ( randomYPosition, randomYVelocity ) -> 124 | ( { model | ball = updateBallWithRandomness randomYPosition randomYVelocity model.ball }, Cmd.none ) 125 | 126 | PlayerClickedShowBallPathRadioButton showBallPathValue -> 127 | ( { model | showBallPath = showBallPathValue }, Cmd.none ) 128 | 129 | PlayerClickedShowFpsRadioButton showFpsValue -> 130 | ( { model | showFps = showFpsValue }, Cmd.none ) 131 | 132 | PlayerClickedWinningScoreRadioButton winningScoreValue -> 133 | ( { model | winningScore = winningScoreValue }, Cmd.none ) 134 | 135 | PlayerPressedKeyDown key -> 136 | case key of 137 | " " -> 138 | case model.gameState of 139 | Pong.Game.StartingScreen -> 140 | ( updateGameState Pong.Game.PlayingScreen Nothing model, Cmd.none ) 141 | 142 | Pong.Game.PlayingScreen -> 143 | ( model, Cmd.none ) 144 | 145 | Pong.Game.EndingScreen -> 146 | ( updateGameState Pong.Game.StartingScreen Nothing initialModel, Cmd.none ) 147 | 148 | _ -> 149 | ( updateKeyPress key model, Cmd.none ) 150 | 151 | PlayerReleasedKey _ -> 152 | ( { model | playerKeyPress = Set.empty }, Cmd.none ) 153 | 154 | 155 | 156 | -- UPDATES 157 | 158 | 159 | updateBall : Ball -> Maybe Paddle -> Maybe WindowEdge -> Time -> Model -> Model 160 | updateBall ball maybePaddle maybeWindowEdge deltaTime model = 161 | { model | ball = updateBallWithCollisions ball maybePaddle maybeWindowEdge deltaTime } 162 | 163 | 164 | updateBallWithCollisions : Ball -> Maybe Paddle -> Maybe WindowEdge -> Time -> Ball 165 | updateBallWithCollisions ball maybePaddle maybeWindowEdge deltaTime = 166 | let 167 | ballSpeedChangeAfterCollision = 168 | 50 169 | 170 | ballAngleChangeMultiplier = 171 | 6 172 | 173 | limitBallSpeedChange = 174 | clamp -650 650 175 | in 176 | case ( maybePaddle, maybeWindowEdge ) of 177 | ( Just paddle, Nothing ) -> 178 | case paddle.id of 179 | Pong.Paddle.Left -> 180 | { ball 181 | | x = ball.x + ball.width 182 | , vx = limitBallSpeedChange <| negate <| ball.vx - ballSpeedChangeAfterCollision 183 | , vy = ballAngleChangeMultiplier * Pong.Paddle.getPaddleHitByBallDistanceFromCenter ball paddle 184 | } 185 | 186 | Pong.Paddle.Right -> 187 | { ball 188 | | x = ball.x - ball.width 189 | , vx = limitBallSpeedChange <| negate <| ball.vx + ballSpeedChangeAfterCollision 190 | , vy = ballAngleChangeMultiplier * Pong.Paddle.getPaddleHitByBallDistanceFromCenter ball paddle 191 | } 192 | 193 | ( Nothing, Just edge ) -> 194 | case edge of 195 | Pong.Window.Bottom -> 196 | { ball 197 | | y = ball.y - ball.height 198 | , vy = negate ball.vy 199 | } 200 | 201 | Pong.Window.Left -> 202 | { ball 203 | | x = Pong.Ball.initialBall.x + 100 204 | , vx = negate Pong.Ball.initialBall.vx 205 | , vy = Pong.Ball.initialBall.vy 206 | } 207 | 208 | Pong.Window.Right -> 209 | { ball 210 | | x = Pong.Ball.initialBall.x - 100 211 | , vx = Pong.Ball.initialBall.vx 212 | , vy = Pong.Ball.initialBall.vy 213 | } 214 | 215 | Pong.Window.Top -> 216 | { ball 217 | | y = ball.y + ball.height 218 | , vy = negate ball.vy 219 | } 220 | 221 | ( Just paddle, Just _ ) -> 222 | case paddle.id of 223 | Pong.Paddle.Left -> 224 | { ball 225 | | x = ball.x + ball.width 226 | , vx = limitBallSpeedChange <| negate <| ball.vx - ballSpeedChangeAfterCollision 227 | } 228 | 229 | Pong.Paddle.Right -> 230 | { ball 231 | | x = ball.x - ball.width 232 | , vx = limitBallSpeedChange <| negate <| ball.vx + ballSpeedChangeAfterCollision 233 | } 234 | 235 | ( Nothing, Nothing ) -> 236 | { ball 237 | | x = ball.x + ball.vx * deltaTime 238 | , y = ball.y + ball.vy * deltaTime 239 | } 240 | 241 | 242 | updateBallWithRandomness : Float -> Float -> Ball -> Ball 243 | updateBallWithRandomness y vy ball = 244 | { ball 245 | | y = y 246 | , vy = vy 247 | } 248 | 249 | 250 | updateBallPath : Ball -> BallPath -> Maybe WindowEdge -> Model -> Model 251 | updateBallPath ball ballPath maybeWindowEdge model = 252 | case model.showBallPath of 253 | Pong.Ball.Off -> 254 | model 255 | 256 | Pong.Ball.On -> 257 | case maybeWindowEdge of 258 | Just Pong.Window.Left -> 259 | { model | ballPath = [] } 260 | 261 | Just Pong.Window.Right -> 262 | { model | ballPath = [] } 263 | 264 | _ -> 265 | { model | ballPath = List.take 99 <| ball :: ballPath } 266 | 267 | 268 | updateDeltaTimes : ShowFps -> Time -> Model -> Model 269 | updateDeltaTimes showFps deltaTime model = 270 | case showFps of 271 | Util.Fps.Off -> 272 | model 273 | 274 | Util.Fps.On -> 275 | { model | deltaTimes = List.take 50 (deltaTime :: model.deltaTimes) } 276 | 277 | 278 | updateGameState : State -> Maybe Paddle -> Model -> Model 279 | updateGameState gameState maybePaddle model = 280 | case maybePaddle of 281 | Just _ -> 282 | { model | gameState = Pong.Game.EndingScreen } 283 | 284 | Nothing -> 285 | { model | gameState = gameState } 286 | 287 | 288 | updateKeyPress : String -> Model -> Model 289 | updateKeyPress key model = 290 | if Set.member key Util.Keyboard.validKeys then 291 | { model | playerKeyPress = Set.insert key model.playerKeyPress } 292 | 293 | else 294 | model 295 | 296 | 297 | updatePaddle : Paddle -> Maybe Direction -> Ball -> Window -> Time -> Model -> Model 298 | updatePaddle paddle maybeDirection ball window deltaTime model = 299 | case paddle.id of 300 | Pong.Paddle.Left -> 301 | { model 302 | | leftPaddle = 303 | paddle 304 | |> Pong.Paddle.updateLeftPaddle maybeDirection ball deltaTime 305 | |> Pong.Paddle.updateYWithinWindow window 306 | } 307 | 308 | Pong.Paddle.Right -> 309 | { model 310 | | rightPaddle = 311 | paddle 312 | |> Pong.Paddle.updateRightPaddle ball deltaTime 313 | |> Pong.Paddle.updateYWithinWindow window 314 | } 315 | 316 | 317 | updatePaddleScores : Maybe WindowEdge -> Model -> Model 318 | updatePaddleScores maybeWindowEdge model = 319 | case maybeWindowEdge of 320 | Just Pong.Window.Left -> 321 | { model | rightPaddle = Pong.Paddle.updateScore model.rightPaddle } 322 | 323 | Just Pong.Window.Right -> 324 | { model | leftPaddle = Pong.Paddle.updateScore model.leftPaddle } 325 | 326 | _ -> 327 | model 328 | 329 | 330 | updateWinner : Maybe Paddle -> Model -> Model 331 | updateWinner maybePaddle model = 332 | { model | winner = maybePaddle } 333 | 334 | 335 | 336 | -- COMMANDS 337 | 338 | 339 | addCommand : Maybe Paddle -> Maybe WindowEdge -> Model -> ( Model, Cmd Msg ) 340 | addCommand maybePaddle maybeWindowEdge model = 341 | case ( maybePaddle, maybeWindowEdge ) of 342 | ( Just _, Nothing ) -> 343 | ( model, playSoundCommand "beep.wav" ) 344 | 345 | ( Nothing, Just edge ) -> 346 | case edge of 347 | Pong.Window.Bottom -> 348 | ( model, playSoundCommand "boop.wav" ) 349 | 350 | Pong.Window.Left -> 351 | ( model, generateRandomBallPosition ) 352 | 353 | Pong.Window.Right -> 354 | ( model, generateRandomBallPosition ) 355 | 356 | Pong.Window.Top -> 357 | ( model, playSoundCommand "boop.wav" ) 358 | 359 | ( _, _ ) -> 360 | ( model, Cmd.none ) 361 | 362 | 363 | randomBallYPositionGenerator : Generator Float 364 | randomBallYPositionGenerator = 365 | Random.float 366 | (Pong.Window.globalWindow.y + 100.0) 367 | (Pong.Window.globalWindow.height - 100.0) 368 | 369 | 370 | randomBallYVelocityGenerator : Generator Float 371 | randomBallYVelocityGenerator = 372 | Random.float (negate Pong.Ball.initialBall.vy) Pong.Ball.initialBall.vy 373 | 374 | 375 | randomBallPositionAndVelocity : Generator ( Float, Float ) 376 | randomBallPositionAndVelocity = 377 | Random.pair randomBallYPositionGenerator randomBallYVelocityGenerator 378 | 379 | 380 | generateRandomBallPosition : Cmd Msg 381 | generateRandomBallPosition = 382 | Random.generate CollisionGeneratedRandomBallYPositionAndYVelocity randomBallPositionAndVelocity 383 | 384 | 385 | playSoundCommand : String -> Cmd Msg 386 | playSoundCommand soundFile = 387 | Util.Ports.playSound <| Json.Encode.string soundFile 388 | 389 | 390 | 391 | -- SUBSCRIPTIONS 392 | 393 | 394 | subscriptions : Model -> Sub Msg 395 | subscriptions model = 396 | Sub.batch 397 | [ browserAnimationSubscription model.gameState 398 | , keyDownSubscription 399 | , keyUpSubscription 400 | ] 401 | 402 | 403 | browserAnimationSubscription : State -> Sub Msg 404 | browserAnimationSubscription gameState = 405 | case gameState of 406 | Pong.Game.StartingScreen -> 407 | Sub.none 408 | 409 | Pong.Game.PlayingScreen -> 410 | Browser.Events.onAnimationFrameDelta <| handleAnimationFrames 411 | 412 | Pong.Game.EndingScreen -> 413 | Sub.none 414 | 415 | 416 | handleAnimationFrames : Time -> Msg 417 | handleAnimationFrames milliseconds = 418 | BrowserAdvancedAnimationFrame <| milliseconds / 1000 419 | 420 | 421 | keyDownSubscription : Sub Msg 422 | keyDownSubscription = 423 | Browser.Events.onKeyDown <| Json.Decode.map PlayerPressedKeyDown <| Util.Keyboard.keyDecoder 424 | 425 | 426 | keyUpSubscription : Sub Msg 427 | keyUpSubscription = 428 | Browser.Events.onKeyUp <| Json.Decode.map PlayerReleasedKey <| Util.Keyboard.keyDecoder 429 | 430 | 431 | 432 | -- VIEW 433 | 434 | 435 | view : (Msg -> msg) -> Model -> Document msg 436 | view msg model = 437 | { title = "\u{1F3D3} Pong" 438 | , body = List.map (Html.map msg) [ viewMain model, Util.View.footer ] 439 | } 440 | 441 | 442 | viewMain : Model -> Html Msg 443 | viewMain model = 444 | Html.main_ [ Html.Attributes.class "bg-yellow-200 h-full p-8" ] 445 | [ viewHeader 446 | , viewGame model 447 | , viewInformation model 448 | ] 449 | 450 | 451 | viewHeader : Html msg 452 | viewHeader = 453 | Html.header [ Html.Attributes.class "flex justify-center" ] 454 | [ logo ] 455 | 456 | 457 | logo : Svg msg 458 | logo = 459 | Svg.svg 460 | [ Svg.Attributes.version "1.0" 461 | , Svg.Attributes.width "400" 462 | , Svg.Attributes.height "75" 463 | , Svg.Attributes.viewBox "0 0 400 75" 464 | ] 465 | [ Svg.g 466 | [ Svg.Attributes.transform "translate(75,75) scale(0.03,-0.03)" 467 | , Svg.Attributes.fill "black" 468 | ] 469 | [ Svg.path [ Svg.Attributes.d "M2785 2319 c-356 -51 -684 -291 -840 -613 -206 -425 -123 -920 210 -1251 239 -238 582 -357 937 -325 298 26 520 124 715 313 184 179 285 388 313 652 23 222 -18 443 -118 639 -60 116 -114 187 -221 288 -172 161 -400 265 -651 298 -90 11 -262 11 -345 -1z m390 -439 c240 -75 419 -250 501 -490 27 -78 27 -282 0 -360 -38 -112 -88 -192 -171 -275 -84 -84 -154 -131 -255 -172 -399 -161 -844 61 -957 477 -28 103 -23 264 10 365 78 236 274 414 520 470 89 21 262 13 352 -15z" ] [] 470 | , Svg.path [ Svg.Attributes.d "M5108 2320 c-288 -46 -512 -238 -578 -496 -19 -74 -20 -114 -20 -891 l0 -813 220 0 220 0 2 803 3 802 22 41 c50 94 146 153 262 162 137 9 247 -38 306 -133 l30 -48 5 -811 5 -811 220 0 220 0 0 810 0 810 -23 75 c-71 229 -266 404 -534 480 -84 24 -269 34 -360 20z" ] [] 471 | , Svg.path [ Svg.Attributes.d "M7370 2304 c-153 -26 -235 -52 -355 -112 -129 -64 -218 -125 -313 -216 -238 -228 -344 -497 -329 -836 15 -335 180 -647 450 -851 141 -106 318 -182 489 -209 107 -17 379 -8 478 17 l75 18 3 553 2 552 -330 0 -330 0 0 -200 0 -200 100 0 100 0 0 -166 0 -167 -51 7 c-189 25 -394 209 -486 436 -74 183 -65 348 26 535 81 165 226 292 401 351 161 54 340 54 487 -1 36 -14 69 -25 74 -25 5 0 9 104 9 230 l0 230 -27 11 c-73 28 -159 40 -298 44 -82 2 -161 2 -175 -1z" ] [] 472 | , Svg.path [ Svg.Attributes.d "M122 1203 l3 -1088 215 0 215 0 3 406 2 406 173 6 c195 6 251 17 372 76 176 87 310 249 360 437 25 92 30 235 12 332 -36 188 -156 346 -327 429 -154 75 -155 76 -618 80 l-412 5 2 -1089z m783 686 c55 -15 137 -100 158 -164 46 -134 -14 -285 -143 -362 l-54 -33 -148 0 -148 0 0 278 c0 153 3 282 7 285 11 11 285 8 328 -4z" ] [] 473 | ] 474 | ] 475 | 476 | 477 | viewGame : Model -> Html Msg 478 | viewGame model = 479 | Html.section [ Html.Attributes.class "flex justify-center my-4" ] 480 | [ viewSvg Pong.Window.globalWindow model ] 481 | 482 | 483 | viewSvg : Window -> Model -> Svg msg 484 | viewSvg window model = 485 | let 486 | leftPaddleScoreOffset = 487 | -200.0 488 | 489 | rightPaddleScoreOffset = 490 | 150.0 491 | 492 | viewBoxString = 493 | [ window.x 494 | , window.y 495 | , window.width 496 | , window.height 497 | ] 498 | |> List.map String.fromFloat 499 | |> String.join " " 500 | in 501 | Svg.svg 502 | [ Svg.Attributes.viewBox viewBoxString 503 | , Svg.Attributes.width <| String.fromFloat window.width 504 | , Svg.Attributes.height <| String.fromFloat window.height 505 | ] 506 | [ Pong.Window.viewGameWindow window 507 | , Pong.Window.viewNet window 508 | , Pong.Paddle.viewPaddleScore model.leftPaddle.score window leftPaddleScoreOffset 509 | , Pong.Paddle.viewPaddleScore model.rightPaddle.score window rightPaddleScoreOffset 510 | , Pong.Paddle.viewPaddle model.leftPaddle 511 | , Pong.Paddle.viewPaddle model.rightPaddle 512 | , Pong.Ball.viewBall model.ball 513 | , Pong.Ball.viewBallPath model.showBallPath model.ballPath |> Svg.g [] 514 | , Util.Fps.viewFps model.showFps model.deltaTimes 515 | ] 516 | 517 | 518 | 519 | -- VIEW INFO 520 | 521 | 522 | viewInformation : Model -> Html Msg 523 | viewInformation model = 524 | Html.section [] 525 | [ viewWinner model.gameState model.winner 526 | , viewInstructions 527 | , viewOptions model.showBallPath model.showFps model.winningScore 528 | ] 529 | 530 | 531 | 532 | -- WINNER 533 | 534 | 535 | viewWinner : State -> Maybe Paddle -> Html msg 536 | viewWinner gameState maybePaddle = 537 | case gameState of 538 | Pong.Game.StartingScreen -> 539 | Html.span [] [] 540 | 541 | Pong.Game.PlayingScreen -> 542 | Html.span [] [] 543 | 544 | Pong.Game.EndingScreen -> 545 | Html.div [ Html.Attributes.class "pt-4 text-center" ] 546 | [ Html.h2 [ Html.Attributes.class "font-extrabold font-gray-800 pb-1 text-xl" ] 547 | [ Html.text "Winner!" ] 548 | , viewWinnerPaddle maybePaddle 549 | ] 550 | 551 | 552 | viewWinnerPaddle : Maybe Paddle -> Html msg 553 | viewWinnerPaddle maybePaddle = 554 | case maybePaddle of 555 | Just paddle -> 556 | Html.div [] 557 | [ Html.p [] 558 | [ Html.text <| "\u{1F947} " ++ Pong.Paddle.paddleIdToString paddle.id ++ " paddle wins!" ] 559 | , Html.p [] 560 | [ Html.text "🆕 Press the SPACEBAR key to reset the game." ] 561 | ] 562 | 563 | Nothing -> 564 | Html.span [] [] 565 | 566 | 567 | 568 | -- INSTRUCTIONS 569 | 570 | 571 | viewInstructions : Html msg 572 | viewInstructions = 573 | Html.div [ Html.Attributes.class "pt-4" ] 574 | [ Html.h2 [ Html.Attributes.class "font-extrabold font-gray-800 pb-1 text-center text-xl" ] 575 | [ Html.text "Instructions" ] 576 | , Html.div [ Html.Attributes.class "flex justify-center" ] 577 | [ Html.ul [ Html.Attributes.class "leading-relaxed list-disc list-inside mx-3" ] 578 | [ Html.li [] [ Html.text "\u{1F3D3} Press the SPACEBAR key to serve the ball." ] 579 | , Html.li [] [ Html.text "⌨️ Use the arrow keys to move the left paddle." ] 580 | , Html.li [] [ Html.text "🏆 Avoid missing ball for high score." ] 581 | ] 582 | ] 583 | ] 584 | 585 | 586 | 587 | -- OPTIONS 588 | 589 | 590 | viewOptions : ShowBallPath -> ShowFps -> WinningScore -> Html Msg 591 | viewOptions showBallPath_ showFps winningScore = 592 | Html.div [ Html.Attributes.class "pt-4" ] 593 | [ Html.h2 [ Html.Attributes.class "font-extrabold font-gray-800 pb-1 text-center text-xl" ] 594 | [ Html.text "Options" ] 595 | , Html.form [ Html.Attributes.class "flex justify-center" ] 596 | [ Html.ul [ Html.Attributes.class "leading-relaxed list-disc list-inside mx-3" ] 597 | [ Html.li [] [ viewShowBallPathOptions showBallPath_ ] 598 | , Html.li [] [ viewShowFpsOptions showFps ] 599 | , Html.li [] [ viewWinningScoreOptions winningScore ] 600 | ] 601 | ] 602 | ] 603 | 604 | 605 | viewShowBallPathOptions : ShowBallPath -> Html Msg 606 | viewShowBallPathOptions showBallPath_ = 607 | Html.fieldset [ Html.Attributes.class "inline" ] 608 | [ Html.span [ Html.Attributes.class "mr-3" ] 609 | [ Html.text "Show ball path history:" ] 610 | , Util.View.radioButton Pong.Ball.Off showBallPath_ Pong.Ball.showBallPathToString PlayerClickedShowBallPathRadioButton 611 | , Util.View.radioButton Pong.Ball.On showBallPath_ Pong.Ball.showBallPathToString PlayerClickedShowBallPathRadioButton 612 | ] 613 | 614 | 615 | viewShowFpsOptions : ShowFps -> Html Msg 616 | viewShowFpsOptions showFps_ = 617 | Html.fieldset [ Html.Attributes.class "inline" ] 618 | [ Html.span [ Html.Attributes.class "mr-3" ] 619 | [ Html.text "Show FPS meter:" ] 620 | , Util.View.radioButton Util.Fps.Off showFps_ Util.Fps.showFpsToString PlayerClickedShowFpsRadioButton 621 | , Util.View.radioButton Util.Fps.On showFps_ Util.Fps.showFpsToString PlayerClickedShowFpsRadioButton 622 | ] 623 | 624 | 625 | viewWinningScoreOptions : WinningScore -> Html Msg 626 | viewWinningScoreOptions winningScore = 627 | Html.fieldset [ Html.Attributes.class "inline" ] 628 | [ Html.span [ Html.Attributes.class "mr-3" ] 629 | [ Html.text "Set winning score:" ] 630 | , Util.View.radioButton Pong.Game.Eleven winningScore Pong.Game.winningScoreToString PlayerClickedWinningScoreRadioButton 631 | , Util.View.radioButton Pong.Game.Fifteen winningScore Pong.Game.winningScoreToString PlayerClickedWinningScoreRadioButton 632 | ] 633 | -------------------------------------------------------------------------------- /assets/elm/src/Pong/Ball.elm: -------------------------------------------------------------------------------- 1 | module Pong.Ball exposing 2 | ( Ball 3 | , BallPath 4 | , ShowBallPath(..) 5 | , initialBall 6 | , initialBallPath 7 | , initialShowBallPath 8 | , showBallPathToString 9 | , viewBall 10 | , viewBallPath 11 | ) 12 | 13 | -- IMPORTS 14 | 15 | import Svg exposing (Svg) 16 | import Svg.Attributes 17 | 18 | 19 | 20 | -- MODEL 21 | 22 | 23 | type alias Ball = 24 | { color : String 25 | , x : Float 26 | , y : Float 27 | , vx : Float 28 | , vy : Float 29 | , width : Float 30 | , height : Float 31 | } 32 | 33 | 34 | type alias BallPath = 35 | List Ball 36 | 37 | 38 | type ShowBallPath 39 | = Off 40 | | On 41 | 42 | 43 | 44 | -- INIT 45 | 46 | 47 | initialBall : Ball 48 | initialBall = 49 | { color = "white" 50 | , x = 395.0 51 | , y = 310.0 52 | , vx = 350.0 53 | , vy = 350.0 54 | , width = 10.0 55 | , height = 10.0 56 | } 57 | 58 | 59 | initialBallPath : BallPath 60 | initialBallPath = 61 | [] 62 | 63 | 64 | initialShowBallPath : ShowBallPath 65 | initialShowBallPath = 66 | On 67 | 68 | 69 | 70 | -- HELPERS 71 | 72 | 73 | showBallPathToString : ShowBallPath -> String 74 | showBallPathToString showBallPath = 75 | case showBallPath of 76 | On -> 77 | "On" 78 | 79 | Off -> 80 | "Off" 81 | 82 | 83 | 84 | -- VIEW 85 | 86 | 87 | viewBall : Ball -> Svg msg 88 | viewBall ball = 89 | Svg.rect 90 | [ Svg.Attributes.fill <| ball.color 91 | , Svg.Attributes.x <| String.fromFloat ball.x 92 | , Svg.Attributes.y <| String.fromFloat ball.y 93 | , Svg.Attributes.width <| String.fromFloat ball.width 94 | , Svg.Attributes.height <| String.fromFloat ball.height 95 | ] 96 | [] 97 | 98 | 99 | viewBallPath : ShowBallPath -> BallPath -> List (Svg msg) 100 | viewBallPath showBallPath ballPath = 101 | case showBallPath of 102 | On -> 103 | List.indexedMap viewBallPathSegment ballPath 104 | 105 | Off -> 106 | [] 107 | 108 | 109 | viewBallPathSegment : Int -> Ball -> Svg msg 110 | viewBallPathSegment index ball = 111 | Svg.rect 112 | [ Svg.Attributes.fillOpacity <| String.fromFloat <| 0.01 * toFloat (80 - index) 113 | , Svg.Attributes.fill <| "darkorange" 114 | , Svg.Attributes.x <| String.fromFloat ball.x 115 | , Svg.Attributes.y <| String.fromFloat ball.y 116 | , Svg.Attributes.width <| String.fromFloat ball.width 117 | , Svg.Attributes.height <| String.fromFloat ball.height 118 | ] 119 | [] 120 | -------------------------------------------------------------------------------- /assets/elm/src/Pong/Game.elm: -------------------------------------------------------------------------------- 1 | module Pong.Game exposing 2 | ( State(..) 3 | , Winner 4 | , WinningScore(..) 5 | , getWinner 6 | , initialState 7 | , initialWinner 8 | , initialWinningScore 9 | , winningScoreToInt 10 | , winningScoreToString 11 | ) 12 | 13 | -- IMPORTS 14 | 15 | import Pong.Paddle exposing (Paddle) 16 | 17 | 18 | 19 | -- MODEL 20 | 21 | 22 | type State 23 | = StartingScreen 24 | | PlayingScreen 25 | | EndingScreen 26 | 27 | 28 | type alias Winner = 29 | Maybe Paddle 30 | 31 | 32 | type WinningScore 33 | = Eleven 34 | | Fifteen 35 | 36 | 37 | 38 | -- INIT 39 | 40 | 41 | initialState : State 42 | initialState = 43 | StartingScreen 44 | 45 | 46 | initialWinner : Winner 47 | initialWinner = 48 | Nothing 49 | 50 | 51 | initialWinningScore : WinningScore 52 | initialWinningScore = 53 | Eleven 54 | 55 | 56 | 57 | -- GET 58 | 59 | 60 | getWinner : Paddle -> Paddle -> WinningScore -> Maybe Paddle 61 | getWinner leftPaddle rightPaddle winningScore = 62 | if leftPaddle.score == winningScoreToInt winningScore then 63 | Just leftPaddle 64 | 65 | else if rightPaddle.score == winningScoreToInt winningScore then 66 | Just rightPaddle 67 | 68 | else 69 | Nothing 70 | 71 | 72 | 73 | -- HELPERS 74 | 75 | 76 | winningScoreToInt : WinningScore -> Int 77 | winningScoreToInt winningScore = 78 | case winningScore of 79 | Eleven -> 80 | 11 81 | 82 | Fifteen -> 83 | 15 84 | 85 | 86 | winningScoreToString : WinningScore -> String 87 | winningScoreToString winningScore = 88 | case winningScore of 89 | Eleven -> 90 | "11" 91 | 92 | Fifteen -> 93 | "15" 94 | -------------------------------------------------------------------------------- /assets/elm/src/Pong/Paddle.elm: -------------------------------------------------------------------------------- 1 | module Pong.Paddle exposing 2 | ( Direction(..) 3 | , Paddle 4 | , PaddleId(..) 5 | , getPaddleHitByBall 6 | , getPaddleHitByBallDistanceFromCenter 7 | , initialLeftPaddle 8 | , initialRightPaddle 9 | , paddleIdToString 10 | , playerKeyPressToDirection 11 | , updateLeftPaddle 12 | , updateRightPaddle 13 | , updateScore 14 | , updateYWithinWindow 15 | , viewPaddle 16 | , viewPaddleScore 17 | ) 18 | 19 | -- IMPORTS 20 | 21 | import Pong.Ball exposing (Ball) 22 | import Pong.Window exposing (Window) 23 | import Set exposing (Set) 24 | import Svg exposing (Svg) 25 | import Svg.Attributes 26 | import Util.Keyboard 27 | 28 | 29 | 30 | -- MODEL 31 | 32 | 33 | type Direction 34 | = Down 35 | | Up 36 | 37 | 38 | type alias Paddle = 39 | { color : String 40 | , id : PaddleId 41 | , score : Int 42 | , x : Float 43 | , y : Float 44 | , vy : Float 45 | , width : Float 46 | , height : Float 47 | } 48 | 49 | 50 | type PaddleId 51 | = Left 52 | | Right 53 | 54 | 55 | 56 | -- INIT 57 | 58 | 59 | initialLeftPaddle : Paddle 60 | initialLeftPaddle = 61 | { color = "lightblue" 62 | , id = Left 63 | , score = 0 64 | , x = 48.0 65 | , y = 200.0 66 | , vy = 600.0 67 | , width = 10.0 68 | , height = 60.0 69 | } 70 | 71 | 72 | initialRightPaddle : Paddle 73 | initialRightPaddle = 74 | { color = "lightpink" 75 | , id = Right 76 | , score = 0 77 | , x = 740.0 78 | , y = 300.0 79 | , vy = 475.0 80 | , width = 10.0 81 | , height = 60.0 82 | } 83 | 84 | 85 | 86 | -- UPDATE 87 | 88 | 89 | updateScore : Paddle -> Paddle 90 | updateScore paddle = 91 | { paddle | score = paddle.score + 1 } 92 | 93 | 94 | updateLeftPaddle : Maybe Direction -> Ball -> Float -> Paddle -> Paddle 95 | updateLeftPaddle direction _ deltaTime paddle = 96 | case direction of 97 | Just Down -> 98 | { paddle | y = paddle.y + paddle.vy * deltaTime } 99 | 100 | Just Up -> 101 | { paddle | y = paddle.y - paddle.vy * deltaTime } 102 | 103 | Nothing -> 104 | paddle 105 | 106 | 107 | updateRightPaddle : Ball -> Float -> Paddle -> Paddle 108 | updateRightPaddle ball deltaTime paddle = 109 | if ball.y > paddle.y then 110 | { paddle | y = paddle.y + paddle.vy * deltaTime } 111 | 112 | else if ball.y < paddle.y then 113 | { paddle | y = paddle.y - paddle.vy * deltaTime } 114 | 115 | else 116 | paddle 117 | 118 | 119 | updateYWithinWindow : Window -> Paddle -> Paddle 120 | updateYWithinWindow window paddle = 121 | let 122 | topEdge = 123 | window.y 124 | 125 | bottomEdge = 126 | window.height - paddle.height 127 | in 128 | { paddle | y = clamp topEdge bottomEdge paddle.y } 129 | 130 | 131 | 132 | -- COLLISIONS 133 | 134 | 135 | ballHitPaddle : Ball -> Paddle -> Bool 136 | ballHitPaddle ball paddle = 137 | ballHitLeftPaddle ball paddle || ballHitRightPaddle ball paddle 138 | 139 | 140 | ballHitLeftPaddle : Ball -> Paddle -> Bool 141 | ballHitLeftPaddle ball paddle = 142 | (paddle.y <= ball.y && ball.y <= paddle.y + paddle.height) 143 | && (paddle.x <= ball.x && ball.x <= paddle.x + paddle.width) 144 | && (ball.vx < 0) 145 | 146 | 147 | ballHitRightPaddle : Ball -> Paddle -> Bool 148 | ballHitRightPaddle ball paddle = 149 | (paddle.y <= ball.y && ball.y <= paddle.y + paddle.height) 150 | && (paddle.x <= ball.x + ball.width && ball.x <= paddle.x + paddle.width) 151 | && (ball.vx > 0) 152 | 153 | 154 | getPaddleHitByBall : Ball -> Paddle -> Paddle -> Maybe Paddle 155 | getPaddleHitByBall ball leftPaddle rightPaddle = 156 | if ballHitLeftPaddle ball leftPaddle then 157 | Just leftPaddle 158 | 159 | else if ballHitRightPaddle ball rightPaddle then 160 | Just rightPaddle 161 | 162 | else 163 | Nothing 164 | 165 | 166 | getPaddleHitByBallDistanceFromCenter : Ball -> Paddle -> Float 167 | getPaddleHitByBallDistanceFromCenter ball paddle = 168 | if ballHitPaddle ball paddle then 169 | let 170 | paddleCenter = 171 | paddle.height / 2 172 | in 173 | -- -100 for Paddle Top 174 | -- 0 for Paddle Center 175 | -- 100 for Paddle Bottom 176 | (ball.y - paddle.y - paddleCenter) * 100 / paddleCenter 177 | 178 | else 179 | 0 180 | 181 | 182 | 183 | -- HELPERS 184 | 185 | 186 | playerKeyPressToDirection : Set String -> Maybe Direction 187 | playerKeyPressToDirection playerKeyPress = 188 | if Util.Keyboard.playerPressedArrowUpKey playerKeyPress then 189 | Just Up 190 | 191 | else if Util.Keyboard.playerPressedArrowDownKey playerKeyPress then 192 | Just Down 193 | 194 | else 195 | Nothing 196 | 197 | 198 | paddleIdToString : PaddleId -> String 199 | paddleIdToString paddleId = 200 | case paddleId of 201 | Left -> 202 | "Left" 203 | 204 | Right -> 205 | "Right" 206 | 207 | 208 | 209 | -- VIEW 210 | 211 | 212 | viewPaddle : Paddle -> Svg msg 213 | viewPaddle paddle = 214 | Svg.rect 215 | [ Svg.Attributes.fill <| paddle.color 216 | , Svg.Attributes.x <| String.fromFloat paddle.x 217 | , Svg.Attributes.y <| String.fromFloat paddle.y 218 | , Svg.Attributes.width <| String.fromFloat paddle.width 219 | , Svg.Attributes.height <| String.fromFloat paddle.height 220 | ] 221 | [] 222 | 223 | 224 | viewPaddleScore : Int -> Window -> Float -> Svg msg 225 | viewPaddleScore score window positionOffset = 226 | Svg.text_ 227 | [ Svg.Attributes.fill "white" 228 | , Svg.Attributes.fontFamily "monospace" 229 | , Svg.Attributes.fontSize "80" 230 | , Svg.Attributes.fontWeight "bold" 231 | , Svg.Attributes.x <| String.fromFloat <| (window.width / 2) + positionOffset 232 | , Svg.Attributes.y "100" 233 | ] 234 | [ Svg.text <| String.fromInt score ] 235 | -------------------------------------------------------------------------------- /assets/elm/src/Pong/Window.elm: -------------------------------------------------------------------------------- 1 | module Pong.Window exposing 2 | ( Window 3 | , WindowEdge(..) 4 | , getWindowEdgeHitByBall 5 | , globalWindow 6 | , viewGameWindow 7 | , viewNet 8 | ) 9 | 10 | -- IMPORTS 11 | 12 | import Pong.Ball exposing (Ball) 13 | import Svg exposing (Svg) 14 | import Svg.Attributes 15 | 16 | 17 | 18 | -- MODEL 19 | 20 | 21 | type alias Window = 22 | { backgroundColor : String 23 | , x : Float 24 | , y : Float 25 | , width : Float 26 | , height : Float 27 | } 28 | 29 | 30 | type WindowEdge 31 | = Bottom 32 | | Left 33 | | Right 34 | | Top 35 | 36 | 37 | 38 | -- GLOBAL 39 | 40 | 41 | globalWindow : Window 42 | globalWindow = 43 | { backgroundColor = "black" 44 | , x = 0.0 45 | , y = 0.0 46 | , width = 800.0 47 | , height = 600.0 48 | } 49 | 50 | 51 | 52 | -- COLLISIONS 53 | 54 | 55 | getWindowEdgeHitByBall : Ball -> Window -> Maybe WindowEdge 56 | getWindowEdgeHitByBall ball window = 57 | if (ball.y + ball.height) >= window.height then 58 | Just Bottom 59 | 60 | else if (ball.x - ball.width) <= window.x then 61 | Just Left 62 | 63 | else if (ball.x + ball.width) >= window.width then 64 | Just Right 65 | 66 | else if (ball.y - ball.height) <= window.x then 67 | Just Top 68 | 69 | else 70 | Nothing 71 | 72 | 73 | 74 | -- VIEW 75 | 76 | 77 | viewGameWindow : Window -> Svg msg 78 | viewGameWindow window = 79 | Svg.rect 80 | [ Svg.Attributes.fill <| window.backgroundColor 81 | , Svg.Attributes.x <| String.fromFloat window.x 82 | , Svg.Attributes.y <| String.fromFloat window.y 83 | , Svg.Attributes.width <| String.fromFloat window.width 84 | , Svg.Attributes.height <| String.fromFloat window.height 85 | ] 86 | [] 87 | 88 | 89 | viewNet : Window -> Svg msg 90 | viewNet window = 91 | Svg.line 92 | [ Svg.Attributes.stroke "white" 93 | , Svg.Attributes.strokeDasharray "14, 14" 94 | , Svg.Attributes.strokeWidth "4" 95 | , Svg.Attributes.x1 <| String.fromFloat <| (window.width / 2) 96 | , Svg.Attributes.x2 <| String.fromFloat <| (window.width / 2) 97 | , Svg.Attributes.y1 <| String.fromFloat <| window.y 98 | , Svg.Attributes.y2 <| String.fromFloat <| window.height 99 | ] 100 | [] 101 | -------------------------------------------------------------------------------- /assets/elm/src/Route.elm: -------------------------------------------------------------------------------- 1 | module Route exposing (Route(..)) 2 | 3 | -- IMPORTS 4 | 5 | import Adventure 6 | import Breakout 7 | import Mario 8 | import Pong 9 | 10 | 11 | 12 | -- ROUTING 13 | 14 | 15 | type Route 16 | = Adventure Adventure.Model 17 | | Breakout Breakout.Model 18 | | Landing 19 | | Mario Mario.Model 20 | | Pong Pong.Model 21 | | NotFound 22 | -------------------------------------------------------------------------------- /assets/elm/src/Util/Fps.elm: -------------------------------------------------------------------------------- 1 | module Util.Fps exposing 2 | ( ShowFps(..) 3 | , Time 4 | , initialDeltaTimes 5 | , initialShowFps 6 | , showFpsToString 7 | , viewFps 8 | ) 9 | 10 | -- IMPORTS 11 | 12 | import Svg exposing (Svg) 13 | import Svg.Attributes 14 | 15 | 16 | 17 | -- MODEL 18 | 19 | 20 | type alias Time = 21 | Float 22 | 23 | 24 | type ShowFps 25 | = Off 26 | | On 27 | 28 | 29 | 30 | -- INIT 31 | 32 | 33 | initialDeltaTimes : List Time 34 | initialDeltaTimes = 35 | [] 36 | 37 | 38 | initialShowFps : ShowFps 39 | initialShowFps = 40 | On 41 | 42 | 43 | 44 | -- VIEW 45 | 46 | 47 | viewFps : ShowFps -> List Time -> Svg msg 48 | viewFps showFps deltaTimes = 49 | let 50 | average currentWeight sumOfWeights weightedSum list = 51 | case list of 52 | [] -> 53 | weightedSum / sumOfWeights 54 | 55 | head :: tail -> 56 | average 57 | (currentWeight * 0.9) 58 | (currentWeight + sumOfWeights) 59 | (head * currentWeight + weightedSum) 60 | tail 61 | 62 | fps = 63 | String.fromInt <| round <| 1 / average 1 0 0 deltaTimes 64 | in 65 | case showFps of 66 | Off -> 67 | Svg.g [] [] 68 | 69 | On -> 70 | case deltaTimes of 71 | [] -> 72 | Svg.g [] [] 73 | 74 | _ -> 75 | Svg.text_ 76 | [ Svg.Attributes.class "font-retro text-xs" 77 | , Svg.Attributes.x <| String.fromInt 5 78 | , Svg.Attributes.y <| String.fromInt 20 79 | , Svg.Attributes.fill "white" 80 | , Svg.Attributes.fontSize "20" 81 | ] 82 | [ Svg.text <| fps ++ "fps" ] 83 | 84 | 85 | 86 | -- HELPERS 87 | 88 | 89 | showFpsToString : ShowFps -> String 90 | showFpsToString showFps = 91 | case showFps of 92 | On -> 93 | "On" 94 | 95 | Off -> 96 | "Off" 97 | -------------------------------------------------------------------------------- /assets/elm/src/Util/Icon.elm: -------------------------------------------------------------------------------- 1 | module Util.Icon exposing 2 | ( dev 3 | , github 4 | , home 5 | , thumbsUp 6 | , twitter 7 | ) 8 | 9 | -- IMPORTS 10 | 11 | import Svg exposing (Svg) 12 | import Svg.Attributes 13 | 14 | 15 | 16 | -- ICONS 17 | 18 | 19 | dev : Svg a 20 | dev = 21 | Svg.svg 22 | [ Svg.Attributes.class "h-6 mr-2 w-6" 23 | , Svg.Attributes.fill "currentColor" 24 | , Svg.Attributes.viewBox "0 0 512 512" 25 | , Svg.Attributes.width "512" 26 | , Svg.Attributes.height "512" 27 | ] 28 | [ Svg.path 29 | [ Svg.Attributes.d "M0,91.4v329.1h512V91.4H0z M36.6,128h438.9v256H36.6V128z M73.1,164.6v182.9H128c30.2,0,54.9-24.6,54.9-54.9v-73.1\n\t\tc0-30.2-24.6-54.9-54.9-54.9H73.1z M256,164.6c-20.2,0-36.6,16.4-36.6,36.6v109.7c0,20.2,16.4,36.6,36.6,36.6h36.6v-36.6H256v-36.6\n\t\th36.6v-36.6H256v-36.6h36.6v-36.6H256z M323.5,164.6l38.1,165c2.4,10.4,11.7,17.8,22.4,17.8s20-7.4,22.4-17.8l38.1-165H407L384,264\n\t\tl-23-99.5H323.5z M109.7,201.1H128c10.1,0,18.3,8.2,18.3,18.3v73.1c0,10.1-8.2,18.3-18.3,18.3h-18.3V201.1z" ] 30 | [] 31 | ] 32 | 33 | 34 | github : Svg a 35 | github = 36 | Svg.svg 37 | [ Svg.Attributes.class "h-6 mr-2 w-6" 38 | , Svg.Attributes.fill "currentColor" 39 | , Svg.Attributes.viewBox "0 0 440 440" 40 | , Svg.Attributes.width "440" 41 | , Svg.Attributes.height "440" 42 | ] 43 | [ Svg.path 44 | [ Svg.Attributes.d "M409.132,114.573c-19.608-33.596-46.205-60.194-79.798-79.8C295.736,15.166,259.057,5.365,219.271,5.365\n\t\tc-39.781,0-76.472,9.804-110.063,29.408c-33.596,19.605-60.192,46.204-79.8,79.8C9.803,148.168,0,184.854,0,224.63\n\t\tc0,47.78,13.94,90.745,41.827,128.906c27.884,38.164,63.906,64.572,108.063,79.227c5.14,0.954,8.945,0.283,11.419-1.996\n\t\tc2.475-2.282,3.711-5.14,3.711-8.562c0-0.571-0.049-5.708-0.144-15.417c-0.098-9.709-0.144-18.179-0.144-25.406l-6.567,1.136\n\t\tc-4.187,0.767-9.469,1.092-15.846,1c-6.374-0.089-12.991-0.757-19.842-1.999c-6.854-1.231-13.229-4.086-19.13-8.559\n\t\tc-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559\n\t\tc-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-0.951-2.568-2.098-3.711-3.429c-1.142-1.331-1.997-2.663-2.568-3.997\n\t\tc-0.572-1.335-0.098-2.43,1.427-3.289c1.525-0.859,4.281-1.276,8.28-1.276l5.708,0.853c3.807,0.763,8.516,3.042,14.133,6.851\n\t\tc5.614,3.806,10.229,8.754,13.846,14.842c4.38,7.806,9.657,13.754,15.846,17.847c6.184,4.093,12.419,6.136,18.699,6.136\n\t\tc6.28,0,11.704-0.476,16.274-1.423c4.565-0.952,8.848-2.383,12.847-4.285c1.713-12.758,6.377-22.559,13.988-29.41\n\t\tc-10.848-1.14-20.601-2.857-29.264-5.14c-8.658-2.286-17.605-5.996-26.835-11.14c-9.235-5.137-16.896-11.516-22.985-19.126\n\t\tc-6.09-7.614-11.088-17.61-14.987-29.979c-3.901-12.374-5.852-26.648-5.852-42.826c0-23.035,7.52-42.637,22.557-58.817\n\t\tc-7.044-17.318-6.379-36.732,1.997-58.24c5.52-1.715,13.706-0.428,24.554,3.853c10.85,4.283,18.794,7.952,23.84,10.994\n\t\tc5.046,3.041,9.089,5.618,12.135,7.708c17.705-4.947,35.976-7.421,54.818-7.421s37.117,2.474,54.823,7.421l10.849-6.849\n\t\tc7.419-4.57,16.18-8.758,26.262-12.565c10.088-3.805,17.802-4.853,23.134-3.138c8.562,21.509,9.325,40.922,2.279,58.24\n\t\tc15.036,16.18,22.559,35.787,22.559,58.817c0,16.178-1.958,30.497-5.853,42.966c-3.9,12.471-8.941,22.457-15.125,29.979\n\t\tc-6.191,7.521-13.901,13.85-23.131,18.986c-9.232,5.14-18.182,8.85-26.84,11.136c-8.662,2.286-18.415,4.004-29.263,5.146\n\t\tc9.894,8.562,14.842,22.077,14.842,40.539v60.237c0,3.422,1.19,6.279,3.572,8.562c2.379,2.279,6.136,2.95,11.276,1.995\n\t\tc44.163-14.653,80.185-41.062,108.068-79.226c27.88-38.161,41.825-81.126,41.825-128.906\n\t\tC438.536,184.851,428.728,148.168,409.132,114.573z" ] 45 | [] 46 | ] 47 | 48 | 49 | home : Svg a 50 | home = 51 | Svg.svg 52 | [ Svg.Attributes.class "h-6 mr-2 w-6" 53 | , Svg.Attributes.fill "currentColor" 54 | , Svg.Attributes.viewBox "0 0 20 20" 55 | , Svg.Attributes.width "20" 56 | , Svg.Attributes.height "20" 57 | ] 58 | [ Svg.path 59 | [ Svg.Attributes.d "M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" ] 60 | [] 61 | ] 62 | 63 | 64 | thumbsUp : Svg a 65 | thumbsUp = 66 | Svg.svg 67 | [ Svg.Attributes.class "h-6 mr-2 w-6" 68 | , Svg.Attributes.fill "currentColor" 69 | , Svg.Attributes.viewBox "0 0 24 24" 70 | , Svg.Attributes.width "24" 71 | , Svg.Attributes.height "24" 72 | ] 73 | [ Svg.path 74 | [ Svg.Attributes.d "M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z" ] 75 | [] 76 | ] 77 | 78 | 79 | twitter : Svg a 80 | twitter = 81 | Svg.svg 82 | [ Svg.Attributes.class "h-6 mr-2 w-6" 83 | , Svg.Attributes.fill "currentColor" 84 | , Svg.Attributes.viewBox "0 0 24 24" 85 | , Svg.Attributes.width "24" 86 | , Svg.Attributes.height "24" 87 | ] 88 | [ Svg.path 89 | [ Svg.Attributes.d "M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z" ] 90 | [] 91 | ] 92 | -------------------------------------------------------------------------------- /assets/elm/src/Util/Keyboard.elm: -------------------------------------------------------------------------------- 1 | module Util.Keyboard exposing 2 | ( Controls 3 | , initialKeys 4 | , keyDecoder 5 | , playerPressedArrowDownKey 6 | , playerPressedArrowLeftKey 7 | , playerPressedArrowRightKey 8 | , playerPressedArrowUpKey 9 | , playerPressedKey 10 | , playerPressedSpacebarKey 11 | , validKeys 12 | ) 13 | 14 | -- IMPORTS 15 | 16 | import Json.Decode exposing (Decoder) 17 | import Set exposing (Set) 18 | 19 | 20 | 21 | -- MODEL 22 | 23 | 24 | type alias Controls = 25 | Set String 26 | 27 | 28 | 29 | -- INIT 30 | 31 | 32 | initialKeys : Controls 33 | initialKeys = 34 | Set.empty 35 | 36 | 37 | 38 | -- DECODER 39 | 40 | 41 | keyDecoder : Decoder String 42 | keyDecoder = 43 | Json.Decode.field "key" Json.Decode.string 44 | 45 | 46 | 47 | -- VALIDATION 48 | 49 | 50 | validKeys : Set String 51 | validKeys = 52 | Set.empty 53 | |> Set.insert "ArrowDown" 54 | |> Set.insert "ArrowLeft" 55 | |> Set.insert "ArrowRight" 56 | |> Set.insert "ArrowUp" 57 | |> Set.insert " " 58 | 59 | 60 | 61 | -- HELPERS 62 | 63 | 64 | playerPressedKey : Set String -> Bool 65 | playerPressedKey = 66 | Set.isEmpty >> not 67 | 68 | 69 | playerPressedSpacebarKey : Set String -> Bool 70 | playerPressedSpacebarKey = 71 | Set.member " " 72 | 73 | 74 | playerPressedArrowDownKey : Set String -> Bool 75 | playerPressedArrowDownKey = 76 | Set.member "ArrowDown" 77 | 78 | 79 | playerPressedArrowLeftKey : Set String -> Bool 80 | playerPressedArrowLeftKey = 81 | Set.member "ArrowLeft" 82 | 83 | 84 | playerPressedArrowRightKey : Set String -> Bool 85 | playerPressedArrowRightKey = 86 | Set.member "ArrowRight" 87 | 88 | 89 | playerPressedArrowUpKey : Set String -> Bool 90 | playerPressedArrowUpKey = 91 | Set.member "ArrowUp" 92 | -------------------------------------------------------------------------------- /assets/elm/src/Util/List.elm: -------------------------------------------------------------------------------- 1 | module Util.List exposing (flatten) 2 | 3 | 4 | flatten : List (List a) -> List a 5 | flatten = 6 | List.foldr (++) [] 7 | -------------------------------------------------------------------------------- /assets/elm/src/Util/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Util.Ports exposing 2 | ( playMusic 3 | , playSound 4 | ) 5 | 6 | -- IMPORTS 7 | 8 | import Json.Encode exposing (Value) 9 | 10 | 11 | 12 | -- PORTS 13 | 14 | 15 | port playMusic : Value -> Cmd msg 16 | 17 | 18 | port playSound : Value -> Cmd msg 19 | -------------------------------------------------------------------------------- /assets/elm/src/Util/Sound.elm: -------------------------------------------------------------------------------- 1 | module Util.Sound exposing 2 | ( PlayMusic(..) 3 | , initialPlayMusic 4 | , playMusicToBool 5 | , playMusicToString 6 | , stopMusic 7 | ) 8 | 9 | -- IMPORTS 10 | 11 | import Json.Encode 12 | import Util.Ports 13 | 14 | 15 | 16 | -- MODEL 17 | 18 | 19 | type PlayMusic 20 | = On 21 | | Off 22 | 23 | 24 | 25 | -- INIT 26 | 27 | 28 | initialPlayMusic : PlayMusic 29 | initialPlayMusic = 30 | On 31 | 32 | 33 | 34 | -- HELPERS 35 | 36 | 37 | playMusicToBool : PlayMusic -> Bool 38 | playMusicToBool playMusic = 39 | case playMusic of 40 | On -> 41 | True 42 | 43 | Off -> 44 | False 45 | 46 | 47 | playMusicToString : PlayMusic -> String 48 | playMusicToString playMusic = 49 | case playMusic of 50 | On -> 51 | "On" 52 | 53 | Off -> 54 | "Off" 55 | 56 | 57 | stopMusic : Cmd a 58 | stopMusic = 59 | Util.Ports.playMusic <| Json.Encode.object [ ( "play", Json.Encode.bool False ) ] 60 | -------------------------------------------------------------------------------- /assets/elm/src/Util/Vector.elm: -------------------------------------------------------------------------------- 1 | module Util.Vector exposing 2 | ( Vector 3 | , add 4 | , getX 5 | , getY 6 | , negate 7 | , scale 8 | , subtract 9 | ) 10 | 11 | 12 | type alias Vector = 13 | ( Float, Float ) 14 | 15 | 16 | 17 | -- ACCESS 18 | 19 | 20 | getX : Vector -> Float 21 | getX ( x, _ ) = 22 | x 23 | 24 | 25 | getY : Vector -> Float 26 | getY ( _, y ) = 27 | y 28 | 29 | 30 | 31 | -- TRANSFORM 32 | 33 | 34 | add : Vector -> Vector -> Vector 35 | add ( x, y ) ( a, b ) = 36 | ( x + a 37 | , y + b 38 | ) 39 | 40 | 41 | subtract : Vector -> Vector -> Vector 42 | subtract ( x, y ) ( a, b ) = 43 | ( x - a 44 | , y - b 45 | ) 46 | 47 | 48 | negate : Vector -> Vector 49 | negate ( x, y ) = 50 | ( Basics.negate x 51 | , Basics.negate y 52 | ) 53 | 54 | 55 | scale : Float -> Vector -> Vector 56 | scale scaleFactor ( x, y ) = 57 | ( scaleFactor * x 58 | , scaleFactor * y 59 | ) 60 | -------------------------------------------------------------------------------- /assets/elm/src/Util/View.elm: -------------------------------------------------------------------------------- 1 | module Util.View exposing 2 | ( footer 3 | , radioButton 4 | ) 5 | 6 | -- IMPORTS 7 | 8 | import Html exposing (Html) 9 | import Html.Attributes 10 | import Html.Events 11 | import Svg exposing (Svg) 12 | import Util.Icon 13 | 14 | 15 | 16 | -- FOOTER 17 | 18 | 19 | footer : Html a 20 | footer = 21 | Html.footer [ Html.Attributes.class "bg-black bottom-0 font-semibold px-16 py-8 relative sm:px-6 text-md text-white w-full" ] 22 | [ Html.ul [ Html.Attributes.class "flex flex-col sm:justify-evenly sm:flex-row" ] 23 | [ footerItem "Home" "/" Util.Icon.home "pink" 24 | , footerItem "GitHub" "https://github.com/create-with/games" Util.Icon.github "purple" 25 | , footerItem "Twitter" "https://twitter.com/bijanbwb" Util.Icon.twitter "blue" 26 | , footerItem "Articles" "https://dev.to/bijanbwb/" Util.Icon.dev "yellow" 27 | , footerItem "Credits" "https://github.com/create-with/games/blob/master/CREDITS.md" Util.Icon.thumbsUp "teal" 28 | ] 29 | ] 30 | 31 | 32 | footerItem : String -> String -> Svg a -> String -> Html a 33 | footerItem title url icon color = 34 | Html.a 35 | [ Html.Attributes.href url 36 | , if title /= "Home" then 37 | Html.Attributes.target "_blank" 38 | 39 | else 40 | Html.Attributes.target "_self" 41 | ] 42 | [ Html.li [ Html.Attributes.class <| "flex hover:text-" ++ color ++ "-300 justify-center self-center py-4" ] 43 | [ Html.span [ Html.Attributes.class "mr-1 self-center" ] [ icon ] 44 | , Html.span [ Html.Attributes.class "self-center" ] [ Html.text title ] 45 | ] 46 | ] 47 | 48 | 49 | 50 | -- RADIO BUTTON 51 | 52 | 53 | radioButton : a -> a -> (a -> String) -> (a -> msg) -> Html msg 54 | radioButton type_ current toString msg = 55 | Html.label [] 56 | [ Html.input 57 | [ Html.Attributes.checked <| current == type_ 58 | , Html.Attributes.type_ "radio" 59 | , Html.Events.onClick <| msg type_ 60 | ] 61 | [] 62 | , Html.span [ Html.Attributes.class "px-1 text-xs" ] 63 | [ Html.text <| toString type_ ] 64 | ] 65 | -------------------------------------------------------------------------------- /assets/elm/static/screens/01: -------------------------------------------------------------------------------- 1 | [ (0,0),(1,0),(2,0),(3,0),(4,0),(5,0),(6,0),(7,0),(8,0),(9,0),(10,0),(11,0),(12,0),(13,0),(14,0),(15,0),(16,0),(17,0),(18,0),(19,0),(20,0),(21,0),(24,0),(25,0),(28,0),(29,0),(32,0),(33,0),(46,0),(47,0),(50,0),(51,0),(54,0),(55,0),(58,0),(59,0),(60,0),(61,0),(62,0),(63,0),(64,0),(65,0),(66,0),(67,0),(68,0),(69,0),(70,0),(71,0),(72,0),(73,0),(74,0),(75,0),(76,0),(77,0),(78,0),(79,0) 2 | , (0,1),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,1),(10,1),(11,1),(12,1),(13,1),(14,1),(15,1),(16,1),(17,1),(18,1),(19,1),(20,1),(21,1),(24,1),(25,1),(28,1),(29,1),(32,1),(33,1),(46,1),(47,1),(50,1),(51,1),(54,1),(55,1),(58,1),(59,1),(60,1),(61,1),(62,1),(63,1),(64,1),(65,1),(66,1),(67,1),(68,1),(69,1),(70,1),(71,1),(72,1),(73,1),(74,1),(75,1),(76,1),(77,1),(78,1),(79,1) 3 | , (0,2),(1,2),(2,2),(3,2),(4,2),(5,2),(6,2),(7,2),(8,2),(9,2),(10,2),(11,2),(12,2),(13,2),(14,2),(15,2),(16,2),(17,2),(18,2),(19,2),(20,2),(21,2),(24,2),(25,2),(28,2),(29,2),(32,2),(33,2),(46,2),(47,2),(50,2),(51,2),(54,2),(55,2),(58,2),(59,2),(60,2),(61,2),(62,2),(63,2),(64,2),(65,2),(66,2),(67,2),(68,2),(69,2),(70,2),(71,2),(72,2),(73,2),(74,2),(75,2),(76,2),(77,2),(78,2),(79,2) 4 | , (0,3),(1,3),(2,3),(3,3),(4,3),(5,3),(6,3),(7,3),(8,3),(9,3),(10,3),(11,3),(12,3),(13,3),(14,3),(15,3),(16,3),(17,3),(18,3),(19,3),(20,3),(21,3),(24,3),(25,3),(28,3),(29,3),(32,3),(33,3),(46,3),(47,3),(50,3),(51,3),(54,3),(55,3),(58,3),(59,3),(60,3),(61,3),(62,3),(63,3),(64,3),(65,3),(66,3),(67,3),(68,3),(69,3),(70,3),(71,3),(72,3),(73,3),(74,3),(75,3),(76,3),(77,3),(78,3),(79,3) 5 | , (0,4),(1,4),(2,4),(3,4),(4,4),(5,4),(6,4),(7,4),(8,4),(9,4),(10,4),(11,4),(12,4),(13,4),(14,4),(15,4),(16,4),(17,4),(18,4),(19,4),(20,4),(21,4),(24,4),(25,4),(28,4),(29,4),(32,4),(33,4),(46,4),(47,4),(50,4),(51,4),(54,4),(55,4),(58,4),(59,4),(60,4),(61,4),(62,4),(63,4),(64,4),(65,4),(66,4),(67,4),(68,4),(69,4),(70,4),(71,4),(72,4),(73,4),(74,4),(75,4),(76,4),(77,4),(78,4),(79,4) 6 | , (0,5),(1,5),(2,5),(3,5),(20,5),(21,5),(22,5),(23,5),(24,5),(25,5),(26,5),(27,5),(28,5),(29,5),(30,5),(31,5),(32,5),(33,5),(46,5),(47,5),(48,5),(49,5),(50,5),(51,5),(52,5),(53,5),(54,5),(55,5),(56,5),(57,5),(58,5),(59,5),(76,5),(77,5),(78,5),(79,5) 7 | , (0,6),(1,6),(2,6),(3,6),(20,6),(21,6),(22,6),(23,6),(24,6),(25,6),(26,6),(27,6),(28,6),(29,6),(30,6),(31,6),(32,6),(33,6),(46,6),(47,6),(48,6),(49,6),(50,6),(51,6),(52,6),(53,6),(54,6),(55,6),(56,6),(57,6),(58,6),(59,6),(76,6),(77,6),(78,6),(79,6) 8 | , (0,7),(1,7),(2,7),(3,7),(20,7),(21,7),(22,7),(23,7),(24,7),(25,7),(26,7),(27,7),(28,7),(29,7),(30,7),(31,7),(32,7),(33,7),(46,7),(47,7),(48,7),(49,7),(50,7),(51,7),(52,7),(53,7),(54,7),(55,7),(56,7),(57,7),(58,7),(59,7),(76,7),(77,7),(78,7),(79,7) 9 | , (0,8),(1,8),(2,8),(3,8),(20,8),(21,8),(22,8),(23,8),(24,8),(25,8),(26,8),(27,8),(28,8),(29,8),(30,8),(31,8),(32,8),(33,8),(46,8),(47,8),(48,8),(49,8),(50,8),(51,8),(52,8),(53,8),(54,8),(55,8),(56,8),(57,8),(58,8),(59,8),(76,8),(77,8),(78,8),(79,8) 10 | , (0,9),(1,9),(2,9),(3,9),(20,9),(21,9),(22,9),(23,9),(24,9),(25,9),(26,9),(27,9),(28,9),(29,9),(30,9),(31,9),(32,9),(33,9),(46,9),(47,9),(48,9),(49,9),(50,9),(51,9),(52,9),(53,9),(54,9),(55,9),(56,9),(57,9),(58,9),(59,9),(76,9),(77,9),(78,9),(79,9) 11 | , (0,10),(1,10),(2,10),(3,10),(20,10),(21,10),(22,10),(23,10),(24,10),(25,10),(26,10),(27,10),(28,10),(29,10),(30,10),(31,10),(32,10),(33,10),(46,10),(47,10),(48,10),(49,10),(50,10),(51,10),(52,10),(53,10),(54,10),(55,10),(56,10),(57,10),(58,10),(59,10),(76,10),(77,10),(78,10),(79,10) 12 | , (0,11),(1,11),(2,11),(3,11),(20,11),(21,11),(22,11),(23,11),(24,11),(25,11),(26,11),(27,11),(28,11),(29,11),(30,11),(31,11),(32,11),(33,11),(46,11),(47,11),(48,11),(49,11),(50,11),(51,11),(52,11),(53,11),(54,11),(55,11),(56,11),(57,11),(58,11),(59,11),(76,11),(77,11),(78,11),(79,11) 13 | , (0,12),(1,12),(2,12),(3,12),(20,12),(21,12),(22,12),(23,12),(24,12),(25,12),(26,12),(27,12),(28,12),(29,12),(30,12),(31,12),(32,12),(33,12),(46,12),(47,12),(48,12),(49,12),(50,12),(51,12),(52,12),(53,12),(54,12),(55,12),(56,12),(57,12),(58,12),(59,12),(76,12),(77,12),(78,12),(79,12) 14 | , (0,13),(1,13),(2,13),(3,13),(20,13),(21,13),(22,13),(23,13),(24,13),(25,13),(26,13),(27,13),(28,13),(29,13),(30,13),(31,13),(32,13),(33,13),(46,13),(47,13),(48,13),(49,13),(50,13),(51,13),(52,13),(53,13),(54,13),(55,13),(56,13),(57,13),(58,13),(59,13),(76,13),(77,13),(78,13),(79,13) 15 | , (0,14),(1,14),(2,14),(3,14),(20,14),(21,14),(22,14),(23,14),(24,14),(25,14),(26,14),(27,14),(28,14),(29,14),(30,14),(31,14),(32,14),(33,14),(34,14),(35,14),(36,14),(37,14),(38,14),(39,14),(40,14),(41,14),(42,14),(43,14),(44,14),(45,14),(46,14),(47,14),(48,14),(49,14),(50,14),(51,14),(52,14),(53,14),(54,14),(55,14),(56,14),(57,14),(58,14),(59,14),(76,14),(77,14),(78,14),(79,14) 16 | , (0,15),(1,15),(2,15),(3,15),(20,15),(21,15),(22,15),(23,15),(24,15),(25,15),(26,15),(27,15),(28,15),(29,15),(30,15),(31,15),(32,15),(33,15),(34,15),(35,15),(36,15),(37,15),(38,15),(39,15),(40,15),(41,15),(42,15),(43,15),(44,15),(45,15),(46,15),(47,15),(48,15),(49,15),(50,15),(51,15),(52,15),(53,15),(54,15),(55,15),(56,15),(57,15),(58,15),(59,15),(76,15),(77,15),(78,15),(79,15) 17 | , (0,16),(1,16),(2,16),(3,16),(20,16),(21,16),(22,16),(23,16),(24,16),(25,16),(26,16),(27,16),(28,16),(29,16),(30,16),(31,16),(32,16),(33,16),(34,16),(35,16),(36,16),(37,16),(38,16),(39,16),(40,16),(41,16),(42,16),(43,16),(44,16),(45,16),(46,16),(47,16),(48,16),(49,16),(50,16),(51,16),(52,16),(53,16),(54,16),(55,16),(56,16),(57,16),(58,16),(59,16),(76,16),(77,16),(78,16),(79,16) 18 | , (0,17),(1,17),(2,17),(3,17),(20,17),(21,17),(22,17),(23,17),(24,17),(25,17),(26,17),(27,17),(28,17),(29,17),(30,17),(31,17),(32,17),(33,17),(34,17),(35,17),(36,17),(37,17),(38,17),(39,17),(40,17),(41,17),(42,17),(43,17),(44,17),(45,17),(46,17),(47,17),(48,17),(49,17),(50,17),(51,17),(52,17),(53,17),(54,17),(55,17),(56,17),(57,17),(58,17),(59,17),(76,17),(77,17),(78,17),(79,17) 19 | , (0,18),(1,18),(2,18),(3,18),(20,18),(21,18),(22,18),(23,18),(24,18),(25,18),(26,18),(27,18),(28,18),(29,18),(30,18),(31,18),(32,18),(33,18),(34,18),(35,18),(36,18),(37,18),(38,18),(39,18),(40,18),(41,18),(42,18),(43,18),(44,18),(45,18),(46,18),(47,18),(48,18),(49,18),(50,18),(51,18),(52,18),(53,18),(54,18),(55,18),(56,18),(57,18),(58,18),(59,18),(76,18),(77,18),(78,18),(79,18) 20 | , (0,19),(1,19),(2,19),(3,19),(20,19),(21,19),(22,19),(23,19),(24,19),(25,19),(26,19),(27,19),(28,19),(29,19),(30,19),(31,19),(32,19),(33,19),(34,19),(35,19),(36,19),(37,19),(38,19),(39,19),(40,19),(41,19),(42,19),(43,19),(44,19),(45,19),(46,19),(47,19),(48,19),(49,19),(50,19),(51,19),(52,19),(53,19),(54,19),(55,19),(56,19),(57,19),(58,19),(59,19),(76,19),(77,19),(78,19),(79,19) 21 | , (0,20),(1,20),(2,20),(3,20),(20,20),(21,20),(22,20),(23,20),(24,20),(25,20),(26,20),(27,20),(28,20),(29,20),(30,20),(31,20),(32,20),(33,20),(34,20),(35,20),(36,20),(37,20),(38,20),(39,20),(40,20),(41,20),(42,20),(43,20),(44,20),(45,20),(46,20),(47,20),(48,20),(49,20),(50,20),(51,20),(52,20),(53,20),(54,20),(55,20),(56,20),(57,20),(58,20),(59,20),(76,20),(77,20),(78,20),(79,20) 22 | , (0,21),(1,21),(2,21),(3,21),(20,21),(21,21),(22,21),(23,21),(24,21),(25,21),(26,21),(27,21),(28,21),(29,21),(30,21),(31,21),(32,21),(33,21),(34,21),(35,21),(36,21),(37,21),(38,21),(39,21),(40,21),(41,21),(42,21),(43,21),(44,21),(45,21),(46,21),(47,21),(48,21),(49,21),(50,21),(51,21),(52,21),(53,21),(54,21),(55,21),(56,21),(57,21),(58,21),(59,21),(76,21),(77,21),(78,21),(79,21) 23 | , (0,22),(1,22),(2,22),(3,22),(20,22),(21,22),(22,22),(23,22),(24,22),(25,22),(26,22),(27,22),(28,22),(29,22),(30,22),(31,22),(32,22),(33,22),(34,22),(35,22),(36,22),(37,22),(38,22),(39,22),(40,22),(41,22),(42,22),(43,22),(44,22),(45,22),(46,22),(47,22),(48,22),(49,22),(50,22),(51,22),(52,22),(53,22),(54,22),(55,22),(56,22),(57,22),(58,22),(59,22),(76,22),(77,22),(78,22),(79,22) 24 | , (0,23),(1,23),(2,23),(3,23),(20,23),(21,23),(22,23),(23,23),(24,23),(25,23),(26,23),(27,23),(28,23),(29,23),(30,23),(31,23),(32,23),(33,23),(34,23),(35,23),(36,23),(37,23),(38,23),(39,23),(40,23),(41,23),(42,23),(43,23),(44,23),(45,23),(46,23),(47,23),(48,23),(49,23),(50,23),(51,23),(52,23),(53,23),(54,23),(55,23),(56,23),(57,23),(58,23),(59,23),(76,23),(77,23),(78,23),(79,23) 25 | , (0,24),(1,24),(2,24),(3,24),(24,24),(25,24),(26,24),(27,24),(28,24),(29,24),(30,24),(31,24),(32,24),(33,24),(34,24),(35,24),(36,24),(37,24),(38,24),(39,24),(40,24),(41,24),(42,24),(43,24),(44,24),(45,24),(46,24),(47,24),(48,24),(49,24),(50,24),(51,24),(52,24),(53,24),(54,24),(55,24),(76,24),(77,24),(78,24),(79,24) 26 | , (0,25),(1,25),(2,25),(3,25),(24,25),(25,25),(26,25),(27,25),(28,25),(29,25),(30,25),(31,25),(32,25),(33,25),(34,25),(35,25),(36,25),(37,25),(38,25),(39,25),(40,25),(41,25),(42,25),(43,25),(44,25),(45,25),(46,25),(47,25),(48,25),(49,25),(50,25),(51,25),(52,25),(53,25),(54,25),(55,25),(76,25),(77,25),(78,25),(79,25) 27 | , (0,26),(1,26),(2,26),(3,26),(24,26),(25,26),(26,26),(27,26),(28,26),(29,26),(30,26),(31,26),(32,26),(33,26),(34,26),(35,26),(36,26),(37,26),(38,26),(39,26),(40,26),(41,26),(42,26),(43,26),(44,26),(45,26),(46,26),(47,26),(48,26),(49,26),(50,26),(51,26),(52,26),(53,26),(54,26),(55,26),(76,26),(77,26),(78,26),(79,26) 28 | , (0,27),(1,27),(2,27),(3,27),(24,27),(25,27),(26,27),(27,27),(28,27),(29,27),(30,27),(31,27),(32,27),(33,27),(34,27),(35,27),(36,27),(37,27),(38,27),(39,27),(40,27),(41,27),(42,27),(43,27),(44,27),(45,27),(46,27),(47,27),(48,27),(49,27),(50,27),(51,27),(52,27),(53,27),(54,27),(55,27),(76,27),(77,27),(78,27),(79,27) 29 | , (0,28),(1,28),(2,28),(3,28),(24,28),(25,28),(26,28),(27,28),(28,28),(29,28),(30,28),(31,28),(32,28),(33,28),(34,28),(35,28),(36,28),(37,28),(38,28),(39,28),(40,28),(41,28),(42,28),(43,28),(44,28),(45,28),(46,28),(47,28),(48,28),(49,28),(50,28),(51,28),(52,28),(53,28),(54,28),(55,28),(76,28),(77,28),(78,28),(79,28) 30 | , (0,29),(1,29),(2,29),(3,29),(24,29),(25,29),(26,29),(27,29),(28,29),(29,29),(30,29),(31,29),(32,29),(33,29),(34,29),(35,29),(36,29),(37,29),(38,29),(39,29),(40,29),(41,29),(42,29),(43,29),(44,29),(45,29),(46,29),(47,29),(48,29),(49,29),(50,29),(51,29),(52,29),(53,29),(54,29),(55,29),(76,29),(77,29),(78,29),(79,29) 31 | , (0,30),(1,30),(2,30),(3,30),(24,30),(25,30),(26,30),(27,30),(28,30),(29,30),(30,30),(31,30),(32,30),(33,30),(34,30),(35,30),(36,30),(37,30),(38,30),(39,30),(40,30),(41,30),(42,30),(43,30),(44,30),(45,30),(46,30),(47,30),(48,30),(49,30),(50,30),(51,30),(52,30),(53,30),(54,30),(55,30),(76,30),(77,30),(78,30),(79,30) 32 | , (0,31),(1,31),(2,31),(3,31),(24,31),(25,31),(26,31),(27,31),(28,31),(29,31),(30,31),(31,31),(32,31),(33,31),(34,31),(35,31),(36,31),(37,31),(38,31),(39,31),(40,31),(41,31),(42,31),(43,31),(44,31),(45,31),(46,31),(47,31),(48,31),(49,31),(50,31),(51,31),(52,31),(53,31),(54,31),(55,31),(76,31),(77,31),(78,31),(79,31) 33 | , (0,32),(1,32),(2,32),(3,32),(24,32),(25,32),(26,32),(27,32),(28,32),(29,32),(30,32),(31,32),(32,32),(33,32),(34,32),(35,32),(36,32),(37,32),(38,32),(39,32),(40,32),(41,32),(42,32),(43,32),(44,32),(45,32),(46,32),(47,32),(48,32),(49,32),(50,32),(51,32),(52,32),(53,32),(54,32),(55,32),(76,32),(77,32),(78,32),(79,32) 34 | , (0,33),(1,33),(2,33),(3,33),(24,33),(25,33),(26,33),(27,33),(28,33),(29,33),(30,33),(31,33),(32,33),(33,33),(34,33),(35,33),(36,33),(37,33),(38,33),(39,33),(40,33),(41,33),(42,33),(43,33),(44,33),(45,33),(46,33),(47,33),(48,33),(49,33),(50,33),(51,33),(52,33),(53,33),(54,33),(55,33),(76,33),(77,33),(78,33),(79,33) 35 | , (0,34),(1,34),(2,34),(3,34),(24,34),(25,34),(26,34),(27,34),(28,34),(29,34),(30,34),(31,34),(32,34),(33,34),(34,34),(35,34),(44,34),(45,34),(46,34),(47,34),(48,34),(49,34),(50,34),(51,34),(52,34),(53,34),(54,34),(55,34),(76,34),(77,34),(78,34),(79,34) 36 | , (0,35),(1,35),(2,35),(3,35),(24,35),(25,35),(26,35),(27,35),(28,35),(29,35),(30,35),(31,35),(32,35),(33,35),(34,35),(35,35),(44,35),(45,35),(46,35),(47,35),(48,35),(49,35),(50,35),(51,35),(52,35),(53,35),(54,35),(55,35),(76,35),(77,35),(78,35),(79,35) 37 | , (0,36),(1,36),(2,36),(3,36),(24,36),(25,36),(26,36),(27,36),(28,36),(29,36),(30,36),(31,36),(32,36),(33,36),(34,36),(35,36),(44,36),(45,36),(46,36),(47,36),(48,36),(49,36),(50,36),(51,36),(52,36),(53,36),(54,36),(55,36),(76,36),(77,36),(78,36),(79,36) 38 | , (0,37),(1,37),(2,37),(3,37),(24,37),(25,37),(26,37),(27,37),(28,37),(29,37),(30,37),(31,37),(32,37),(33,37),(34,37),(35,37),(44,37),(45,37),(46,37),(47,37),(48,37),(49,37),(50,37),(51,37),(52,37),(53,37),(54,37),(55,37),(76,37),(77,37),(78,37),(79,37) 39 | , (0,38),(1,38),(2,38),(3,38),(24,38),(25,38),(26,38),(27,38),(28,38),(29,38),(30,38),(31,38),(32,38),(33,38),(34,38),(35,38),(44,38),(45,38),(46,38),(47,38),(48,38),(49,38),(50,38),(51,38),(52,38),(53,38),(54,38),(55,38),(76,38),(77,38),(78,38),(79,38) 40 | , (0,39),(1,39),(2,39),(3,39),(24,39),(25,39),(26,39),(27,39),(28,39),(29,39),(30,39),(31,39),(32,39),(33,39),(34,39),(35,39),(44,39),(45,39),(46,39),(47,39),(48,39),(49,39),(50,39),(51,39),(52,39),(53,39),(54,39),(55,39),(76,39),(77,39),(78,39),(79,39) 41 | , (0,40),(1,40),(2,40),(3,40),(24,40),(25,40),(26,40),(27,40),(28,40),(29,40),(30,40),(31,40),(32,40),(33,40),(34,40),(35,40),(44,40),(45,40),(46,40),(47,40),(48,40),(49,40),(50,40),(51,40),(52,40),(53,40),(54,40),(55,40),(76,40),(77,40),(78,40),(79,40) 42 | , (0,41),(1,41),(2,41),(3,41),(24,41),(25,41),(26,41),(27,41),(28,41),(29,41),(30,41),(31,41),(32,41),(33,41),(34,41),(35,41),(44,41),(45,41),(46,41),(47,41),(48,41),(49,41),(50,41),(51,41),(52,41),(53,41),(54,41),(55,41),(76,41),(77,41),(78,41),(79,41) 43 | , (0,42),(1,42),(2,42),(3,42),(24,42),(25,42),(26,42),(27,42),(28,42),(29,42),(30,42),(31,42),(32,42),(33,42),(34,42),(35,42),(44,42),(45,42),(46,42),(47,42),(48,42),(49,42),(50,42),(51,42),(52,42),(53,42),(54,42),(55,42),(76,42),(77,42),(78,42),(79,42) 44 | , (0,43),(1,43),(2,43),(3,43),(24,43),(25,43),(26,43),(27,43),(28,43),(29,43),(30,43),(31,43),(32,43),(33,43),(34,43),(35,43),(44,43),(45,43),(46,43),(47,43),(48,43),(49,43),(50,43),(51,43),(52,43),(53,43),(54,43),(55,43),(76,43),(77,43),(78,43),(79,43) 45 | , (0,44),(1,44),(2,44),(3,44),(76,44),(77,44),(78,44),(79,44) 46 | , (0,45),(1,45),(2,45),(3,45),(76,45),(77,45),(78,45),(79,45) 47 | , (0,46),(1,46),(2,46),(3,46),(76,46),(77,46),(78,46),(79,46) 48 | , (0,47),(1,47),(2,47),(3,47),(76,47),(77,47),(78,47),(79,47) 49 | , (0,48),(1,48),(2,48),(3,48),(76,48),(77,48),(78,48),(79,48) 50 | , (0,49),(1,49),(2,49),(3,49),(76,49),(77,49),(78,49),(79,49) 51 | , (0,50),(1,50),(2,50),(3,50),(76,50),(77,50),(78,50),(79,50) 52 | , (0,51),(1,51),(2,51),(3,51),(76,51),(77,51),(78,51),(79,51) 53 | , (0,52),(1,52),(2,52),(3,52),(76,52),(77,52),(78,52),(79,52) 54 | , (0,53),(1,53),(2,53),(3,53),(76,53),(77,53),(78,53),(79,53) 55 | , (0,54),(1,54),(2,54),(3,54),(76,54),(77,54),(78,54),(79,54) 56 | , (0,55),(1,55),(2,55),(3,55),(76,55),(77,55),(78,55),(79,55) 57 | , (0,56),(1,56),(2,56),(3,56),(76,56),(77,56),(78,56),(79,56) 58 | , (0,57),(1,57),(2,57),(3,57),(4,57),(5,57),(6,57),(7,57),(8,57),(9,57),(10,57),(11,57),(12,57),(13,57),(14,57),(15,57),(16,57),(17,57),(18,57),(19,57),(20,57),(21,57),(22,57),(23,57),(24,57),(25,57),(26,57),(27,57),(28,57),(29,57),(30,57),(31,57),(32,57),(47,57),(48,57),(49,57),(50,57),(51,57),(52,57),(53,57),(54,57),(55,57),(56,57),(57,57),(58,57),(59,57),(60,57),(61,57),(62,57),(63,57),(64,57),(65,57),(66,57),(67,57),(68,57),(69,57),(70,57),(71,57),(72,57),(73,57),(74,57),(75,57),(76,57),(77,57),(78,57),(79,57) 59 | , (0,58),(1,58),(2,58),(3,58),(4,58),(5,58),(6,58),(7,58),(8,58),(9,58),(10,58),(11,58),(12,58),(13,58),(14,58),(15,58),(16,58),(17,58),(18,58),(19,58),(20,58),(21,58),(22,58),(23,58),(24,58),(25,58),(26,58),(27,58),(28,58),(29,58),(30,58),(31,58),(32,58),(47,58),(48,58),(49,58),(50,58),(51,58),(52,58),(53,58),(54,58),(55,58),(56,58),(57,58),(58,58),(59,58),(60,58),(61,58),(62,58),(63,58),(64,58),(65,58),(66,58),(67,58),(68,58),(69,58),(70,58),(71,58),(72,58),(73,58),(74,58),(75,58),(76,58),(77,58),(78,58),(79,58) 60 | , (0,59),(1,59),(2,59),(3,59),(4,59),(5,59),(6,59),(7,59),(8,59),(9,59),(10,59),(11,59),(12,59),(13,59),(14,59),(15,59),(16,59),(17,59),(18,59),(19,59),(20,59),(21,59),(22,59),(23,59),(24,59),(25,59),(26,59),(27,59),(28,59),(29,59),(30,59),(31,59),(32,59),(47,59),(48,59),(49,59),(50,59),(51,59),(52,59),(53,59),(54,59),(55,59),(56,59),(57,59),(58,59),(59,59),(60,59),(61,59),(62,59),(63,59),(64,59),(65,59),(66,59),(67,59),(68,59),(69,59),(70,59),(71,59),(72,59),(73,59),(74,59),(75,59),(76,59),(77,59),(78,59),(79,59) 61 | ] -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import "../css/app.scss" 2 | import "phoenix_html" 3 | import { Socket } from "phoenix" 4 | import NProgress from "nprogress" 5 | import { LiveSocket } from "phoenix_live_view" 6 | import BetaHook from "./beta-hook" 7 | import ElmHook from "./elm-hook" 8 | 9 | let hooks = { BetaHook, ElmHook } 10 | let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") 11 | let liveSocket = new LiveSocket("/live", Socket, { params: { _csrf_token: csrfToken }, hooks }) 12 | 13 | // Show progress bar on live navigation and form submits 14 | window.addEventListener("phx:page-loading-start", info => NProgress.start()) 15 | window.addEventListener("phx:page-loading-stop", info => NProgress.done()) 16 | 17 | // connect if there are any LiveViews on the page 18 | liveSocket.connect() 19 | 20 | // expose liveSocket on window for web console debug logs and latency simulation: 21 | // >> liveSocket.enableDebug() 22 | // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session 23 | // >> liveSocket.disableLatencySim() 24 | window.liveSocket = liveSocket 25 | -------------------------------------------------------------------------------- /assets/js/beta-hook.ts: -------------------------------------------------------------------------------- 1 | import { mat4 } from "gl-matrix"; 2 | 3 | let cubeRotation: number = 0.0; 4 | 5 | const BetaHook = { 6 | mounted() { 7 | const betaNode: HTMLElement | null = document.getElementById("beta"); 8 | if (betaNode) { console.log(`🧪 Beta mode enabled!`); } 9 | 10 | const canvasNode = document.getElementById("webgl-canvas"); 11 | const webglContext: WebGLRenderingContext | null = canvasNode.getContext("webgl"); 12 | 13 | if (webglContext == null) { 14 | alert("Unable to initialize WebGL. Your browser or machine may not support it."); 15 | return; 16 | } 17 | 18 | webglContext.clearColor(0.0, 0.0, 0.0, 1.0); 19 | webglContext.clear(webglContext.COLOR_BUFFER_BIT); 20 | 21 | const shaderProgram: WebGLProgram | null = initializeShaderProgram(webglContext, vertexShaderSource, fragmentShaderSource); 22 | 23 | if (shaderProgram) { 24 | const programInfo = { 25 | program: shaderProgram, 26 | attribLocations: { 27 | vertexPosition: webglContext.getAttribLocation(shaderProgram, 'aVertexPosition'), 28 | vertexColor: webglContext.getAttribLocation(shaderProgram, 'aVertexColor'), 29 | }, 30 | uniformLocations: { 31 | projectionMatrix: webglContext.getUniformLocation(shaderProgram, 'uProjectionMatrix'), 32 | modelViewMatrix: webglContext.getUniformLocation(shaderProgram, 'uModelViewMatrix'), 33 | }, 34 | }; 35 | 36 | const buffers = initializeBuffers(webglContext); 37 | 38 | let then: number = 0; 39 | 40 | const render = (now: number): void => { 41 | now *= 0.001; 42 | const deltaTime = now - then; 43 | then = now; 44 | 45 | drawScene(webglContext, programInfo, buffers, deltaTime); 46 | 47 | requestAnimationFrame(render); 48 | } 49 | requestAnimationFrame(render); 50 | } 51 | } 52 | } 53 | 54 | const vertexShaderSource: string = ` 55 | attribute vec4 aVertexPosition; 56 | attribute vec4 aVertexColor; 57 | 58 | uniform mat4 uModelViewMatrix; 59 | uniform mat4 uProjectionMatrix; 60 | 61 | varying lowp vec4 vColor; 62 | 63 | void main() { 64 | gl_Position = uProjectionMatrix * uModelViewMatrix * aVertexPosition; 65 | vColor = aVertexColor; 66 | } 67 | `; 68 | 69 | const fragmentShaderSource: string = ` 70 | varying lowp vec4 vColor; 71 | 72 | void main(void) { 73 | gl_FragColor = vColor; 74 | } 75 | `; 76 | 77 | const loadShader = (webglContext: WebGLRenderingContext, type: number, source: string) => { 78 | const shader: WebGLShader | null = webglContext.createShader(type); 79 | 80 | if (shader) { 81 | webglContext.shaderSource(shader, source); 82 | webglContext.compileShader(shader); 83 | 84 | if (!webglContext.getShaderParameter(shader, webglContext.COMPILE_STATUS)) { 85 | alert("An error occurred compiling the shaders: " + webglContext.getShaderInfoLog(shader)); 86 | webglContext.deleteShader(shader); 87 | return null; 88 | } 89 | } 90 | 91 | return shader; 92 | }; 93 | 94 | 95 | const initializeShaderProgram = (webglContext: WebGLRenderingContext, vertexShaderSource: string, fragmentShaderSource: string) => { 96 | const vertexShader: WebGLShader | null = loadShader(webglContext, webglContext.VERTEX_SHADER, vertexShaderSource); 97 | const fragmentShader: WebGLShader | null = loadShader(webglContext, webglContext.FRAGMENT_SHADER, fragmentShaderSource); 98 | 99 | const shaderProgram: WebGLProgram | null = webglContext.createProgram(); 100 | 101 | if (shaderProgram) { 102 | if (vertexShader) webglContext.attachShader(shaderProgram, vertexShader); 103 | if (fragmentShader) webglContext.attachShader(shaderProgram, fragmentShader); 104 | webglContext.linkProgram(shaderProgram); 105 | 106 | if (!webglContext.getProgramParameter(shaderProgram, webglContext.LINK_STATUS)) { 107 | alert('Unable to initialize the shader program: ' + webglContext.getProgramInfoLog(shaderProgram)); 108 | return null; 109 | } 110 | } 111 | 112 | return shaderProgram; 113 | } 114 | 115 | const initializeBuffers = (webglContext: WebGLRenderingContext) => { 116 | const positionBuffer: WebGLBuffer | null = webglContext.createBuffer(); 117 | 118 | webglContext.bindBuffer(webglContext.ARRAY_BUFFER, positionBuffer); 119 | 120 | const positions: number[] = [ 121 | // Front face 122 | -1.0, -1.0, 1.0, 123 | 1.0, -1.0, 1.0, 124 | 1.0, 1.0, 1.0, 125 | -1.0, 1.0, 1.0, 126 | 127 | // Back face 128 | -1.0, -1.0, -1.0, 129 | -1.0, 1.0, -1.0, 130 | 1.0, 1.0, -1.0, 131 | 1.0, -1.0, -1.0, 132 | 133 | // Top face 134 | -1.0, 1.0, -1.0, 135 | -1.0, 1.0, 1.0, 136 | 1.0, 1.0, 1.0, 137 | 1.0, 1.0, -1.0, 138 | 139 | // Bottom face 140 | -1.0, -1.0, -1.0, 141 | 1.0, -1.0, -1.0, 142 | 1.0, -1.0, 1.0, 143 | -1.0, -1.0, 1.0, 144 | 145 | // Right face 146 | 1.0, -1.0, -1.0, 147 | 1.0, 1.0, -1.0, 148 | 1.0, 1.0, 1.0, 149 | 1.0, -1.0, 1.0, 150 | 151 | // Left face 152 | -1.0, -1.0, -1.0, 153 | -1.0, -1.0, 1.0, 154 | -1.0, 1.0, 1.0, 155 | -1.0, 1.0, -1.0, 156 | ]; 157 | 158 | webglContext.bufferData(webglContext.ARRAY_BUFFER, 159 | new Float32Array(positions), 160 | webglContext.STATIC_DRAW); 161 | 162 | const faceColors = [ 163 | [1.0, 1.0, 1.0, 1.0], // Front face: white 164 | [1.0, 0.0, 0.0, 1.0], // Back face: red 165 | [0.0, 1.0, 0.0, 1.0], // Top face: green 166 | [0.0, 0.0, 1.0, 1.0], // Bottom face: blue 167 | [1.0, 1.0, 0.0, 1.0], // Right face: yellow 168 | [1.0, 0.0, 1.0, 1.0], // Left face: purple 169 | ]; 170 | 171 | let colors: number[] = []; 172 | 173 | for (var j = 0; j < faceColors.length; ++j) { 174 | const c = faceColors[j]; 175 | colors = colors.concat(c, c, c, c); 176 | } 177 | 178 | const colorBuffer: WebGLBuffer | null = webglContext.createBuffer(); 179 | webglContext.bindBuffer(webglContext.ARRAY_BUFFER, colorBuffer); 180 | webglContext.bufferData(webglContext.ARRAY_BUFFER, new Float32Array(colors), webglContext.STATIC_DRAW); 181 | 182 | const indexBuffer: WebGLBuffer | null = webglContext.createBuffer(); 183 | webglContext.bindBuffer(webglContext.ELEMENT_ARRAY_BUFFER, indexBuffer); 184 | 185 | const indices: number[] = [ 186 | 0, 1, 2, 0, 2, 3, // front 187 | 4, 5, 6, 4, 6, 7, // back 188 | 8, 9, 10, 8, 10, 11, // top 189 | 12, 13, 14, 12, 14, 15, // bottom 190 | 16, 17, 18, 16, 18, 19, // right 191 | 20, 21, 22, 20, 22, 23, // left 192 | ]; 193 | 194 | webglContext.bufferData(webglContext.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), webglContext.STATIC_DRAW); 195 | 196 | return { 197 | position: positionBuffer, 198 | color: colorBuffer, 199 | indices: indexBuffer 200 | }; 201 | } 202 | 203 | const drawScene = (webglContext: WebGLRenderingContext, programInfo: any, buffers: any, deltaTime: number) => { 204 | webglContext.clearColor(0.0, 0.0, 0.0, 1.0); 205 | webglContext.clearDepth(1.0); 206 | webglContext.enable(webglContext.DEPTH_TEST); 207 | webglContext.depthFunc(webglContext.LEQUAL); 208 | 209 | webglContext.clear(webglContext.COLOR_BUFFER_BIT | webglContext.DEPTH_BUFFER_BIT); 210 | 211 | const fieldOfView = 45 * Math.PI / 180; // in radians 212 | const zNear = 0.1; 213 | const zFar = 100.0; 214 | const projectionMatrix = mat4.create(); 215 | 216 | const webglCanvas = webglContext.canvas as HTMLCanvasElement; 217 | const aspect = webglCanvas.clientWidth / webglCanvas.clientHeight; 218 | 219 | mat4.perspective(projectionMatrix, fieldOfView, aspect, zNear, zFar); 220 | 221 | const modelViewMatrix = mat4.create(); 222 | 223 | mat4.translate(modelViewMatrix, modelViewMatrix, [-0.0, 0.0, -6.0]); 224 | 225 | mat4.rotate(modelViewMatrix, modelViewMatrix, cubeRotation, [0, 0, 1]); 226 | mat4.rotate(modelViewMatrix, modelViewMatrix, cubeRotation * .7, [0, 1, 0]); 227 | 228 | { 229 | const numComponents = 3; 230 | const type = webglContext.FLOAT; 231 | const normalize = false; 232 | const stride = 0; 233 | const offset = 0; 234 | webglContext.bindBuffer(webglContext.ARRAY_BUFFER, buffers.position); 235 | webglContext.vertexAttribPointer( 236 | programInfo.attribLocations.vertexPosition, 237 | numComponents, 238 | type, 239 | normalize, 240 | stride, 241 | offset); 242 | webglContext.enableVertexAttribArray( 243 | programInfo.attribLocations.vertexPosition); 244 | } 245 | 246 | { 247 | const numComponents = 4; 248 | const type = webglContext.FLOAT; 249 | const normalize = false; 250 | const stride = 0; 251 | const offset = 0; 252 | webglContext.bindBuffer(webglContext.ARRAY_BUFFER, buffers.color); 253 | webglContext.vertexAttribPointer( 254 | programInfo.attribLocations.vertexColor, 255 | numComponents, 256 | type, 257 | normalize, 258 | stride, 259 | offset); 260 | webglContext.enableVertexAttribArray( 261 | programInfo.attribLocations.vertexColor); 262 | } 263 | 264 | webglContext.useProgram(programInfo.program); 265 | 266 | webglContext.uniformMatrix4fv( 267 | programInfo.uniformLocations.projectionMatrix, 268 | false, 269 | projectionMatrix); 270 | webglContext.uniformMatrix4fv( 271 | programInfo.uniformLocations.modelViewMatrix, 272 | false, 273 | modelViewMatrix); 274 | 275 | { 276 | const offset = 0; 277 | const vertexCount = 4; 278 | webglContext.drawArrays(webglContext.TRIANGLE_STRIP, offset, vertexCount); 279 | } 280 | 281 | webglContext.bindBuffer(webglContext.ELEMENT_ARRAY_BUFFER, buffers.indices); 282 | 283 | { 284 | const vertexCount = 36; 285 | const type = webglContext.UNSIGNED_SHORT; 286 | const offset = 0; 287 | webglContext.drawElements(webglContext.TRIANGLES, vertexCount, type, offset); 288 | } 289 | 290 | cubeRotation += deltaTime; 291 | } 292 | 293 | export default BetaHook; 294 | -------------------------------------------------------------------------------- /assets/js/elm-hook.js: -------------------------------------------------------------------------------- 1 | import { Elm } from "../elm/src/Main.elm"; 2 | import { Howl, Howler } from "howler"; 3 | 4 | const ElmHook = { 5 | mounted() { 6 | const flags = {} 7 | const node = document.getElementById("elm") 8 | const app = Elm.Main.init({ node, flags }) 9 | 10 | // Sounds with Howler 11 | 12 | app.ports.playSound.subscribe(data => { 13 | const soundPath = "/sounds/" + data; 14 | const sound = new Howl({ 15 | src: [soundPath], 16 | volume: 0.35 17 | }); 18 | 19 | sound.play(); 20 | }); 21 | 22 | app.ports.playMusic.subscribe(data => { 23 | // data example: { play: true, soundFile: "music.wav" } 24 | 25 | const soundPath = "/sounds/" + data.soundFile; 26 | const sound = new Howl({ 27 | src: [soundPath], 28 | loop: true, 29 | volume: 0.15 30 | }); 31 | 32 | const startPlaying = () => { 33 | const soundId = sound.play(); 34 | sound.rate(0.95, soundId); 35 | sound.fade(0, 1, 2000, soundId); 36 | } 37 | 38 | const stopPlaying = () => { 39 | Howler.stop() 40 | } 41 | 42 | if (data.play) { startPlaying() } 43 | if (!data.play) { stopPlaying() } 44 | }); 45 | 46 | // Prevent Default Keyboard Behavior 47 | 48 | const gameKeys = { 49 | " ": 32, 50 | "ArrowUp": 38, 51 | "ArrowDown": 40 52 | }; 53 | 54 | const preventDefaultForGameKeys = (event) => { 55 | const keys = Object.values(gameKeys) 56 | if (keys.includes(event.keycode) || 57 | keys.includes(event.which)) 58 | event.preventDefault(); 59 | } 60 | 61 | document.documentElement.addEventListener( 62 | "keydown", 63 | (event) => preventDefaultForGameKeys(event), 64 | false 65 | ); 66 | 67 | // DevTools Easter Egg 68 | 69 | const string = ` 70 | C 71 | r b 72 | e y 73 | a . 74 | t . 75 | e . 76 | d . 77 | 78 | B . 79 | i . 80 | j B 81 | a o 82 | n u 83 | . s 84 | . t 85 | . a 86 | . n 87 | . i 88 | 🐰🥚 89 | ` 90 | 91 | console.log(string); 92 | 93 | } 94 | } 95 | 96 | export default ElmHook; 97 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "description": " ", 4 | "license": "MIT", 5 | "scripts": { 6 | "deploy": "webpack --mode production", 7 | "watch": "webpack --mode development --watch" 8 | }, 9 | "dependencies": { 10 | "phoenix": "file:../deps/phoenix", 11 | "phoenix_html": "file:../deps/phoenix_html", 12 | "phoenix_live_view": "file:../deps/phoenix_live_view", 13 | "nprogress": "^0.2.0" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.0.0", 17 | "@babel/preset-env": "^7.0.0", 18 | "@types/phoenix": "^1.5.0", 19 | "babel-loader": "^8.0.0", 20 | "copy-webpack-plugin": "^6.0.3", 21 | "css-loader": "^3.4.2", 22 | "elm": "^0.19.1-3", 23 | "elm-webpack-loader": "^6.0.1", 24 | "gl-matrix": "^3.3.0", 25 | "hard-source-webpack-plugin": "^0.13.1", 26 | "howler": "^2.2.0", 27 | "mini-css-extract-plugin": "^0.9.0", 28 | "node-sass": "^4.13.1", 29 | "optimize-css-assets-webpack-plugin": "^5.0.1", 30 | "sass-loader": "^8.0.2", 31 | "serialize-javascript": ">=3.1.0", 32 | "source-map-loader": "^1.1.2", 33 | "terser-webpack-plugin": "^2.3.2", 34 | "ts-loader": "^8.0.7", 35 | "typescript": "^4.0.5", 36 | "webpack": "4.41.5", 37 | "webpack-cli": "^3.3.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/create-with/games/a6c8a96b05ed4f803da25dd5f89e732cb2b0a673/assets/static/favicon.ico -------------------------------------------------------------------------------- /assets/static/images/pixel-ball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/create-with/games/a6c8a96b05ed4f803da25dd5f89e732cb2b0a673/assets/static/images/pixel-ball.png -------------------------------------------------------------------------------- /assets/static/images/pixel-paddle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/create-with/games/a6c8a96b05ed4f803da25dd5f89e732cb2b0a673/assets/static/images/pixel-paddle.png -------------------------------------------------------------------------------- /assets/static/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /assets/static/sounds/beep.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/create-with/games/a6c8a96b05ed4f803da25dd5f89e732cb2b0a673/assets/static/sounds/beep.wav -------------------------------------------------------------------------------- /assets/static/sounds/boop.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/create-with/games/a6c8a96b05ed4f803da25dd5f89e732cb2b0a673/assets/static/sounds/boop.wav -------------------------------------------------------------------------------- /assets/static/sounds/music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/create-with/games/a6c8a96b05ed4f803da25dd5f89e732cb2b0a673/assets/static/sounds/music.wav -------------------------------------------------------------------------------- /assets/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* https://www.typescriptlang.org/tsconfig */ 4 | "target": "es5", 5 | "module": "ESNext", 6 | "allowJs": true, 7 | "outDir": "./dist/", 8 | "isolatedModules": true, 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true 14 | }, 15 | "exclude": [ 16 | "/node_modules/**/*" 17 | ] 18 | } -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const glob = require('glob'); 3 | const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | const TerserPlugin = require('terser-webpack-plugin'); 6 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 7 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 8 | 9 | module.exports = (env, options) => { 10 | const devMode = options.mode !== 'production'; 11 | 12 | return { 13 | optimization: { 14 | minimizer: [ 15 | new TerserPlugin({ 16 | cache: true, 17 | parallel: true, 18 | sourceMap: devMode, 19 | terserOptions: { 20 | compress: { 21 | pure_funcs: ['F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8', 'A9'], 22 | pure_getters: true, 23 | keep_fargs: false, 24 | unsafe_comps: true, 25 | unsafe: true, 26 | passes: 2 27 | }, 28 | mangle: true 29 | } 30 | }), 31 | new OptimizeCSSAssetsPlugin({}) 32 | ] 33 | }, 34 | entry: { 35 | 'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js']) 36 | }, 37 | output: { 38 | filename: '[name].js', 39 | path: path.resolve(__dirname, '../priv/static/js'), 40 | publicPath: '/js/' 41 | }, 42 | devtool: devMode ? 'eval-cheap-module-source-map' : undefined, 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.js$/, 47 | exclude: /node_modules/, 48 | use: { 49 | loader: 'babel-loader' 50 | } 51 | }, 52 | { 53 | test: /\.[s]?css$/, 54 | use: [ 55 | MiniCssExtractPlugin.loader, 56 | 'css-loader', 57 | 'sass-loader', 58 | ], 59 | }, 60 | { 61 | test: /\.elm$/, 62 | exclude: [/elm-stuff/, /node_modules/], 63 | use: { 64 | loader: 'elm-webpack-loader', 65 | options: { 66 | cwd: path.resolve(__dirname, 'elm'), 67 | debug: devMode, 68 | optimize: !devMode, 69 | verbose: devMode 70 | } 71 | } 72 | }, 73 | { 74 | test: /\.(j|t)s$/, 75 | exclude: /node_modules/, 76 | use: [ 77 | { 78 | loader: "babel-loader" 79 | }, 80 | { 81 | loader: "ts-loader" 82 | } 83 | ] 84 | } 85 | ] 86 | }, 87 | resolve: { 88 | extensions: [".ts", ".js"] 89 | }, 90 | plugins: [ 91 | new MiniCssExtractPlugin({ filename: '../css/app.css' }), 92 | new CopyWebpackPlugin({ patterns: [{ from: 'static/', to: '../' }] }) 93 | ] 94 | .concat(devMode ? [new HardSourceWebpackPlugin()] : []) 95 | } 96 | }; 97 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | # 4 | # This configuration file is loaded before any dependency and 5 | # is restricted to this project. 6 | 7 | # General application configuration 8 | use Mix.Config 9 | 10 | config :games, 11 | ecto_repos: [Games.Repo] 12 | 13 | # Configures the endpoint 14 | config :games, GamesWeb.Endpoint, 15 | url: [host: "localhost"], 16 | secret_key_base: "hN1WyTZ5m4+7KYTHuwhl+s3FHa+8myW3GMCE98BWg+ifqFO2euPvHrQ/URgcT4x0", 17 | render_errors: [view: GamesWeb.ErrorView, accepts: ~w(html json), layout: false], 18 | pubsub_server: Games.PubSub, 19 | live_view: [signing_salt: "yraOBzUF"] 20 | 21 | # Configures Elixir's Logger 22 | config :logger, :console, 23 | format: "$time $metadata[$level] $message\n", 24 | metadata: [:request_id] 25 | 26 | # Use Jason for JSON parsing in Phoenix 27 | config :phoenix, :json_library, Jason 28 | 29 | # Import environment specific config. This must remain at the bottom 30 | # of this file so it overrides the configuration defined above. 31 | import_config "#{Mix.env()}.exs" 32 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | config :games, Games.Repo, 5 | username: "postgres", 6 | password: "postgres", 7 | database: "games_dev", 8 | hostname: "localhost", 9 | show_sensitive_data_on_connection_error: true, 10 | pool_size: 10 11 | 12 | # For development, we disable any cache and enable 13 | # debugging and code reloading. 14 | # 15 | # The watchers configuration can be used to run external 16 | # watchers to your application. For example, we use it 17 | # with webpack to recompile .js and .css sources. 18 | config :games, GamesWeb.Endpoint, 19 | http: [port: 4000], 20 | debug_errors: true, 21 | code_reloader: true, 22 | check_origin: false, 23 | watchers: [ 24 | node: [ 25 | "node_modules/webpack/bin/webpack.js", 26 | "--mode", 27 | "development", 28 | "--watch-stdin", 29 | cd: Path.expand("../assets", __DIR__) 30 | ] 31 | ] 32 | 33 | # ## SSL Support 34 | # 35 | # In order to use HTTPS in development, a self-signed 36 | # certificate can be generated by running the following 37 | # Mix task: 38 | # 39 | # mix phx.gen.cert 40 | # 41 | # Note that this task requires Erlang/OTP 20 or later. 42 | # Run `mix help phx.gen.cert` for more information. 43 | # 44 | # The `http:` config above can be replaced with: 45 | # 46 | # https: [ 47 | # port: 4001, 48 | # cipher_suite: :strong, 49 | # keyfile: "priv/cert/selfsigned_key.pem", 50 | # certfile: "priv/cert/selfsigned.pem" 51 | # ], 52 | # 53 | # If desired, both `http:` and `https:` keys can be 54 | # configured to run both http and https servers on 55 | # different ports. 56 | 57 | # Watch static and templates for browser reloading. 58 | config :games, GamesWeb.Endpoint, 59 | live_reload: [ 60 | patterns: [ 61 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 62 | ~r"priv/gettext/.*(po)$", 63 | ~r"lib/games_web/(live|views)/.*(ex)$", 64 | ~r"lib/games_web/templates/.*(eex)$" 65 | ] 66 | ] 67 | 68 | # Do not include metadata nor timestamps in development logs 69 | config :logger, :console, format: "[$level] $message\n" 70 | 71 | # Set a higher stacktrace during development. Avoid configuring such 72 | # in production as building large stacktraces may be expensive. 73 | config :phoenix, :stacktrace_depth, 20 74 | 75 | # Initialize plugs at runtime for faster development compilation 76 | config :phoenix, :plug_init_mode, :runtime 77 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # For production, don't forget to configure the url host 4 | # to something meaningful, Phoenix uses this information 5 | # when generating URLs. 6 | # 7 | # Note we also include the path to a cache manifest 8 | # containing the digested version of static files. This 9 | # manifest is generated by the `mix phx.digest` task, 10 | # which you should run after static files are built and 11 | # before starting your production server. 12 | config :games, GamesWeb.Endpoint, 13 | http: [port: {:system, "PORT"}], 14 | url: [scheme: "https", host: "create-with-games.herokuapp.com", port: 443], 15 | force_ssl: [rewrite_on: [:x_forwarded_proto]], 16 | cache_static_manifest: "priv/static/cache_manifest.json" 17 | 18 | # Do not print debug messages in production 19 | config :logger, level: :info 20 | 21 | # ## SSL Support 22 | # 23 | # To get SSL working, you will need to add the `https` key 24 | # to the previous section and set your `:url` port to 443: 25 | # 26 | # config :games, GamesWeb.Endpoint, 27 | # ... 28 | # url: [host: "example.com", port: 443], 29 | # https: [ 30 | # port: 443, 31 | # cipher_suite: :strong, 32 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 33 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH"), 34 | # transport_options: [socket_opts: [:inet6]] 35 | # ] 36 | # 37 | # The `cipher_suite` is set to `:strong` to support only the 38 | # latest and more secure SSL ciphers. This means old browsers 39 | # and clients may not be supported. You can set it to 40 | # `:compatible` for wider support. 41 | # 42 | # `:keyfile` and `:certfile` expect an absolute path to the key 43 | # and cert in disk or a relative path inside priv, for example 44 | # "priv/ssl/server.key". For all supported SSL configuration 45 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 46 | # 47 | # We also recommend setting `force_ssl` in your endpoint, ensuring 48 | # no data is ever sent via http, always redirecting to https: 49 | # 50 | # config :games, GamesWeb.Endpoint, 51 | # force_ssl: [hsts: true] 52 | # 53 | # Check `Plug.SSL` for all available options in `force_ssl`. 54 | 55 | # Finally import the config/prod.secret.exs which loads secrets 56 | # and configuration from environment variables. 57 | import_config "prod.secret.exs" 58 | -------------------------------------------------------------------------------- /config/prod.secret.exs: -------------------------------------------------------------------------------- 1 | # In this file, we load production configuration and secrets 2 | # from environment variables. You can also hardcode secrets, 3 | # although such is generally not recommended and you have to 4 | # remember to add this file to your .gitignore. 5 | use Mix.Config 6 | 7 | database_url = 8 | System.get_env("DATABASE_URL") || 9 | raise """ 10 | environment variable DATABASE_URL is missing. 11 | For example: ecto://USER:PASS@HOST/DATABASE 12 | """ 13 | 14 | config :games, Games.Repo, 15 | ssl: true, 16 | url: database_url, 17 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") 18 | 19 | secret_key_base = 20 | System.get_env("SECRET_KEY_BASE") || 21 | raise """ 22 | environment variable SECRET_KEY_BASE is missing. 23 | You can generate one by calling: mix phx.gen.secret 24 | """ 25 | 26 | config :games, GamesWeb.Endpoint, 27 | http: [ 28 | port: String.to_integer(System.get_env("PORT") || "4000"), 29 | transport_options: [socket_opts: [:inet6]] 30 | ], 31 | secret_key_base: secret_key_base 32 | 33 | # ## Using releases (Elixir v1.9+) 34 | # 35 | # If you are doing OTP releases, you need to instruct Phoenix 36 | # to start each relevant endpoint: 37 | # 38 | # config :games, GamesWeb.Endpoint, server: true 39 | # 40 | # Then you can assemble a release by calling `mix release`. 41 | # See `mix help release` for more information. 42 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Configure your database 4 | # 5 | # The MIX_TEST_PARTITION environment variable can be used 6 | # to provide built-in test partitioning in CI environment. 7 | # Run `mix help test` for more information. 8 | config :games, Games.Repo, 9 | username: "postgres", 10 | password: "postgres", 11 | database: "games_test#{System.get_env("MIX_TEST_PARTITION")}", 12 | hostname: "localhost", 13 | pool: Ecto.Adapters.SQL.Sandbox 14 | 15 | # We don't run a server during test. If one is required, 16 | # you can enable the server option below. 17 | config :games, GamesWeb.Endpoint, 18 | http: [port: 4002], 19 | server: false 20 | 21 | # Print only warnings and errors during test 22 | config :logger, level: :warn 23 | -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | erlang_version=23.0 2 | elixir_version=1.10.4 3 | always_rebuild=true -------------------------------------------------------------------------------- /lib/games.ex: -------------------------------------------------------------------------------- 1 | defmodule Games do 2 | @moduledoc """ 3 | Games keeps the contexts that define your domain 4 | and business logic. 5 | 6 | Contexts are also responsible for managing your data, regardless 7 | if it comes from the database, an external API or others. 8 | """ 9 | end 10 | -------------------------------------------------------------------------------- /lib/games/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Games.Application do 2 | # See https://hexdocs.pm/elixir/Application.html 3 | # for more information on OTP Applications 4 | @moduledoc false 5 | 6 | use Application 7 | 8 | def start(_type, _args) do 9 | children = [ 10 | # Start the Ecto repository 11 | Games.Repo, 12 | # Start the Telemetry supervisor 13 | GamesWeb.Telemetry, 14 | # Start the PubSub system 15 | {Phoenix.PubSub, name: Games.PubSub}, 16 | # Start the Endpoint (http/https) 17 | GamesWeb.Endpoint 18 | # Start a worker by calling: Games.Worker.start_link(arg) 19 | # {Games.Worker, arg} 20 | ] 21 | 22 | # See https://hexdocs.pm/elixir/Supervisor.html 23 | # for other strategies and supported options 24 | opts = [strategy: :one_for_one, name: Games.Supervisor] 25 | Supervisor.start_link(children, opts) 26 | end 27 | 28 | # Tell Phoenix to update the endpoint configuration 29 | # whenever the application is updated. 30 | def config_change(changed, _new, removed) do 31 | GamesWeb.Endpoint.config_change(changed, removed) 32 | :ok 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/games/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Games.Repo do 2 | use Ecto.Repo, 3 | otp_app: :games, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /lib/games_web.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb do 2 | @moduledoc """ 3 | The entrypoint for defining your web interface, such 4 | as controllers, views, channels and so on. 5 | 6 | This can be used in your application as: 7 | 8 | use GamesWeb, :controller 9 | use GamesWeb, :view 10 | 11 | The definitions below will be executed for every view, 12 | controller, etc, so keep them short and clean, focused 13 | on imports, uses and aliases. 14 | 15 | Do NOT define functions inside the quoted expressions 16 | below. Instead, define any helper function in modules 17 | and import those modules here. 18 | """ 19 | 20 | def controller do 21 | quote do 22 | use Phoenix.Controller, namespace: GamesWeb 23 | 24 | import Plug.Conn 25 | import GamesWeb.Gettext 26 | alias GamesWeb.Router.Helpers, as: Routes 27 | end 28 | end 29 | 30 | def view do 31 | quote do 32 | use Phoenix.View, 33 | root: "lib/games_web/templates", 34 | namespace: GamesWeb 35 | 36 | # Import convenience functions from controllers 37 | import Phoenix.Controller, 38 | only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] 39 | 40 | # Include shared imports and aliases for views 41 | unquote(view_helpers()) 42 | end 43 | end 44 | 45 | def live_view do 46 | quote do 47 | use Phoenix.LiveView, 48 | layout: {GamesWeb.LayoutView, "live.html"} 49 | 50 | unquote(view_helpers()) 51 | end 52 | end 53 | 54 | def live_component do 55 | quote do 56 | use Phoenix.LiveComponent 57 | 58 | unquote(view_helpers()) 59 | end 60 | end 61 | 62 | def router do 63 | quote do 64 | use Phoenix.Router 65 | 66 | import Plug.Conn 67 | import Phoenix.Controller 68 | import Phoenix.LiveView.Router 69 | end 70 | end 71 | 72 | def channel do 73 | quote do 74 | use Phoenix.Channel 75 | import GamesWeb.Gettext 76 | end 77 | end 78 | 79 | defp view_helpers do 80 | quote do 81 | # Use all HTML functionality (forms, tags, etc) 82 | use Phoenix.HTML 83 | 84 | # Import LiveView helpers (live_render, live_component, live_patch, etc) 85 | import Phoenix.LiveView.Helpers 86 | 87 | # Import basic rendering functionality (render, render_layout, etc) 88 | import Phoenix.View 89 | 90 | import GamesWeb.ErrorHelpers 91 | import GamesWeb.Gettext 92 | alias GamesWeb.Router.Helpers, as: Routes 93 | end 94 | end 95 | 96 | @doc """ 97 | When used, dispatch to the appropriate controller/view/etc. 98 | """ 99 | defmacro __using__(which) when is_atom(which) do 100 | apply(__MODULE__, which, []) 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/games_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", GamesWeb.RoomChannel 6 | 7 | # Socket params are passed from the client and can 8 | # be used to verify and authenticate a user. After 9 | # verification, you can put default assigns into 10 | # the socket that will be set for all channels, ie 11 | # 12 | # {:ok, assign(socket, :user_id, verified_user_id)} 13 | # 14 | # To deny connection, return `:error`. 15 | # 16 | # See `Phoenix.Token` documentation for examples in 17 | # performing token verification on connect. 18 | @impl true 19 | def connect(_params, socket, _connect_info) do 20 | {:ok, socket} 21 | end 22 | 23 | # Socket id's are topics that allow you to identify all sockets for a given user: 24 | # 25 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 26 | # 27 | # Would allow you to broadcast a "disconnect" event and terminate 28 | # all active sockets and channels for a given user: 29 | # 30 | # GamesWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 31 | # 32 | # Returning `nil` makes this socket anonymous. 33 | @impl true 34 | def id(_socket), do: nil 35 | end 36 | -------------------------------------------------------------------------------- /lib/games_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :games 3 | 4 | # The session will be stored in the cookie and signed, 5 | # this means its contents can be read but not tampered with. 6 | # Set :encryption_salt if you would also like to encrypt it. 7 | @session_options [ 8 | same_site: "Strict", 9 | store: :cookie, 10 | key: "_games_key", 11 | signing_salt: "Icp+g5gL" 12 | ] 13 | 14 | socket("/socket", GamesWeb.UserSocket, 15 | websocket: [timeout: 45_000], 16 | longpoll: false 17 | ) 18 | 19 | socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]) 20 | 21 | # Serve at "/" the static files from "priv/static" directory. 22 | # 23 | # You should set gzip to true if you are running phx.digest 24 | # when deploying your static files in production. 25 | plug(Plug.Static, 26 | at: "/", 27 | from: :games, 28 | gzip: false, 29 | only: ~w(css fonts images sounds js favicon.ico robots.txt) 30 | ) 31 | 32 | # Code reloading can be explicitly enabled under the 33 | # :code_reloader configuration of your endpoint. 34 | if code_reloading? do 35 | socket("/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket) 36 | plug(Phoenix.LiveReloader) 37 | plug(Phoenix.CodeReloader) 38 | plug(Phoenix.Ecto.CheckRepoStatus, otp_app: :games) 39 | end 40 | 41 | plug(Phoenix.LiveDashboard.RequestLogger, 42 | param_key: "request_logger", 43 | cookie_key: "request_logger" 44 | ) 45 | 46 | plug(Plug.RequestId) 47 | plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint]) 48 | 49 | plug(Plug.Parsers, 50 | parsers: [:urlencoded, :multipart, :json], 51 | pass: ["*/*"], 52 | json_decoder: Phoenix.json_library() 53 | ) 54 | 55 | plug(Plug.MethodOverride) 56 | plug(Plug.Head) 57 | plug(Plug.Session, @session_options) 58 | plug(GamesWeb.Router) 59 | end 60 | -------------------------------------------------------------------------------- /lib/games_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.Gettext do 2 | @moduledoc """ 3 | A module providing Internationalization with a gettext-based API. 4 | 5 | By using [Gettext](https://hexdocs.pm/gettext), 6 | your module gains a set of macros for translations, for example: 7 | 8 | import GamesWeb.Gettext 9 | 10 | # Simple translation 11 | gettext("Here is the string to translate") 12 | 13 | # Plural translation 14 | ngettext("Here is the string to translate", 15 | "Here are the strings to translate", 16 | 3) 17 | 18 | # Domain-based translation 19 | dgettext("errors", "Here is the error message to translate") 20 | 21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. 22 | """ 23 | use Gettext, otp_app: :games 24 | end 25 | -------------------------------------------------------------------------------- /lib/games_web/live/beta_live.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.BetaLive do 2 | use GamesWeb, :live_view 3 | 4 | alias Phoenix.LiveView 5 | 6 | @impl LiveView 7 | def render(assigns) do 8 | ~L""" 9 |
10 | 11 |
12 | """ 13 | end 14 | 15 | @impl LiveView 16 | def mount(_params, _session, socket) do 17 | {:ok, socket} 18 | end 19 | 20 | @impl LiveView 21 | def handle_event(_event, _params, socket) do 22 | {:noreply, socket} 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/games_web/live/page_live.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.PageLive do 2 | use GamesWeb, :live_view 3 | 4 | alias Phoenix.LiveView 5 | 6 | @impl LiveView 7 | def render(assigns) do 8 | ~L""" 9 |
10 | """ 11 | end 12 | 13 | @impl LiveView 14 | def mount(_params, _session, socket) do 15 | {:ok, socket} 16 | end 17 | 18 | @impl LiveView 19 | def handle_event(_event, _params, socket) do 20 | {:noreply, socket} 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/games_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.Router do 2 | use GamesWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_live_flash 8 | plug :put_root_layout, {GamesWeb.LayoutView, :root} 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | pipeline :api do 14 | plug :accepts, ["json"] 15 | end 16 | 17 | if Mix.env() in [:dev, :test] do 18 | import Phoenix.LiveDashboard.Router 19 | 20 | scope "/" do 21 | pipe_through :browser 22 | live_dashboard "/dashboard", metrics: GamesWeb.Telemetry 23 | end 24 | end 25 | 26 | scope "/", GamesWeb do 27 | pipe_through :browser 28 | 29 | live "/beta", BetaLive, :index 30 | live "/*path", PageLive, :index 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/games_web/telemetry.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.Telemetry do 2 | use Supervisor 3 | import Telemetry.Metrics 4 | 5 | def start_link(arg) do 6 | Supervisor.start_link(__MODULE__, arg, name: __MODULE__) 7 | end 8 | 9 | @impl true 10 | def init(_arg) do 11 | children = [ 12 | # Telemetry poller will execute the given period measurements 13 | # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics 14 | {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} 15 | # Add reporters as children of your supervision tree. 16 | # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} 17 | ] 18 | 19 | Supervisor.init(children, strategy: :one_for_one) 20 | end 21 | 22 | def metrics do 23 | [ 24 | # Phoenix Metrics 25 | summary("phoenix.endpoint.stop.duration", 26 | unit: {:native, :millisecond} 27 | ), 28 | summary("phoenix.router_dispatch.stop.duration", 29 | tags: [:route], 30 | unit: {:native, :millisecond} 31 | ), 32 | 33 | # Database Metrics 34 | summary("games.repo.query.total_time", unit: {:native, :millisecond}), 35 | summary("games.repo.query.decode_time", unit: {:native, :millisecond}), 36 | summary("games.repo.query.query_time", unit: {:native, :millisecond}), 37 | summary("games.repo.query.queue_time", unit: {:native, :millisecond}), 38 | summary("games.repo.query.idle_time", unit: {:native, :millisecond}), 39 | 40 | # VM Metrics 41 | summary("vm.memory.total", unit: {:byte, :kilobyte}), 42 | summary("vm.total_run_queue_lengths.total"), 43 | summary("vm.total_run_queue_lengths.cpu"), 44 | summary("vm.total_run_queue_lengths.io") 45 | ] 46 | end 47 | 48 | defp periodic_measurements do 49 | [ 50 | # A module, function and arguments to be invoked periodically. 51 | # This function must call :telemetry.execute/3 and a metric must be added above. 52 | # {GamesWeb, :count_users, []} 53 | ] 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/games_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | <%= @inner_content %> 5 |
6 | -------------------------------------------------------------------------------- /lib/games_web/templates/layout/live.html.leex: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 9 | 10 | <%= @inner_content %> 11 |
12 | -------------------------------------------------------------------------------- /lib/games_web/templates/layout/root.html.leex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= csrf_meta_tag() %> 9 | <%= live_title_tag assigns[:page_title] || "Games", suffix: " · Phoenix Framework" %> 10 | 11 | 12 | " /> 13 | 14 | 15 | 16 | 17 | <%= @inner_content %> 18 | 19 | 20 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/games_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.ErrorHelpers do 2 | @moduledoc """ 3 | Conveniences for translating and building error messages. 4 | """ 5 | 6 | use Phoenix.HTML 7 | 8 | @doc """ 9 | Generates tag for inlined form input errors. 10 | """ 11 | def error_tag(form, field) do 12 | Enum.map(Keyword.get_values(form.errors, field), fn error -> 13 | content_tag(:span, translate_error(error), 14 | class: "invalid-feedback", 15 | phx_feedback_for: input_id(form, field) 16 | ) 17 | end) 18 | end 19 | 20 | @doc """ 21 | Translates an error message using gettext. 22 | """ 23 | def translate_error({msg, opts}) do 24 | # When using gettext, we typically pass the strings we want 25 | # to translate as a static argument: 26 | # 27 | # # Translate "is invalid" in the "errors" domain 28 | # dgettext("errors", "is invalid") 29 | # 30 | # # Translate the number of files with plural rules 31 | # dngettext("errors", "1 file", "%{count} files", count) 32 | # 33 | # Because the error messages we show in our forms and APIs 34 | # are defined inside Ecto, we need to translate them dynamically. 35 | # This requires us to call the Gettext module passing our gettext 36 | # backend as first argument. 37 | # 38 | # Note we use the "errors" domain, which means translations 39 | # should be written to the errors.po file. The :count option is 40 | # set by Ecto and indicates we should also apply plural rules. 41 | if count = opts[:count] do 42 | Gettext.dngettext(GamesWeb.Gettext, "errors", msg, msg, count, opts) 43 | else 44 | Gettext.dgettext(GamesWeb.Gettext, "errors", msg, opts) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/games_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.ErrorView do 2 | use GamesWeb, :view 3 | 4 | # If you want to customize a particular status code 5 | # for a certain format, you may uncomment below. 6 | # def render("500.html", _assigns) do 7 | # "Internal Server Error" 8 | # end 9 | 10 | # By default, Phoenix returns the status message from 11 | # the template name. For example, "404.html" becomes 12 | # "Not Found". 13 | def template_not_found(template, _assigns) do 14 | Phoenix.Controller.status_message_from_template(template) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/games_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.LayoutView do 2 | use GamesWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Games.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :games, 7 | version: "0.1.0", 8 | elixir: "~> 1.7", 9 | elixirc_paths: elixirc_paths(Mix.env()), 10 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 11 | start_permanent: Mix.env() == :prod, 12 | aliases: aliases(), 13 | deps: deps() 14 | ] 15 | end 16 | 17 | # Configuration for the OTP application. 18 | # 19 | # Type `mix help compile.app` for more information. 20 | def application do 21 | [ 22 | mod: {Games.Application, []}, 23 | extra_applications: [:logger, :runtime_tools] 24 | ] 25 | end 26 | 27 | # Specifies which paths to compile per environment. 28 | defp elixirc_paths(:test), do: ["lib", "test/support"] 29 | defp elixirc_paths(_), do: ["lib"] 30 | 31 | # Specifies your project dependencies. 32 | # 33 | # Type `mix help deps` for examples and options. 34 | defp deps do 35 | [ 36 | {:phoenix, "~> 1.5.4"}, 37 | {:phoenix_ecto, "~> 4.1"}, 38 | {:ecto_sql, "~> 3.4"}, 39 | {:postgrex, ">= 0.0.0"}, 40 | {:phoenix_live_view, "~> 0.13.0"}, 41 | {:floki, ">= 0.0.0", only: :test}, 42 | {:phoenix_html, "~> 2.11"}, 43 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 44 | {:phoenix_live_dashboard, "~> 0.2"}, 45 | {:telemetry_metrics, "~> 0.4"}, 46 | {:telemetry_poller, "~> 0.4"}, 47 | {:gettext, "~> 0.11"}, 48 | {:jason, "~> 1.0"}, 49 | {:plug_cowboy, "~> 2.0"} 50 | ] 51 | end 52 | 53 | # Aliases are shortcuts or tasks specific to the current project. 54 | # For example, to install project dependencies and perform other setup tasks, run: 55 | # 56 | # $ mix setup 57 | # 58 | # See the documentation for `Mix` for more info on aliases. 59 | defp aliases do 60 | [ 61 | setup: ["deps.get", "ecto.setup", "cmd npm install --prefix assets"], 62 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 63 | "ecto.reset": ["ecto.drop", "ecto.setup"], 64 | test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"] 65 | ] 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"}, 3 | "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, 4 | "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, 5 | "db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"}, 6 | "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"}, 7 | "ecto": {:hex, :ecto, "3.4.5", "2bcd262f57b2c888b0bd7f7a28c8a48aa11dc1a2c6a858e45dd8f8426d504265", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8c6d1d4d524559e9b7a062f0498e2c206122552d63eacff0a6567ffe7a8e8691"}, 8 | "ecto_sql": {:hex, :ecto_sql, "3.4.5", "30161f81b167d561a9a2df4329c10ae05ff36eca7ccc84628f2c8b9fa1e43323", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31990c6a3579b36a3c0841d34a94c275e727de8b84f58509da5f1b2032c98ac2"}, 9 | "file_system": {:hex, :file_system, "0.2.8", "f632bd287927a1eed2b718f22af727c5aeaccc9a98d8c2bd7bff709e851dc986", [:mix], [], "hexpm", "97a3b6f8d63ef53bd0113070102db2ce05352ecf0d25390eb8d747c2bde98bca"}, 10 | "floki": {:hex, :floki, "0.27.0", "6b29a14283f1e2e8fad824bc930eaa9477c462022075df6bea8f0ad811c13599", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "583b8c13697c37179f1f82443bcc7ad2f76fbc0bf4c186606eebd658f7f2631b"}, 11 | "gettext": {:hex, :gettext, "0.18.0", "406d6b9e0e3278162c2ae1de0a60270452c553536772167e2d701f028116f870", [:mix], [], "hexpm", "c3f850be6367ebe1a08616c2158affe4a23231c70391050bf359d5f92f66a571"}, 12 | "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, 13 | "jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"}, 14 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, 15 | "phoenix": {:hex, :phoenix, "1.5.4", "0fca9ce7e960f9498d6315e41fcd0c80bfa6fbeb5fa3255b830c67fdfb7e703f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4e516d131fde87b568abd62e1b14aa07ba7d5edfd230bab4e25cc9dedbb39135"}, 16 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"}, 17 | "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, 18 | "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.2.6", "1b4e1b7d797386b7f9d70d2af931dc9843a5f2f2423609d22cef1eec4e4dba7d", [:mix], [{:phoenix_html, "~> 2.14.1 or ~> 2.15", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.13.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.4.0 or ~> 0.5.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "b20dcad98c4ca63d38a7f5e7a40936e1e8e9da983d3d722b88ae33afb866c9ca"}, 19 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.4", "940c0344b1d66a2e46eef02af3a70e0c5bb45a4db0bf47917add271b76cd3914", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "38f9308357dea4cc77f247e216da99fcb0224e05ada1469167520bed4cb8cccd"}, 20 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.13.3", "2186c55cc7c54ca45b97c6f28cfd267d1c61b5f205f3c83533704cd991bdfdec", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.17 or ~> 1.5.2", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm", "c6309a7da2e779cb9cdf2fb603d75f38f49ef324bedc7a81825998bd1744ff8a"}, 21 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, 22 | "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"}, 23 | "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"}, 24 | "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, 25 | "postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"}, 26 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, 27 | "telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"}, 28 | "telemetry_metrics": {:hex, :telemetry_metrics, "0.5.0", "1b796e74add83abf844e808564275dfb342bcc930b04c7577ab780e262b0d998", [:mix], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "31225e6ce7a37a421a0a96ec55244386aec1c190b22578bd245188a4a33298fd"}, 29 | "telemetry_poller": {:hex, :telemetry_poller, "0.5.1", "21071cc2e536810bac5628b935521ff3e28f0303e770951158c73eaaa01e962a", [:rebar3], [{:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4cab72069210bc6e7a080cec9afffad1b33370149ed5d379b81c7c5f0c663fd4"}, 30 | } 31 | -------------------------------------------------------------------------------- /phoenix_static_buildpack.config: -------------------------------------------------------------------------------- 1 | clean_cache="true" 2 | node_version="14.5.0" 3 | npm_version="6.14.5" 4 | -------------------------------------------------------------------------------- /priv/gettext/en/LC_MESSAGES/errors.po: -------------------------------------------------------------------------------- 1 | ## `msgid`s in this file come from POT (.pot) files. 2 | ## 3 | ## Do not add, change, or remove `msgid`s manually here as 4 | ## they're tied to the ones in the corresponding POT file 5 | ## (with the same domain). 6 | ## 7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge` 8 | ## to merge POT files into PO files. 9 | msgid "" 10 | msgstr "" 11 | "Language: en\n" 12 | 13 | ## From Ecto.Changeset.cast/4 14 | msgid "can't be blank" 15 | msgstr "" 16 | 17 | ## From Ecto.Changeset.unique_constraint/3 18 | msgid "has already been taken" 19 | msgstr "" 20 | 21 | ## From Ecto.Changeset.put_change/3 22 | msgid "is invalid" 23 | msgstr "" 24 | 25 | ## From Ecto.Changeset.validate_acceptance/3 26 | msgid "must be accepted" 27 | msgstr "" 28 | 29 | ## From Ecto.Changeset.validate_format/3 30 | msgid "has invalid format" 31 | msgstr "" 32 | 33 | ## From Ecto.Changeset.validate_subset/3 34 | msgid "has an invalid entry" 35 | msgstr "" 36 | 37 | ## From Ecto.Changeset.validate_exclusion/3 38 | msgid "is reserved" 39 | msgstr "" 40 | 41 | ## From Ecto.Changeset.validate_confirmation/3 42 | msgid "does not match confirmation" 43 | msgstr "" 44 | 45 | ## From Ecto.Changeset.no_assoc_constraint/3 46 | msgid "is still associated with this entry" 47 | msgstr "" 48 | 49 | msgid "are still associated with this entry" 50 | msgstr "" 51 | 52 | ## From Ecto.Changeset.validate_length/3 53 | msgid "should be %{count} character(s)" 54 | msgid_plural "should be %{count} character(s)" 55 | msgstr[0] "" 56 | msgstr[1] "" 57 | 58 | msgid "should have %{count} item(s)" 59 | msgid_plural "should have %{count} item(s)" 60 | msgstr[0] "" 61 | msgstr[1] "" 62 | 63 | msgid "should be at least %{count} character(s)" 64 | msgid_plural "should be at least %{count} character(s)" 65 | msgstr[0] "" 66 | msgstr[1] "" 67 | 68 | msgid "should have at least %{count} item(s)" 69 | msgid_plural "should have at least %{count} item(s)" 70 | msgstr[0] "" 71 | msgstr[1] "" 72 | 73 | msgid "should be at most %{count} character(s)" 74 | msgid_plural "should be at most %{count} character(s)" 75 | msgstr[0] "" 76 | msgstr[1] "" 77 | 78 | msgid "should have at most %{count} item(s)" 79 | msgid_plural "should have at most %{count} item(s)" 80 | msgstr[0] "" 81 | msgstr[1] "" 82 | 83 | ## From Ecto.Changeset.validate_number/3 84 | msgid "must be less than %{number}" 85 | msgstr "" 86 | 87 | msgid "must be greater than %{number}" 88 | msgstr "" 89 | 90 | msgid "must be less than or equal to %{number}" 91 | msgstr "" 92 | 93 | msgid "must be greater than or equal to %{number}" 94 | msgstr "" 95 | 96 | msgid "must be equal to %{number}" 97 | msgstr "" 98 | -------------------------------------------------------------------------------- /priv/gettext/errors.pot: -------------------------------------------------------------------------------- 1 | ## This is a PO Template file. 2 | ## 3 | ## `msgid`s here are often extracted from source code. 4 | ## Add new translations manually only if they're dynamic 5 | ## translations that can't be statically extracted. 6 | ## 7 | ## Run `mix gettext.extract` to bring this file up to 8 | ## date. Leave `msgstr`s empty as changing them here has no 9 | ## effect: edit them in PO (`.po`) files instead. 10 | 11 | ## From Ecto.Changeset.cast/4 12 | msgid "can't be blank" 13 | msgstr "" 14 | 15 | ## From Ecto.Changeset.unique_constraint/3 16 | msgid "has already been taken" 17 | msgstr "" 18 | 19 | ## From Ecto.Changeset.put_change/3 20 | msgid "is invalid" 21 | msgstr "" 22 | 23 | ## From Ecto.Changeset.validate_acceptance/3 24 | msgid "must be accepted" 25 | msgstr "" 26 | 27 | ## From Ecto.Changeset.validate_format/3 28 | msgid "has invalid format" 29 | msgstr "" 30 | 31 | ## From Ecto.Changeset.validate_subset/3 32 | msgid "has an invalid entry" 33 | msgstr "" 34 | 35 | ## From Ecto.Changeset.validate_exclusion/3 36 | msgid "is reserved" 37 | msgstr "" 38 | 39 | ## From Ecto.Changeset.validate_confirmation/3 40 | msgid "does not match confirmation" 41 | msgstr "" 42 | 43 | ## From Ecto.Changeset.no_assoc_constraint/3 44 | msgid "is still associated with this entry" 45 | msgstr "" 46 | 47 | msgid "are still associated with this entry" 48 | msgstr "" 49 | 50 | ## From Ecto.Changeset.validate_length/3 51 | msgid "should be %{count} character(s)" 52 | msgid_plural "should be %{count} character(s)" 53 | msgstr[0] "" 54 | msgstr[1] "" 55 | 56 | msgid "should have %{count} item(s)" 57 | msgid_plural "should have %{count} item(s)" 58 | msgstr[0] "" 59 | msgstr[1] "" 60 | 61 | msgid "should be at least %{count} character(s)" 62 | msgid_plural "should be at least %{count} character(s)" 63 | msgstr[0] "" 64 | msgstr[1] "" 65 | 66 | msgid "should have at least %{count} item(s)" 67 | msgid_plural "should have at least %{count} item(s)" 68 | msgstr[0] "" 69 | msgstr[1] "" 70 | 71 | msgid "should be at most %{count} character(s)" 72 | msgid_plural "should be at most %{count} character(s)" 73 | msgstr[0] "" 74 | msgstr[1] "" 75 | 76 | msgid "should have at most %{count} item(s)" 77 | msgid_plural "should have at most %{count} item(s)" 78 | msgstr[0] "" 79 | msgstr[1] "" 80 | 81 | ## From Ecto.Changeset.validate_number/3 82 | msgid "must be less than %{number}" 83 | msgstr "" 84 | 85 | msgid "must be greater than %{number}" 86 | msgstr "" 87 | 88 | msgid "must be less than or equal to %{number}" 89 | msgstr "" 90 | 91 | msgid "must be greater than or equal to %{number}" 92 | msgstr "" 93 | 94 | msgid "must be equal to %{number}" 95 | msgstr "" 96 | -------------------------------------------------------------------------------- /priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /priv/repo/seeds.exs: -------------------------------------------------------------------------------- 1 | # Script for populating the database. You can run it as: 2 | # 3 | # mix run priv/repo/seeds.exs 4 | # 5 | # Inside the script, you can read and write to any of your 6 | # repositories directly: 7 | # 8 | # Games.Repo.insert!(%Games.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /test/games_web/live/page_live_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.PageLiveTest do 2 | use GamesWeb.ConnCase 3 | 4 | import Phoenix.LiveViewTest 5 | 6 | test "disconnected and connected render", %{conn: conn} do 7 | {:ok, page_live, disconnected_html} = live(conn, "/") 8 | assert disconnected_html =~ "div id=\"elm\"" 9 | assert render(page_live) =~ "div id=\"elm\"" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/games_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.ErrorViewTest do 2 | use GamesWeb.ConnCase, async: true 3 | 4 | # Bring render/3 and render_to_string/3 for testing custom views 5 | import Phoenix.View 6 | 7 | test "renders 404.html" do 8 | assert render_to_string(GamesWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(GamesWeb.ErrorView, "500.html", []) == "Internal Server Error" 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/games_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.LayoutViewTest do 2 | use GamesWeb.ConnCase, async: true 3 | 4 | # When testing helpers, you may want to import Phoenix.HTML and 5 | # use functions such as safe_to_string() to convert the helper 6 | # result into an HTML string. 7 | # import Phoenix.HTML 8 | end 9 | -------------------------------------------------------------------------------- /test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.ChannelCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | channel tests. 5 | 6 | Such tests rely on `Phoenix.ChannelTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use GamesWeb.ChannelCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with channels 23 | import Phoenix.ChannelTest 24 | import GamesWeb.ChannelCase 25 | 26 | # The default endpoint for testing 27 | @endpoint GamesWeb.Endpoint 28 | end 29 | end 30 | 31 | setup tags do 32 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Games.Repo) 33 | 34 | unless tags[:async] do 35 | Ecto.Adapters.SQL.Sandbox.mode(Games.Repo, {:shared, self()}) 36 | end 37 | 38 | :ok 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule GamesWeb.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common data structures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | we enable the SQL sandbox, so changes done to the database 12 | are reverted at the end of every test. If you are using 13 | PostgreSQL, you can even run database tests asynchronously 14 | by setting `use GamesWeb.ConnCase, async: true`, although 15 | this option is not recommended for other databases. 16 | """ 17 | 18 | use ExUnit.CaseTemplate 19 | 20 | using do 21 | quote do 22 | # Import conveniences for testing with connections 23 | import Plug.Conn 24 | import Phoenix.ConnTest 25 | import GamesWeb.ConnCase 26 | 27 | alias GamesWeb.Router.Helpers, as: Routes 28 | 29 | # The default endpoint for testing 30 | @endpoint GamesWeb.Endpoint 31 | end 32 | end 33 | 34 | setup tags do 35 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Games.Repo) 36 | 37 | unless tags[:async] do 38 | Ecto.Adapters.SQL.Sandbox.mode(Games.Repo, {:shared, self()}) 39 | end 40 | 41 | {:ok, conn: Phoenix.ConnTest.build_conn()} 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule Games.DataCase do 2 | @moduledoc """ 3 | This module defines the setup for tests requiring 4 | access to the application's data layer. 5 | 6 | You may define functions here to be used as helpers in 7 | your tests. 8 | 9 | Finally, if the test case interacts with the database, 10 | we enable the SQL sandbox, so changes done to the database 11 | are reverted at the end of every test. If you are using 12 | PostgreSQL, you can even run database tests asynchronously 13 | by setting `use Games.DataCase, async: true`, although 14 | this option is not recommended for other databases. 15 | """ 16 | 17 | use ExUnit.CaseTemplate 18 | 19 | using do 20 | quote do 21 | alias Games.Repo 22 | 23 | import Ecto 24 | import Ecto.Changeset 25 | import Ecto.Query 26 | import Games.DataCase 27 | end 28 | end 29 | 30 | setup tags do 31 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(Games.Repo) 32 | 33 | unless tags[:async] do 34 | Ecto.Adapters.SQL.Sandbox.mode(Games.Repo, {:shared, self()}) 35 | end 36 | 37 | :ok 38 | end 39 | 40 | @doc """ 41 | A helper that transforms changeset errors into a map of messages. 42 | 43 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 44 | assert "password is too short" in errors_on(changeset).password 45 | assert %{password: ["password is too short"]} = errors_on(changeset) 46 | 47 | """ 48 | def errors_on(changeset) do 49 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 50 | Regex.replace(~r"%{(\w+)}", message, fn _, key -> 51 | opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() 52 | end) 53 | end) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(Games.Repo, :manual) 3 | --------------------------------------------------------------------------------