├── apps ├── game │ ├── test │ │ ├── test_helper.exs │ │ └── game_test.exs │ ├── lib │ │ ├── hive.ex │ │ ├── tile.ex │ │ ├── tree.ex │ │ ├── honey_drop.ex │ │ ├── viewport.ex │ │ ├── field.ex │ │ ├── bear.ex │ │ ├── player.ex │ │ ├── bee.ex │ │ └── game_server.ex │ ├── .formatter.exs │ ├── README.md │ ├── .gitignore │ ├── mix.exs │ └── config │ │ └── config.exs ├── bear_necessities │ ├── README.md │ ├── priv │ │ └── repo │ │ │ ├── migrations │ │ │ └── .formatter.exs │ │ │ └── seeds.exs │ ├── test │ │ ├── test_helper.exs │ │ └── support │ │ │ └── data_case.ex │ ├── lib │ │ ├── bear_necessities │ │ │ ├── repo.ex │ │ │ └── application.ex │ │ └── bear_necessities.ex │ ├── .formatter.exs │ ├── config │ │ ├── config.exs │ │ ├── prod.exs │ │ ├── dev.exs │ │ └── test.exs │ ├── .gitignore │ └── mix.exs └── bear_necessities_web │ ├── assets │ ├── .babelrc │ ├── static │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── bear │ │ │ │ ├── up.gif │ │ │ │ ├── dead.gif │ │ │ │ ├── down.gif │ │ │ │ ├── left.gif │ │ │ │ ├── right.gif │ │ │ │ ├── up-claw.gif │ │ │ │ ├── up-idle.gif │ │ │ │ ├── down-claw.gif │ │ │ │ ├── down-idle.gif │ │ │ │ ├── left-claw.gif │ │ │ │ ├── left-idle.gif │ │ │ │ ├── right-claw.gif │ │ │ │ └── right-idle.gif │ │ │ ├── bees │ │ │ │ ├── bees.gif │ │ │ │ └── bees-1.gif │ │ │ ├── honey │ │ │ │ └── drop.gif │ │ │ ├── trees │ │ │ │ ├── tree-1.gif │ │ │ │ └── treehive.gif │ │ │ └── terain │ │ │ │ ├── grass-1.gif │ │ │ │ ├── grass-2.gif │ │ │ │ ├── grass-3.gif │ │ │ │ ├── grass-4.gif │ │ │ │ └── nothing.gif │ │ ├── sound │ │ │ └── game-sound.mp3 │ │ └── robots.txt │ ├── postcss.config.js │ ├── css │ │ ├── base.css │ │ ├── app.css │ │ ├── game │ │ │ ├── lobby.css │ │ │ ├── scoreboard.css │ │ │ └── game.css │ │ └── form.css │ ├── js │ │ ├── app.js │ │ └── socket.js │ ├── package.json │ └── webpack.config.js │ ├── test │ ├── test_helper.exs │ ├── bear_necessities_web │ │ └── views │ │ │ ├── layout_view_test.exs │ │ │ ├── error_view_test.exs │ │ │ └── playfield_test.exs │ └── support │ │ ├── channel_case.ex │ │ └── conn_case.ex │ ├── .formatter.exs │ ├── lib │ ├── bear_necessities_web │ │ ├── views │ │ │ ├── layout_view.ex │ │ │ ├── error_view.ex │ │ │ ├── playfield.ex │ │ │ └── error_helpers.ex │ │ ├── templates │ │ │ ├── playfield │ │ │ │ ├── scoreboard.html.leex │ │ │ │ └── template.html.leex │ │ │ └── layout │ │ │ │ └── app.html.eex │ │ ├── router.ex │ │ ├── gettext.ex │ │ ├── application.ex │ │ ├── channels │ │ │ └── user_socket.ex │ │ ├── endpoint.ex │ │ └── live │ │ │ └── game.ex │ └── bear_necessities_web.ex │ ├── config │ ├── test.exs │ ├── config.exs │ ├── dev.exs │ └── prod.exs │ ├── README.md │ ├── .gitignore │ ├── mix.exs │ └── priv │ └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── Procfile ├── elixir_buildpack.config ├── .formatter.exs ├── artwork ├── back.aseprite ├── death.aseprite ├── front.aseprite ├── grave.aseprite ├── rotate.aseprite ├── side.aseprite ├── back-attack.aseprite ├── side-attack.aseprite └── front-attack.aseprite ├── config ├── prod.exs ├── test.exs ├── dev.exs └── config.exs ├── phoenix_static_buildpack.config ├── .gitignore ├── mix.exs ├── README.md ├── circle.yml └── mix.lock /apps/game/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: mix phx.server 2 | release: POOL_SIZE=2 mix ecto.migrate 3 | -------------------------------------------------------------------------------- /apps/bear_necessities/README.md: -------------------------------------------------------------------------------- 1 | # BearNecessities 2 | 3 | **TODO: Add description** 4 | -------------------------------------------------------------------------------- /apps/game/lib/hive.ex: -------------------------------------------------------------------------------- 1 | defmodule Hive do 2 | defstruct hp: nil, honey: nil 3 | end 4 | -------------------------------------------------------------------------------- /apps/game/lib/tile.ex: -------------------------------------------------------------------------------- 1 | defmodule Tile do 2 | defstruct [:type, :texture] 3 | end 4 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /elixir_buildpack.config: -------------------------------------------------------------------------------- 1 | erlang_version=21.1.4 2 | elixir_version=1.8.1 3 | always_rebuild=true 4 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "config/*.exs"], 3 | subdirectories: ["apps/*"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/game/lib/tree.ex: -------------------------------------------------------------------------------- 1 | defmodule Tree do 2 | defstruct pos_x: nil, pos_y: nil, hive: nil 3 | end 4 | -------------------------------------------------------------------------------- /apps/game/lib/honey_drop.ex: -------------------------------------------------------------------------------- 1 | defmodule HoneyDrop do 2 | defstruct pos_x: nil, pos_y: nil, honey: 1 3 | end 4 | -------------------------------------------------------------------------------- /artwork/back.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/back.aseprite -------------------------------------------------------------------------------- /artwork/death.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/death.aseprite -------------------------------------------------------------------------------- /artwork/front.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/front.aseprite -------------------------------------------------------------------------------- /artwork/grave.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/grave.aseprite -------------------------------------------------------------------------------- /artwork/rotate.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/rotate.aseprite -------------------------------------------------------------------------------- /artwork/side.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/side.aseprite -------------------------------------------------------------------------------- /artwork/back-attack.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/back-attack.aseprite -------------------------------------------------------------------------------- /artwork/side-attack.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/side-attack.aseprite -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Do not print debug messages in production 4 | config :logger, level: :info 5 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Print only warnings and errors during test 4 | config :logger, level: :warn 5 | -------------------------------------------------------------------------------- /apps/bear_necessities/priv/repo/migrations/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto_sql], 3 | inputs: ["*.exs"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/bear_necessities/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(BearNecessities.Repo, :manual) 3 | -------------------------------------------------------------------------------- /artwork/front-attack.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/artwork/front-attack.aseprite -------------------------------------------------------------------------------- /apps/bear_necessities_web/test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | Ecto.Adapters.SQL.Sandbox.mode(BearNecessities.Repo, :manual) 3 | -------------------------------------------------------------------------------- /apps/game/.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:phoenix], 3 | inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /phoenix_static_buildpack.config: -------------------------------------------------------------------------------- 1 | node_version=10.15.3 2 | yarn_version=1.15.0 3 | phoenix_relative_path=/apps/bear_necessities_web/ 4 | remove_node=true 5 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.LayoutView do 2 | use BearNecessitiesWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/favicon.ico -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/up.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/up.gif -------------------------------------------------------------------------------- /apps/game/lib/viewport.ex: -------------------------------------------------------------------------------- 1 | defmodule ViewPort do 2 | defstruct fields: [] 3 | 4 | def get_viewport(id) do 5 | GenServer.call(Game, {:get_viewport, id}) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/dead.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/dead.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/down.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/left.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/left.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bees/bees.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bees/bees.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/sound/game-sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/sound/game-sound.mp3 -------------------------------------------------------------------------------- /apps/bear_necessities_web/test/bear_necessities_web/views/layout_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.LayoutViewTest do 2 | use BearNecessitiesWeb.ConnCase, async: true 3 | end 4 | -------------------------------------------------------------------------------- /apps/bear_necessities/lib/bear_necessities/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessities.Repo do 2 | use Ecto.Repo, 3 | otp_app: :bear_necessities, 4 | adapter: Ecto.Adapters.Postgres 5 | end 6 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/right.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/right.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/up-claw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/up-claw.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/up-idle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/up-idle.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bees/bees-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bees/bees-1.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/honey/drop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/honey/drop.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/trees/tree-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/trees/tree-1.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/down-claw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/down-claw.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/down-idle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/down-idle.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/left-claw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/left-claw.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/left-idle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/left-idle.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/terain/grass-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/terain/grass-1.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/terain/grass-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/terain/grass-2.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/terain/grass-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/terain/grass-3.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/terain/grass-4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/terain/grass-4.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/terain/nothing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/terain/nothing.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/trees/treehive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/trees/treehive.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/right-claw.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/right-claw.gif -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/static/images/bear/right-idle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DefactoSoftware/BearNecessities/HEAD/apps/bear_necessities_web/assets/static/images/bear/right-idle.gif -------------------------------------------------------------------------------- /apps/bear_necessities/.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | import_deps: [:ecto], 3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | subdirectories: ["priv/*/migrations"] 5 | ] 6 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ident: "postcss", 3 | plugins: { 4 | "postcss-preset-env": { 5 | stage: 0 6 | }, 7 | "postcss-import": {}, 8 | "postcss-nested": {} 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/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 | -------------------------------------------------------------------------------- /apps/bear_necessities/lib/bear_necessities.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessities do 2 | @moduledoc """ 3 | BearNecessities 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 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/css/base.css: -------------------------------------------------------------------------------- 1 | @import "../node_modules/normalize.css/normalize.css"; 2 | 3 | body { 4 | background: #363636; 5 | color: white; 6 | font-family: "Press Start 2P", Courier, monospace; 7 | font-size: 16px; 8 | margin: 0; 9 | text-shadow: 2px 2px rgba(0, 0, 0, 0.8); 10 | } 11 | 12 | h1 { 13 | text-shadow: 6px 6px rgba(0, 0, 0, 0.8); 14 | } 15 | -------------------------------------------------------------------------------- /apps/bear_necessities/config/config.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | config :bear_necessities, 8 | ecto_repos: [BearNecessities.Repo] 9 | 10 | import_config "#{Mix.env()}.exs" 11 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Press+Start+2P"); 2 | @import "../../../../deps/phoenix_live_view/assets/css/live_view.css"; 3 | 4 | @import "./base.css"; 5 | @import "./form.css"; 6 | @import "./game/lobby.css"; 7 | @import "./game/scoreboard.css"; 8 | @import "./game/game.css"; 9 | 10 | .preload-images { 11 | height: 0; 12 | width: 0; 13 | z-index: -1; 14 | } 15 | -------------------------------------------------------------------------------- /apps/bear_necessities/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 | # BearNecessities.Repo.insert!(%BearNecessities.SomeSchema{}) 9 | # 10 | # We recommend using the bang functions (`insert!`, `update!` 11 | # and so on) as they will fail if something goes wrong. 12 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/css/game/lobby.css: -------------------------------------------------------------------------------- 1 | .lobby { 2 | background: #44d873 url("/images/terain/grass-1.gif"); 3 | color: white; 4 | text-align: center; 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | bottom: 0; 9 | right: 0; 10 | image-rendering: pixelated; 11 | background-size: 20%; 12 | padding: 30px; 13 | 14 | h1 { 15 | font-size: 3em; 16 | } 17 | 18 | .player-image { 19 | width: 200px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | # Do not include metadata nor timestamps in development logs 4 | config :logger, :console, format: "[$level] $message\n" 5 | 6 | # Initialize plugs at runtime for faster development compilation 7 | config :phoenix, :plug_init_mode, :runtime 8 | 9 | # Set a higher stacktrace during development. Avoid configuring such 10 | # in production as building large stacktraces may be expensive. 11 | config :phoenix, :stacktrace_depth, 20 12 | -------------------------------------------------------------------------------- /apps/bear_necessities/lib/bear_necessities/application.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessities.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 | BearNecessities.Repo, 11 | Game 12 | ] 13 | 14 | Supervisor.start_link(children, strategy: :one_for_one, name: BearNecessities.Supervisor) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /apps/bear_necessities/config/prod.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | # Configure your database 8 | config :bear_necessities, BearNecessities.Repo, 9 | url: System.get_env("DATABASE_URL"), 10 | pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"), 11 | ssl: true 12 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/config/test.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities_web application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | # We don't run a server during test. If one is required, 8 | # you can enable the server option below. 9 | config :bear_necessities_web, BearNecessitiesWeb.Endpoint, 10 | http: [port: 4002], 11 | server: false 12 | -------------------------------------------------------------------------------- /apps/bear_necessities/config/dev.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | # Configure your database 8 | config :bear_necessities, BearNecessities.Repo, 9 | username: "postgres", 10 | password: "postgres", 11 | database: "bear_necessities_dev", 12 | hostname: "localhost", 13 | pool_size: 10 14 | -------------------------------------------------------------------------------- /apps/bear_necessities/config/test.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | # Configure your database 8 | config :bear_necessities, BearNecessities.Repo, 9 | username: "postgres", 10 | password: "postgres", 11 | database: "bear_necessities_test", 12 | hostname: "localhost", 13 | pool: Ecto.Adapters.SQL.Sandbox 14 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/templates/playfield/scoreboard.html.leex: -------------------------------------------------------------------------------- 1 |
2 |

Leaderboard

3 | 4 | <%= if Enum.any? @players do %> 5 | 12 | <% end %> 13 |
14 | -------------------------------------------------------------------------------- /apps/game/README.md: -------------------------------------------------------------------------------- 1 | # Game 2 | 3 | **TODO: Add description** 4 | 5 | ## Installation 6 | 7 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 | by adding `game` to your list of dependencies in `mix.exs`: 9 | 10 | ```elixir 11 | def deps do 12 | [ 13 | {:game, "~> 0.1.0"} 14 | ] 15 | end 16 | ``` 17 | 18 | Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 | and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 | be found at [https://hexdocs.pm/game](https://hexdocs.pm/game). 21 | 22 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/test/bear_necessities_web/views/error_view_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.ErrorViewTest do 2 | use BearNecessitiesWeb.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(BearNecessitiesWeb.ErrorView, "404.html", []) == "Not Found" 9 | end 10 | 11 | test "renders 500.html" do 12 | assert render_to_string(BearNecessitiesWeb.ErrorView, "500.html", []) == 13 | "Internal Server Error" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # The directory Mix will write compiled artifacts to. 4 | /_build/ 5 | 6 | # If you run "mix test --cover", coverage assets end up here. 7 | /cover/ 8 | 9 | # The directory Mix downloads your dependencies sources to. 10 | /deps/ 11 | 12 | # Where 3rd-party dependencies like ExDoc output generated docs. 13 | /doc/ 14 | 15 | # Ignore .fetch files in case you like to edit your project deps locally. 16 | /.fetch 17 | 18 | # If the VM crashes, it generates a dump, let's ignore it too. 19 | erl_crash.dump 20 | 21 | # Also ignore archive artifacts (built via "mix archive.build"). 22 | *.ez 23 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.ErrorView do 2 | use BearNecessitiesWeb, :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 | -------------------------------------------------------------------------------- /apps/game/.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 third-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 | game-*.tar 24 | 25 | -------------------------------------------------------------------------------- /apps/game/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Game.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :game, 7 | version: "0.1.0", 8 | elixir: "~> 1.8", 9 | start_permanent: Mix.env() == :prod, 10 | deps: deps() 11 | ] 12 | end 13 | 14 | # Run "mix help compile.app" to learn about applications. 15 | def application do 16 | [ 17 | extra_applications: [:logger] 18 | ] 19 | end 20 | 21 | # Run "mix help deps" to learn about dependencies. 22 | defp deps do 23 | [ 24 | # {:dep_from_hexpm, "~> 0.3.0"}, 25 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 26 | ] 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.Router do 2 | use BearNecessitiesWeb, :router 3 | 4 | pipeline :browser do 5 | plug :accepts, ["html"] 6 | plug :fetch_session 7 | plug :fetch_flash 8 | plug Phoenix.LiveView.Flash 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 | scope "/", BearNecessitiesWeb do 18 | pipe_through :browser 19 | 20 | live "/", Game 21 | end 22 | 23 | # Other scopes may use custom stacks. 24 | # scope "/api", BearNecessitiesWeb do 25 | # pipe_through :api 26 | # end 27 | end 28 | -------------------------------------------------------------------------------- /apps/game/lib/field.ex: -------------------------------------------------------------------------------- 1 | defmodule Field do 2 | defstruct [:height, :width, :tiles] 3 | 4 | @textures %{ 5 | grass: 4 6 | } 7 | 8 | def create_field(height, width) do 9 | tiles = create_tiles(height, width) 10 | 11 | %Field{ 12 | height: height, 13 | width: width, 14 | tiles: tiles 15 | } 16 | end 17 | 18 | defp create_tiles(height, width) do 19 | Enum.map(1..height, fn _row -> 20 | Enum.map(1..width, fn _col -> 21 | create_tile(:grass) 22 | end) 23 | end) 24 | end 25 | 26 | defp create_tile(type) do 27 | texture_count = Map.get(@textures, type) 28 | 29 | %Tile{type: type, texture: :rand.uniform(texture_count)} 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BearNecessities.Umbrella.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | start_permanent: Mix.env() == :prod, 8 | deps: deps() 9 | ] 10 | end 11 | 12 | # Dependencies can be Hex packages: 13 | # 14 | # {:mydep, "~> 0.3.0"} 15 | # 16 | # Or git/path repositories: 17 | # 18 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 19 | # 20 | # Type "mix help deps" for more examples and options. 21 | # 22 | # Dependencies listed here are available only for this project 23 | # and cannot be accessed from applications inside the apps folder 24 | defp deps do 25 | [] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/js/app.js: -------------------------------------------------------------------------------- 1 | // We need to import the CSS so that webpack will load it. 2 | // The MiniCssExtractPlugin is used to separate it out into 3 | // its own CSS file. 4 | import css from "../css/app.css" 5 | 6 | // webpack automatically bundles all modules in your 7 | // entry points. Those entry points can be configured 8 | // in "webpack.config.js". 9 | // 10 | // Import dependencies 11 | // 12 | import "phoenix_html" 13 | 14 | // Import local files 15 | // 16 | // Local files can be imported directly using relative paths, for example: 17 | // import socket from "./socket" 18 | 19 | // Connect to LiveView socket 20 | import {Socket} from "phoenix" 21 | import LiveSocket from "phoenix_live_view" 22 | 23 | let liveSocket = new LiveSocket("/live", Socket) 24 | liveSocket.connect() 25 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/README.md: -------------------------------------------------------------------------------- 1 | # BearNecessitiesWeb 2 | 3 | To start your Phoenix server: 4 | 5 | * Install dependencies with `mix deps.get` 6 | * Create and migrate your database with `mix ecto.setup` 7 | * Install Node.js dependencies with `cd assets && npm install` 8 | * Start Phoenix endpoint with `mix phx.server` 9 | 10 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 11 | 12 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html). 13 | 14 | ## Learn more 15 | 16 | * Official website: http://www.phoenixframework.org/ 17 | * Guides: https://hexdocs.pm/phoenix/overview.html 18 | * Docs: https://hexdocs.pm/phoenix 19 | * Mailing list: http://groups.google.com/group/phoenix-talk 20 | * Source: https://github.com/phoenixframework/phoenix 21 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/css/game/scoreboard.css: -------------------------------------------------------------------------------- 1 | .scoreboard { 2 | .players { 3 | list-style: none; 4 | margin: 0 auto; 5 | margin: 0; 6 | max-width: 500px; 7 | padding: 0; 8 | text-align: left; 9 | } 10 | 11 | .player { 12 | display: flex; 13 | width: 100%; 14 | 15 | &::before { 16 | background-image: url("/images/bear/idle.gif"); 17 | background-size: cover; 18 | content: ""; 19 | display: inline-block; 20 | flex: 0 0 30px; 21 | height: 32px; 22 | margin-right: 16px; 23 | vertical-align: middle; 24 | width: 32px; 25 | } 26 | 27 | &.self { 28 | color: lime; 29 | } 30 | 31 | .player-name { 32 | flex: 1; 33 | line-height: 1.5em; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | white-space: nowrap; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/gettext.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.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 BearNecessitiesWeb.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: :bear_necessities_web 24 | end 25 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | BearNecessities · Phoenix Live Views 8 | "> 9 | 10 | 11 | <%= if flash_info = get_flash(@conn, :info) do %> 12 | 13 | <% end %> 14 | <%= if flash_error = get_flash(@conn, :error) do %> 15 | 16 | <% end %> 17 | 18 | <%= render @view_module, @view_template, assigns %> 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /apps/bear_necessities/.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 | bear_necessities-*.tar 24 | 25 | # Files matching config/*.secret.exs pattern contain sensitive 26 | # data and you should not commit them into version control. 27 | # 28 | # Alternatively, you may comment the line below and commit the 29 | # secrets files as long as you replace their contents by environment 30 | # variables. 31 | /config/*.secret.exs 32 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "repository": {}, 3 | "license": "MIT", 4 | "scripts": { 5 | "deploy": "webpack --mode production", 6 | "watch": "webpack --mode development --watch" 7 | }, 8 | "dependencies": { 9 | "normalize.css": "^8.0.1", 10 | "phoenix": "file:../../../deps/phoenix", 11 | "phoenix_html": "file:../../../deps/phoenix_html", 12 | "phoenix_live_view": "file:../../../deps/phoenix_live_view" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.0.0", 16 | "@babel/preset-env": "^7.0.0", 17 | "babel-loader": "^8.0.0", 18 | "copy-webpack-plugin": "^4.5.0", 19 | "css-loader": "^2.1.1", 20 | "mini-css-extract-plugin": "^0.4.0", 21 | "optimize-css-assets-webpack-plugin": "^4.0.0", 22 | "postcss-import": "^12.0.1", 23 | "postcss-loader": "^3.0.0", 24 | "postcss-nested": "^4.1.2", 25 | "postcss-preset-env": "^6.6.0", 26 | "uglifyjs-webpack-plugin": "^1.2.4", 27 | "webpack": "4.4.0", 28 | "webpack-cli": "^2.0.10" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/application.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.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 | # List all child processes to be supervised 10 | children = [ 11 | # Start the endpoint when the application starts 12 | BearNecessitiesWeb.Endpoint 13 | # Starts a worker by calling: BearNecessitiesWeb.Worker.start_link(arg) 14 | # {BearNecessitiesWeb.Worker, arg}, 15 | ] 16 | 17 | # See https://hexdocs.pm/elixir/Supervisor.html 18 | # for other strategies and supported options 19 | opts = [strategy: :one_for_one, name: BearNecessitiesWeb.Supervisor] 20 | Supervisor.start_link(children, opts) 21 | end 22 | 23 | # Tell Phoenix to update the endpoint configuration 24 | # whenever the application is updated. 25 | def config_change(changed, _new, removed) do 26 | BearNecessitiesWeb.Endpoint.config_change(changed, removed) 27 | :ok 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /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 | use Mix.Config 4 | 5 | # By default, the umbrella project as well as each child 6 | # application will require this configuration file, as 7 | # configuration and dependencies are shared in an umbrella 8 | # project. While one could configure all applications here, 9 | # we prefer to keep the configuration of each individual 10 | # child application in their own app, but all other 11 | # dependencies, regardless if they belong to one or multiple 12 | # apps, should be configured in the umbrella to avoid confusion. 13 | import_config "../apps/*/config/config.exs" 14 | 15 | # Configures Elixir's Logger 16 | config :logger, :console, 17 | format: "$time $metadata[$level] $message\n", 18 | metadata: [:request_id] 19 | 20 | # Use Jason for JSON parsing in Phoenix 21 | config :phoenix, :json_library, Jason 22 | 23 | # Import environment specific config. This must remain at the bottom 24 | # of this file so it overrides the configuration defined above. 25 | import_config "#{Mix.env()}.exs" 26 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/config/config.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities_web application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | # General application configuration 8 | config :bear_necessities_web, 9 | ecto_repos: [BearNecessities.Repo], 10 | generators: [context_app: :bear_necessities] 11 | 12 | # Configures the endpoint 13 | config :bear_necessities_web, BearNecessitiesWeb.Endpoint, 14 | url: [host: "localhost"], 15 | secret_key_base: "XGyIQiCWhwimCTtX1G470XJdDMgNqYBgjETuYNYeqbYLHj1ATH2IaTtM2Z5iF8JN", 16 | render_errors: [view: BearNecessitiesWeb.ErrorView, accepts: ~w(html json)], 17 | live_view: [signing_salt: "0nq3UJ4Q"], 18 | pubsub: [name: BearNecessitiesWeb.PubSub, adapter: Phoenix.PubSub.PG2] 19 | 20 | # Import environment specific config. This must remain at the bottom 21 | # of this file so it overrides the configuration defined above. 22 | import_config "#{Mix.env()}.exs" 23 | 24 | config :phoenix, 25 | template_engines: [leex: Phoenix.LiveView.Engine] 26 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/test/support/channel_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.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 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with channels 21 | use Phoenix.ChannelTest 22 | 23 | # The default endpoint for testing 24 | @endpoint BearNecessitiesWeb.Endpoint 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(BearNecessities.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(BearNecessities.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BearNecessities.Umbrella 2 | 3 | | Front | Side | Back | 4 | | - | - | -| 5 | | ![front](https://user-images.githubusercontent.com/801034/55959643-75419880-5c6b-11e9-844b-6f094e26b4a4.gif) | ![left](https://user-images.githubusercontent.com/801034/55959641-74a90200-5c6b-11e9-99c1-fd813725697c.gif) | ![back](https://user-images.githubusercontent.com/801034/55959642-75419880-5c6b-11e9-8e10-f65c94062c9f.gif) | 6 | | ![front-attack](https://user-images.githubusercontent.com/801034/56365798-feb81400-61f1-11e9-8bbf-c565e847701b.gif) | ![left-attack](https://user-images.githubusercontent.com/801034/56423160-23bc8d80-62ab-11e9-8725-fe49fe3f050b.gif) | ![back-attack](https://user-images.githubusercontent.com/801034/56423159-23bc8d80-62ab-11e9-9dab-cdc94193cb8f.gif) | 7 | 8 | | Death | 9 | | - | 10 | | ![ded](https://user-images.githubusercontent.com/801034/56366001-6a01e600-61f2-11e9-8a5e-b5107cef87bd.gif) | 11 | 12 | ## Install 13 | 14 | * Run `mix deps.get` in the main dir 15 | * Run `mix ecto.create` in the main dir 16 | * Run `yarn` in `apps/bear_necessities_web/assets` 17 | 18 | ## Running 19 | 20 | Start with mix phx.server, go to https://localhost:4000/ and wait for the assets to compile 21 | -------------------------------------------------------------------------------- /apps/game/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 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # third-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :game, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:game, :key) 18 | # 19 | # You can also configure a third-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env()}.exs" 31 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/channels/user_socket.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.UserSocket do 2 | use Phoenix.Socket 3 | 4 | ## Channels 5 | # channel "room:*", BearNecessitiesWeb.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 | def connect(_params, socket, _connect_info) do 19 | {:ok, socket} 20 | end 21 | 22 | # Socket id's are topics that allow you to identify all sockets for a given user: 23 | # 24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}" 25 | # 26 | # Would allow you to broadcast a "disconnect" event and terminate 27 | # all active sockets and channels for a given user: 28 | # 29 | # BearNecessitiesWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) 30 | # 31 | # Returning `nil` makes this socket anonymous. 32 | def id(_socket), do: nil 33 | end 34 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.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 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | 16 | use ExUnit.CaseTemplate 17 | 18 | using do 19 | quote do 20 | # Import conveniences for testing with connections 21 | use Phoenix.ConnTest 22 | alias BearNecessitiesWeb.Router.Helpers, as: Routes 23 | 24 | # The default endpoint for testing 25 | @endpoint BearNecessitiesWeb.Endpoint 26 | end 27 | end 28 | 29 | setup tags do 30 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(BearNecessities.Repo) 31 | 32 | unless tags[:async] do 33 | Ecto.Adapters.SQL.Sandbox.mode(BearNecessities.Repo, {:shared, self()}) 34 | end 35 | 36 | {:ok, conn: Phoenix.ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const glob = require("glob"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); 5 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 6 | const CopyWebpackPlugin = require("copy-webpack-plugin"); 7 | 8 | module.exports = (env, options) => ({ 9 | optimization: { 10 | minimizer: [ 11 | new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }), 12 | new OptimizeCSSAssetsPlugin({}) 13 | ] 14 | }, 15 | entry: { 16 | "./js/app.js": ["./js/app.js"].concat(glob.sync("./vendor/**/*.js")) 17 | }, 18 | output: { 19 | filename: "app.js", 20 | path: path.resolve(__dirname, "../priv/static/js") 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | exclude: /node_modules/, 27 | use: { 28 | loader: "babel-loader" 29 | } 30 | }, 31 | { 32 | test: /\.css$/, 33 | use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"] 34 | } 35 | ] 36 | }, 37 | plugins: [ 38 | new MiniCssExtractPlugin({ filename: "../css/app.css" }), 39 | new CopyWebpackPlugin([{ from: "static/", to: "../" }]) 40 | ] 41 | }); 42 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/.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 | bear_necessities_web-*.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 web/static, 32 | # we ignore priv/static. You may want to comment 33 | # this depending on your deployment strategy. 34 | /priv/static/ 35 | 36 | # Files matching config/*.secret.exs pattern contain sensitive 37 | # data and you should not commit them into version control. 38 | # 39 | # Alternatively, you may comment the line below and commit the 40 | # secrets files as long as you replace their contents by environment 41 | # variables. 42 | /config/*.secret.exs 43 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/css/form.css: -------------------------------------------------------------------------------- 1 | .button, 2 | .input { 3 | position: relative; 4 | font-size: 20px; 5 | padding: 15px; 6 | } 7 | .input { 8 | border: 6px solid #363636; 9 | box-shadow: inset -0.25em -0.25em 0px 0px #a9a9a9; 10 | } 11 | .button { 12 | background: #f6f6f6; 13 | box-shadow: inset -0.25em -0.25em 0px 0px #a9a9a9; 14 | color: #363636; 15 | text-align: center; 16 | text-decoration: none; 17 | cursor: pointer; 18 | } 19 | 20 | .button::before, 21 | .button::after { 22 | content: ""; 23 | position: absolute; 24 | width: 100%; 25 | height: 100%; 26 | box-sizing: content-box; 27 | } 28 | 29 | .button:before { 30 | top: -6px; 31 | left: 0; 32 | border-top: 6px #363636 solid; 33 | border-bottom: 6px #363636 solid; 34 | } 35 | 36 | .button::after { 37 | left: -6px; 38 | top: 0; 39 | border-left: 6px #363636 solid; 40 | border-right: 6px #363636 solid; 41 | } 42 | 43 | .button.red { 44 | color: white; 45 | background: #e76e55; 46 | box-shadow: inset -0.25em -0.25em 0px 0px #8c2022; 47 | } 48 | .button.red:hover, 49 | .button.red:focus { 50 | background: #ce372b; 51 | box-shadow: inset -0.35em -0.35em 0px 0px #8c2022; 52 | } 53 | 54 | .button.green { 55 | color: white; 56 | background: #92cd41; 57 | box-shadow: inset -0.25em -0.25em 0px 0px #4aa52e; 58 | } 59 | .button.green:hover, 60 | .button.green:focus { 61 | background: #76c442; 62 | box-shadow: inset -0.35em -0.35em 0px 0px #4aa52e; 63 | } 64 | 65 | .button.small { 66 | font-size: 12px; 67 | padding: 7px; 68 | } 69 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/views/playfield.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.Playfield do 2 | use BearNecessitiesWeb, :view 3 | 4 | def tile_class(tile) do 5 | case tile do 6 | %Tile{type: :grass, texture: texture} -> "grass-#{texture}" 7 | %Tile{type: :nothing} -> "nothing" 8 | end 9 | end 10 | 11 | def item_class(item, player_id) do 12 | case item do 13 | %Bear{id: id, direction: direction, moving: moving, clawing: clawing, dead: nil} = bear 14 | when id == player_id -> 15 | ["bear", "self", direction] 16 | |> bear_action_class(moving, clawing) 17 | |> Enum.join(" ") 18 | 19 | %Bear{id: id, direction: direction, dead: _} when id == player_id -> 20 | "bear dead" 21 | 22 | %Bear{direction: direction, moving: moving, clawing: clawing, dead: nil} -> 23 | ["bear", "opponent", direction] 24 | |> bear_action_class(moving, clawing) 25 | |> Enum.join(" ") 26 | 27 | %Bear{direction: direction, dead: _dead} -> 28 | "bear opponent dead" 29 | 30 | %Bee{} -> 31 | "bee" 32 | 33 | %Tree{hive: %Hive{}} -> 34 | "tree hive" 35 | 36 | %Tree{} -> 37 | "tree" 38 | 39 | %HoneyDrop{} -> 40 | "honey" 41 | 42 | nil -> 43 | nil 44 | end 45 | end 46 | 47 | defp bear_action_class(classes, false, false), do: ["idle" | classes] 48 | defp bear_action_class(classes, _, true), do: ["clawing" | classes] 49 | defp bear_action_class(classes, _, _), do: classes 50 | end 51 | -------------------------------------------------------------------------------- /apps/game/lib/bear.ex: -------------------------------------------------------------------------------- 1 | defmodule Bear do 2 | defstruct id: nil, 3 | pos_x: nil, 4 | pos_y: nil, 5 | dead: nil, 6 | honey: nil, 7 | display_name: nil, 8 | started: false, 9 | direction: :down, 10 | moving: false, 11 | clawing: false 12 | 13 | def create_bear(%{height: height, width: width}, id, display_name, started) do 14 | pos_x = Enum.random(0..height) 15 | pos_y = Enum.random(0..width) 16 | 17 | %Bear{ 18 | id: id, 19 | pos_x: 5, 20 | pos_y: 5, 21 | honey: 10, 22 | display_name: display_name, 23 | started: started 24 | } 25 | end 26 | 27 | @doc """ 28 | Tries to change the position of a bear , up, left, down or right. 29 | The game will decide if the action is permitted and returns the 30 | actual state of the bear. 31 | """ 32 | def move(id, action) do 33 | id 34 | |> Game.get_bear() 35 | |> _move(action) 36 | end 37 | 38 | defp _move(bear, :down), do: Game.move(bear, :down, to: {bear.pos_x + 1, bear.pos_y}) 39 | defp _move(bear, :up), do: Game.move(bear, :up, to: {bear.pos_x - 1, bear.pos_y}) 40 | defp _move(bear, :left), do: Game.move(bear, :left, to: {bear.pos_x, bear.pos_y - 1}) 41 | defp _move(bear, :right), do: Game.move(bear, :right, to: {bear.pos_x, bear.pos_y + 1}) 42 | 43 | def stop(id) do 44 | GenServer.call(Game, {:stop, id}) 45 | end 46 | 47 | def claw(id) do 48 | id 49 | |> Game.get_bear() 50 | |> Game.claw() 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :bear_necessities_web 3 | 4 | socket "/socket", BearNecessitiesWeb.UserSocket, 5 | websocket: [timeout: 45_000], 6 | longpoll: false 7 | 8 | socket "/live", Phoenix.LiveView.Socket 9 | 10 | # Serve at "/" the static files from "priv/static" directory. 11 | # 12 | # You should set gzip to true if you are running phx.digest 13 | # when deploying your static files in production. 14 | plug Plug.Static, 15 | at: "/", 16 | from: :bear_necessities_web, 17 | gzip: false, 18 | only: ~w(css fonts images js favicon.ico robots.txt sound) 19 | 20 | # Code reloading can be explicitly enabled under the 21 | # :code_reloader configuration of your endpoint. 22 | if code_reloading? do 23 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket 24 | plug Phoenix.LiveReloader 25 | plug Phoenix.CodeReloader 26 | end 27 | 28 | plug Plug.RequestId 29 | plug Plug.Logger 30 | 31 | plug Plug.Parsers, 32 | parsers: [:urlencoded, :multipart, :json], 33 | pass: ["*/*"], 34 | json_decoder: Phoenix.json_library() 35 | 36 | plug Plug.MethodOverride 37 | plug Plug.Head 38 | 39 | # The session will be stored in the cookie and signed, 40 | # this means its contents can be read but not tampered with. 41 | # Set :encryption_salt if you would also like to encrypt it. 42 | plug Plug.Session, 43 | store: :cookie, 44 | key: "_bear_necessities_web_key", 45 | signing_salt: "jPBIZGYu" 46 | 47 | plug BearNecessitiesWeb.Router 48 | end 49 | -------------------------------------------------------------------------------- /apps/game/test/game_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GameTest do 2 | use ExUnit.Case 3 | 4 | test "greets the world" do 5 | Game.start_link([]) 6 | end 7 | 8 | test "create_bear" do 9 | Game.start_link([]) 10 | Player.start("fatboypunk", "phx-1") 11 | 12 | assert %Bear{ 13 | clawing: false, 14 | dead: nil, 15 | direction: :down, 16 | display_name: "fatboypunk", 17 | honey: 10, 18 | id: "phx-1", 19 | moving: false, 20 | pos_x: 5, 21 | pos_y: 5, 22 | started: true 23 | } = Game.get_bear("phx-1") 24 | end 25 | 26 | describe "move_player" do 27 | setup do 28 | Game.start_link([]) 29 | {player_pid, bear} = Player.start("fatboypunk", "phx-1") 30 | 31 | {:ok, player_pid: player_pid, bear: bear} 32 | end 33 | 34 | test "move to a working place", %{player_pid: player_pid} do 35 | Player.move(player_pid, "phx-1", :up_arrow) 36 | end 37 | end 38 | 39 | describe "#try_to_sting/2" do 40 | test "the bee successfully stings the bear when next to it" do 41 | bee = %Bee{id: "bee-1", pos_x: 2, pos_y: 3} 42 | bear = %Bear{id: "bear-1", pos_x: 3, pos_y: 3, honey: 10} 43 | 44 | assert [%Bear{honey: 9}] = Bee.try_to_sting(bee, [bear]) 45 | end 46 | 47 | test "the bee is unsuccessful when diagonal from a bear" do 48 | bee = %Bee{id: "bee-1", pos_x: 4, pos_y: 4} 49 | bear = %Bear{id: "bear-1", pos_x: 3, pos_y: 3, honey: 10} 50 | 51 | assert [%Bear{honey: 10}] = Bee.try_to_sting(bee, [bear]) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /apps/bear_necessities/test/support/data_case.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessities.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 | it cannot be async. For this reason, every test runs 11 | inside a transaction which is reset at the beginning 12 | of the test unless the test case is marked as async. 13 | """ 14 | 15 | use ExUnit.CaseTemplate 16 | 17 | using do 18 | quote do 19 | alias BearNecessities.Repo 20 | 21 | import Ecto 22 | import Ecto.Changeset 23 | import Ecto.Query 24 | import BearNecessities.DataCase 25 | end 26 | end 27 | 28 | setup tags do 29 | :ok = Ecto.Adapters.SQL.Sandbox.checkout(BearNecessities.Repo) 30 | 31 | unless tags[:async] do 32 | Ecto.Adapters.SQL.Sandbox.mode(BearNecessities.Repo, {:shared, self()}) 33 | end 34 | 35 | :ok 36 | end 37 | 38 | @doc """ 39 | A helper that transforms changeset errors into a map of messages. 40 | 41 | assert {:error, changeset} = Accounts.create_user(%{password: "short"}) 42 | assert "password is too short" in errors_on(changeset).password 43 | assert %{password: ["password is too short"]} = errors_on(changeset) 44 | 45 | """ 46 | def errors_on(changeset) do 47 | Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> 48 | Enum.reduce(opts, message, fn {key, value}, acc -> 49 | String.replace(acc, "%{#{key}}", to_string(value)) 50 | end) 51 | end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/views/error_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.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), class: "help-block") 14 | end) 15 | end 16 | 17 | @doc """ 18 | Translates an error message using gettext. 19 | """ 20 | def translate_error({msg, opts}) do 21 | # When using gettext, we typically pass the strings we want 22 | # to translate as a static argument: 23 | # 24 | # # Translate "is invalid" in the "errors" domain 25 | # dgettext("errors", "is invalid") 26 | # 27 | # # Translate the number of files with plural rules 28 | # dngettext("errors", "1 file", "%{count} files", count) 29 | # 30 | # Because the error messages we show in our forms and APIs 31 | # are defined inside Ecto, we need to translate them dynamically. 32 | # This requires us to call the Gettext module passing our gettext 33 | # backend as first argument. 34 | # 35 | # Note we use the "errors" domain, which means translations 36 | # should be written to the errors.po file. The :count option is 37 | # set by Ecto and indicates we should also apply plural rules. 38 | if count = opts[:count] do 39 | Gettext.dngettext(BearNecessitiesWeb.Gettext, "errors", msg, msg, count, opts) 40 | else 41 | Gettext.dgettext(BearNecessitiesWeb.Gettext, "errors", msg, opts) 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /apps/bear_necessities/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BearNecessities.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bear_necessities, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.5", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | start_permanent: Mix.env() == :prod, 15 | aliases: aliases(), 16 | deps: deps() 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application. 21 | # 22 | # Type `mix help compile.app` for more information. 23 | def application do 24 | [ 25 | mod: {BearNecessities.Application, []}, 26 | extra_applications: [:logger, :runtime_tools] 27 | ] 28 | end 29 | 30 | # Specifies which paths to compile per environment. 31 | defp elixirc_paths(:test), do: ["lib", "test/support"] 32 | defp elixirc_paths(_), do: ["lib"] 33 | 34 | # Specifies your project dependencies. 35 | # 36 | # Type `mix help deps` for examples and options. 37 | defp deps do 38 | [ 39 | {:ecto_sql, "~> 3.0"}, 40 | {:postgrex, ">= 0.0.0"}, 41 | {:jason, "~> 1.0"} 42 | ] 43 | end 44 | 45 | # Aliases are shortcuts or tasks specific to the current project. 46 | # For example, to create, migrate and run the seeds file at once: 47 | # 48 | # $ mix ecto.setup 49 | # 50 | # See the documentation for `Mix` for more info on aliases. 51 | defp aliases do 52 | [ 53 | "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], 54 | "ecto.reset": ["ecto.drop", "ecto.setup"], 55 | test: ["ecto.create --quiet", "ecto.migrate", "test"] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bear_necessities_web, 7 | version: "0.1.0", 8 | build_path: "../../_build", 9 | config_path: "../../config/config.exs", 10 | deps_path: "../../deps", 11 | lockfile: "../../mix.lock", 12 | elixir: "~> 1.5", 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | compilers: [:phoenix, :gettext] ++ Mix.compilers(), 15 | start_permanent: Mix.env() == :prod, 16 | aliases: aliases(), 17 | deps: deps() 18 | ] 19 | end 20 | 21 | # Configuration for the OTP application. 22 | # 23 | # Type `mix help compile.app` for more information. 24 | def application do 25 | [ 26 | mod: {BearNecessitiesWeb.Application, []}, 27 | extra_applications: [:logger, :runtime_tools] 28 | ] 29 | end 30 | 31 | # Specifies which paths to compile per environment. 32 | defp elixirc_paths(:test), do: ["lib", "test/support"] 33 | defp elixirc_paths(_), do: ["lib"] 34 | 35 | # Specifies your project dependencies. 36 | # 37 | # Type `mix help deps` for examples and options. 38 | defp deps do 39 | [ 40 | {:bear_necessities, in_umbrella: true}, 41 | {:gettext, "~> 0.11"}, 42 | {:jason, "~> 1.0"}, 43 | {:game, in_umbrella: true}, 44 | {:phoenix, "~> 1.4.3"}, 45 | {:phoenix_pubsub, "~> 1.1"}, 46 | {:phoenix_ecto, "~> 4.0"}, 47 | {:phoenix_html, "~> 2.11"}, 48 | {:phoenix_live_reload, "~> 1.2", only: :dev}, 49 | {:phoenix_live_view, "~> 0.2.0"}, 50 | {:plug_cowboy, "~> 2.0"} 51 | ] 52 | end 53 | 54 | # Aliases are shortcuts or tasks specific to the current project. 55 | # For example, we extend the test task to create and migrate the database. 56 | # 57 | # See the documentation for `Mix` for more info on aliases. 58 | defp aliases do 59 | [test: ["ecto.create --quiet", "ecto.migrate", "test"]] 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/config/dev.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities_web application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | # For development, we disable any cache and enable 8 | # debugging and code reloading. 9 | # 10 | # The watchers configuration can be used to run external 11 | # watchers to your application. For example, we use it 12 | # with webpack to recompile .js and .css sources. 13 | config :bear_necessities_web, BearNecessitiesWeb.Endpoint, 14 | http: [port: 4000], 15 | debug_errors: true, 16 | code_reloader: true, 17 | check_origin: false, 18 | watchers: [ 19 | node: [ 20 | "node_modules/webpack/bin/webpack.js", 21 | "--mode", 22 | "development", 23 | "--watch-stdin", 24 | cd: Path.expand("../assets", __DIR__) 25 | ] 26 | ] 27 | 28 | # ## SSL Support 29 | # 30 | # In order to use HTTPS in development, a self-signed 31 | # certificate can be generated by running the following 32 | # Mix task: 33 | # 34 | # mix phx.gen.cert 35 | # 36 | # Note that this task requires Erlang/OTP 20 or later. 37 | # Run `mix help phx.gen.cert` for more information. 38 | # 39 | # The `http:` config above can be replaced with: 40 | # 41 | # https: [ 42 | # port: 4001, 43 | # cipher_suite: :strong, 44 | # keyfile: "priv/cert/selfsigned_key.pem", 45 | # certfile: "priv/cert/selfsigned.pem" 46 | # ], 47 | # 48 | # If desired, both `http:` and `https:` keys can be 49 | # configured to run both http and https servers on 50 | # different ports. 51 | 52 | # Watch static and templates for browser reloading. 53 | config :bear_necessities_web, BearNecessitiesWeb.Endpoint, 54 | live_reload: [ 55 | patterns: [ 56 | ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$", 57 | ~r"priv/gettext/.*(po)$", 58 | ~r"lib/bear_necessities_web/{live,views}/.*(ex)$", 59 | ~r"lib/bear_necessities_web/templates/.*(eex)$" 60 | ] 61 | ], 62 | live_view: [ 63 | signing_salt: "Z+5xU1Ed8BLzDuUjoyNwUsjVFef0dI+F" 64 | ] 65 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb 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 BearNecessitiesWeb, :controller 9 | use BearNecessitiesWeb, :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: BearNecessitiesWeb 23 | import Plug.Conn 24 | import BearNecessitiesWeb.Gettext 25 | alias BearNecessitiesWeb.Router.Helpers, as: Routes 26 | end 27 | end 28 | 29 | def view do 30 | quote do 31 | use Phoenix.View, 32 | root: "lib/bear_necessities_web/templates", 33 | namespace: BearNecessitiesWeb 34 | 35 | # Import convenience functions from controllers 36 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] 37 | import Phoenix.LiveView, only: [live_render: 2, live_render: 3] 38 | 39 | # Use all HTML functionality (forms, tags, etc) 40 | use Phoenix.HTML 41 | 42 | import BearNecessitiesWeb.ErrorHelpers 43 | import BearNecessitiesWeb.Gettext 44 | alias BearNecessitiesWeb.Router.Helpers, as: Routes 45 | end 46 | end 47 | 48 | def router do 49 | quote do 50 | use Phoenix.Router 51 | import Plug.Conn 52 | import Phoenix.Controller 53 | import Phoenix.LiveView.Router 54 | end 55 | end 56 | 57 | def channel do 58 | quote do 59 | use Phoenix.Channel 60 | import BearNecessitiesWeb.Gettext 61 | end 62 | end 63 | 64 | @doc """ 65 | When used, dispatch to the appropriate controller/view/etc. 66 | """ 67 | defmacro __using__(which) when is_atom(which) do 68 | apply(__MODULE__, which, []) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /apps/game/lib/player.ex: -------------------------------------------------------------------------------- 1 | defmodule Player do 2 | use GenServer 3 | require Logger 4 | 5 | @claw_time_ms 1000 6 | 7 | @enforce_keys [:id] 8 | defstruct [:id, :claw, :timer_pid] 9 | 10 | def start_link(default) when is_list(default) do 11 | GenServer.start_link(__MODULE__, default) 12 | end 13 | 14 | @impl true 15 | def init(id: id) do 16 | {:ok, %Player{id: id}} 17 | end 18 | 19 | @impl true 20 | def handle_call({:action, user_input, id}, _pid, state) do 21 | bear = 22 | case user_input do 23 | :up_arrow -> 24 | Bear.move(id, :up) 25 | 26 | :down_arrow -> 27 | Bear.move(id, :down) 28 | 29 | :left_arrow -> 30 | Bear.move(id, :left) 31 | 32 | :right_arrow -> 33 | Bear.move(id, :right) 34 | end 35 | 36 | {:reply, bear, state} 37 | end 38 | 39 | @impl true 40 | def handle_call({:claw, id}, _pid, state) do 41 | bear = Bear.claw(id) 42 | {:ok, timer_pid} = :timer.send_interval(50, self(), :update_claw) 43 | state = %{state | claw: @claw_time_ms, timer_pid: timer_pid} 44 | 45 | {:reply, bear, state} 46 | end 47 | 48 | @impl true 49 | def handle_info(:update_claw, %{claw: nil} = state) do 50 | {:noreply, state} 51 | end 52 | 53 | @impl true 54 | def handle_info(:update_claw, %{claw: claw_time} = state) when claw_time > 0 do 55 | state = %{state | claw: claw_time - 50} 56 | 57 | {:noreply, state} 58 | end 59 | 60 | @impl true 61 | def handle_info(:update_claw, %{id: id, claw: claw_time, timer_pid: timer_pid} = state) 62 | when claw_time < 1 do 63 | :timer.cancel(timer_pid) 64 | state = %{state | claw: nil, timer_pid: nil} 65 | Bear.stop(id) 66 | 67 | {:noreply, state} 68 | end 69 | 70 | @impl true 71 | def handle_info(:update_claw, state) do 72 | {:noreply, state} 73 | end 74 | 75 | def move(pid, player_id, way) do 76 | GenServer.call(pid, {:action, way, player_id}) 77 | end 78 | 79 | def claw(pid, player_id) do 80 | GenServer.call(pid, {:claw, player_id}) 81 | end 82 | 83 | def start(display_name, id) do 84 | {:ok, pid} = Player.start_link(id: id) 85 | bear = Game.create_bear(display_name: display_name, id: id, started: true) 86 | 87 | {pid, bear} 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | parallelism: 1 5 | docker: 6 | - image: defactosoftware/elixir 7 | MIX_ENV: test 8 | - image: circleci/postgres:10.1-alpine 9 | environment: 10 | POSTGRES_USER: postgres 11 | POSTGRES_DB: bear_necessities_test 12 | POSTGRES_PASSWORD: postgres 13 | steps: 14 | - checkout 15 | - restore_cache: 16 | paths: 17 | - /root/.mix/archives 18 | keys: 19 | - v1-hex-archive 20 | - v1-mix-deps-{{ checksum "mix.lock" }} 21 | - run: 22 | name: Install node 23 | command: | 24 | set +e 25 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash 26 | export NVM_DIR="/root/.nvm" 27 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 28 | nvm install 29 | 30 | # Each step uses the same `$BASH_ENV`, so need to modify it 31 | echo 'export NVM_DIR="/root/.nvm"' >> $BASH_ENV 32 | echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV 33 | - run: 34 | name: Create local hex archive 35 | command: mix local.hex --force 36 | - run: 37 | name: Create local rebar 38 | command: mix local.rebar --force 39 | - run: 40 | name: Installing Elixir dependencies 41 | command: mix deps.get 42 | - run: 43 | name: Compile the Phoenix app 44 | command: mix compile 45 | - run: 46 | name: Phoenix digest 47 | command: mix phx.digest 48 | - run: 49 | name: Creating Postgres database 50 | command: mix ecto.create 51 | - run: 52 | name: Running Elixir tests 53 | command: mix test 54 | - save_cache: 55 | key: v1-hex-archive 56 | paths: 57 | - /root/.mix/archives 58 | - save_cache: 59 | key: v1-mix-deps-{{ checksum "mix.lock" }} 60 | paths: 61 | - "deps" 62 | - "_build" 63 | - save_cache: 64 | key: v1-yarn-deps-{{ checksum "yarn.lock" }} 65 | paths: 66 | - "node_modules" 67 | experimental: 68 | notify: 69 | branches: 70 | only: 71 | - master 72 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket, 5 | // and connect at the socket path in "lib/web/endpoint.ex". 6 | // 7 | // Pass the token on params as below. Or remove it 8 | // from the params if you are not using authentication. 9 | import {Socket} from "phoenix" 10 | 11 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 12 | 13 | // When you connect, you'll often need to authenticate the client. 14 | // For example, imagine you have an authentication plug, `MyAuth`, 15 | // which authenticates the session and assigns a `:current_user`. 16 | // If the current user exists you can assign the user's token in 17 | // the connection for use in the layout. 18 | // 19 | // In your "lib/web/router.ex": 20 | // 21 | // pipeline :browser do 22 | // ... 23 | // plug MyAuth 24 | // plug :put_user_token 25 | // end 26 | // 27 | // defp put_user_token(conn, _) do 28 | // if current_user = conn.assigns[:current_user] do 29 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 30 | // assign(conn, :user_token, token) 31 | // else 32 | // conn 33 | // end 34 | // end 35 | // 36 | // Now you need to pass this token to JavaScript. You can do so 37 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 38 | // 39 | // 40 | // 41 | // You will need to verify the user token in the "connect/3" function 42 | // in "lib/web/channels/user_socket.ex": 43 | // 44 | // def connect(%{"token" => token}, socket, _connect_info) do 45 | // # max_age: 1209600 is equivalent to two weeks in seconds 46 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 47 | // {:ok, user_id} -> 48 | // {:ok, assign(socket, :user, user_id)} 49 | // {:error, reason} -> 50 | // :error 51 | // end 52 | // end 53 | // 54 | // Finally, connect to the socket: 55 | socket.connect() 56 | 57 | // Now that you are connected, you can join channels with a topic: 58 | let channel = socket.channel("topic:subtopic", {}) 59 | channel.join() 60 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 61 | .receive("error", resp => { console.log("Unable to join", resp) }) 62 | 63 | export default socket 64 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/test/bear_necessities_web/views/playfield_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.PlayfieldTest do 2 | use BearNecessitiesWeb.ConnCase 3 | import Phoenix.LiveViewTest 4 | 5 | test "Start with a player name", %{conn: conn} do 6 | conn = get(conn, "/") 7 | assert html_response(conn, 200) =~ "What is Your Name?" 8 | 9 | {:ok, view, _html} = live(conn) 10 | 11 | template = render_submit(view, "start", %{player: %{display_name: "PhoenixPlayer"}}) 12 | 13 | assert template =~ "

Hello, PhoenixPlayer

" 14 | assert template =~ "X: 5" 15 | assert template =~ "Y: 5" 16 | end 17 | 18 | test "move the player up", %{conn: conn} do 19 | conn = get(conn, "/") 20 | assert html_response(conn, 200) =~ "What is Your Name?" 21 | 22 | {:ok, view, _html} = live(conn) 23 | 24 | render_submit(view, "start", %{player: %{display_name: "PhoenixPlayer"}}) 25 | template = render_keydown(view, "key_move", %{"key" => "ArrowUp"}) 26 | 27 | assert template =~ "X: 4" 28 | assert template =~ "Y: 5" 29 | end 30 | 31 | test "move the player down", %{conn: conn} do 32 | conn = get(conn, "/") 33 | assert html_response(conn, 200) =~ "What is Your Name?" 34 | 35 | {:ok, view, _html} = live(conn) 36 | 37 | render_submit(view, "start", %{player: %{display_name: "PhoenixPlayer"}}) 38 | template = render_keydown(view, "key_move", %{"key" => "ArrowDown"}) 39 | 40 | assert template =~ "X: 6" 41 | assert template =~ "Y: 5" 42 | end 43 | 44 | test "move the player left", %{conn: conn} do 45 | conn = get(conn, "/") 46 | assert html_response(conn, 200) =~ "What is Your Name?" 47 | 48 | {:ok, view, _html} = live(conn) 49 | 50 | render_submit(view, "start", %{player: %{display_name: "PhoenixPlayer"}}) 51 | template = render_keydown(view, "key_move", %{"key" => "ArrowLeft"}) 52 | 53 | assert template =~ "X: 5" 54 | assert template =~ "Y: 4" 55 | end 56 | 57 | test "move the player right", %{conn: conn} do 58 | conn = get(conn, "/") 59 | assert html_response(conn, 200) =~ "What is Your Name?" 60 | 61 | {:ok, view, _html} = live(conn) 62 | 63 | render_submit(view, "start", %{player: %{display_name: "PhoenixPlayer"}}) 64 | template = render_keydown(view, "key_move", %{"key" => "ArrowRight"}) 65 | 66 | assert template =~ "X: 5" 67 | assert template =~ "Y: 6" 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/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 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/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 | -------------------------------------------------------------------------------- /apps/game/lib/bee.ex: -------------------------------------------------------------------------------- 1 | defmodule Bee do 2 | use GenServer 3 | 4 | @duration_ms 10000 5 | @directions [:up, :left, :right, :down] 6 | @enforce_keys [:pos_x, :pos_y] 7 | defstruct [:id, :catching, :pos_x, :pos_y] 8 | 9 | def start_link(defaults) do 10 | GenServer.start_link(__MODULE__, defaults) 11 | end 12 | 13 | @impl true 14 | def init(_) do 15 | :timer.send_interval(150, self(), :update) 16 | {:ok, @duration_ms} 17 | end 18 | 19 | @impl true 20 | def handle_info(:update, duration) when duration < 0 do 21 | {:stop, :shutdown, -1} 22 | end 23 | 24 | def handle_info(:update, duration) when duration > 0 do 25 | with %Bee{} = bee = GenServer.call(Game, {:get_bee, self()}), 26 | %Bear{} = bear <- GenServer.call(Game, {:get_bear, bee.catching}), 27 | true <- GenServer.call(Game, {:try_to_sting, bee}) do 28 | bee 29 | |> direction_lengths(bear) 30 | |> move_to(bee) 31 | end 32 | 33 | {:noreply, duration - 100} 34 | end 35 | 36 | def handle_info(:update, _) do 37 | {:stop, :normal, -1} 38 | end 39 | 40 | @impl true 41 | def terminate(reason, _) do 42 | GenServer.cast(Game, {:remove_bee, self()}) 43 | reason 44 | end 45 | 46 | def move_to(directions, bee) do 47 | GenServer.cast(Game, {:move_bee, bee, directions}) 48 | end 49 | 50 | def direction_lengths(bee, bear) do 51 | @directions 52 | |> Enum.reduce([], fn direction, acc -> 53 | [{direction, distance(direction, bee, bee) + distance(direction, bear, bee)} | acc] 54 | end) 55 | |> Enum.sort(fn {_, f}, {_, s} -> f <= s end) 56 | |> Keyword.keys() 57 | end 58 | 59 | def distance(:up, obj, new_pos), 60 | do: abs(obj.pos_x - new_pos.pos_x + 1) + abs(obj.pos_y - new_pos.pos_y) 61 | 62 | def distance(:down, obj, new_pos), 63 | do: abs(obj.pos_x - new_pos.pos_x - 1) + abs(obj.pos_y - new_pos.pos_y) 64 | 65 | def distance(:left, obj, new_pos), 66 | do: abs(obj.pos_x - new_pos.pos_x) + abs(obj.pos_y - new_pos.pos_y + 1) 67 | 68 | def distance(:right, obj, new_pos), 69 | do: abs(obj.pos_x - new_pos.pos_x) + abs(obj.pos_y - new_pos.pos_y - 1) 70 | 71 | def try_to_sting(bee, bears) do 72 | Enum.map(bears, fn bear -> 73 | if next_to_bee?(bee, bear), 74 | do: Game.remove_honey_from_bear(bear), 75 | else: bear 76 | end) 77 | end 78 | 79 | defp next_to_bee?(%Bee{pos_x: pos_x, pos_y: pos_y}, %{pos_x: bx, pos_y: by}) 80 | when (pos_x == bx - 1 and pos_y == by) or 81 | (pos_x == bx + 1 and pos_y == by) or 82 | (pos_x == bx and pos_y == by - 1) or 83 | (pos_x == bx and pos_y == by + 1), 84 | do: true 85 | 86 | defp next_to_bee?(_, _), do: false 87 | end 88 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/config/prod.exs: -------------------------------------------------------------------------------- 1 | # Since configuration is shared in umbrella projects, this file 2 | # should only configure the :bear_necessities_web application itself 3 | # and only for organization purposes. All other config goes to 4 | # the umbrella root. 5 | use Mix.Config 6 | 7 | # For production, don't forget to configure the url host 8 | # to something meaningful, Phoenix uses this information 9 | # when generating URLs. 10 | # 11 | # Note we also include the path to a cache manifest 12 | # containing the digested version of static files. This 13 | # manifest is generated by the `mix phx.digest` task, 14 | # which you should run after static files are built and 15 | # before starting your production server. 16 | 17 | config :bear_necessities_web, BearNecessitiesWeb.Endpoint, 18 | http: [port: System.get_env("PORT")], 19 | url: [scheme: "https", host: "unbearable.nl", port: 443], 20 | force_ssl: [rewrite_on: [:x_forwarded_proto]], 21 | cache_static_manifest: "priv/static/cache_manifest.json", 22 | secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE"), 23 | check_origin: [ 24 | "https://bear-necessities.herokuapp.com", 25 | "https://unbearable.nl", 26 | "https://www.unbearable.nl" 27 | ], 28 | live_view: [ 29 | signing_salt: Map.fetch!(System.get_env(), "LIVE_VIEW_SALT") 30 | ] 31 | 32 | # Do not print debug messages in production 33 | config :logger, level: :info 34 | 35 | # ## SSL Support 36 | # 37 | # To get SSL working, you will need to add the `https` key 38 | # to the previous section and set your `:url` port to 443: 39 | # 40 | # config :bear_necessities_web, BearNecessitiesWeb.Endpoint, 41 | # ... 42 | # url: [host: "example.com", port: 443], 43 | # https: [ 44 | # :inet6, 45 | # port: 443, 46 | # cipher_suite: :strong, 47 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), 48 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH") 49 | # ] 50 | # 51 | # The `cipher_suite` is set to `:strong` to support only the 52 | # latest and more secure SSL ciphers. This means old browsers 53 | # and clients may not be supported. You can set it to 54 | # `:compatible` for wider support. 55 | # 56 | # `:keyfile` and `:certfile` expect an absolute path to the key 57 | # and cert in disk or a relative path inside priv, for example 58 | # "priv/ssl/server.key". For all supported SSL configuration 59 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1 60 | # 61 | # We also recommend setting `force_ssl` in your endpoint, ensuring 62 | # no data is ever sent via http, always redirecting to https: 63 | # 64 | # config :bear_necessities_web, BearNecessitiesWeb.Endpoint, 65 | # force_ssl: [hsts: true] 66 | # 67 | # Check `Plug.SSL` for all available options in `force_ssl`. 68 | 69 | # ## Using releases (distillery) 70 | # 71 | # If you are doing OTP releases, you need to instruct Phoenix 72 | # to start the server for all endpoints: 73 | # 74 | # config :phoenix, :serve_endpoints, true 75 | # 76 | # Alternatively, you can configure exactly which server to 77 | # start per endpoint: 78 | # 79 | # config :bear_necessities_web, BearNecessitiesWeb.Endpoint, server: true 80 | # 81 | # Note you can't rely on `System.get_env/1` when using releases. 82 | # See the releases documentation accordingly. 83 | 84 | # Finally import the config/prod.secret.exs which should be versioned 85 | # separately. 86 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/templates/playfield/template.html.leex: -------------------------------------------------------------------------------- 1 | <%= if @bear.started do %> 2 |
3 |
4 |
5 |

Hello, <%= @bear.display_name %>

6 | X: <%= @pos_x %> 7 | Y: <%= @pos_y %> 8 | <%= if @play_sounds do %> 9 | 10 | 11 | <% else %> 12 | 13 | <% end %> 14 | 15 | 16 | 17 | 18 | 19 | <%= if is_nil(@bear.dead) do %> 20 | 21 | 22 | <% end %> 23 |
24 | 25 |
26 | <%= for row <- @viewport do %> 27 |
28 | <%= for {tile, item} <- row do %> 29 |
30 |
31 |
32 | <% end %> 33 |
34 | <% end %> 35 |
36 | 37 |
38 | 39 | 42 |
43 | 44 | <% else %> 45 | 46 |
47 |

Bear
Necessities

48 | 49 | <%= img_tag(Routes.static_path(BearNecessitiesWeb.Endpoint, "/images/bear/down.gif"), class: "player-image") %> 50 | 51 | <%= form_tag "#", [phx_submit: :start] do %> 52 | <%= label(:player, :display_name, "What is Your Name?") %> 53 |


54 | <%= text_input(:player, :display_name, value: @bear.display_name, class: "input") %> 55 | 56 | <%= submit("Start", phx_disable_with: "Starting...", class: "button red") %> 57 | <% end %> 58 |
59 |

Use the Spacebar to claw.

60 |

Arrow keys to move.

61 |
62 | 63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | 71 |
72 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | <% end %> 91 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/assets/css/game/game.css: -------------------------------------------------------------------------------- 1 | .game { 2 | display: flex; 3 | overflow: hidden; 4 | 5 | .main { 6 | padding: 10px 30px; 7 | flex: 1; 8 | overflow: hidden; 9 | min-width: calc(100vmin - 13em); 10 | } 11 | 12 | .sidebar { 13 | padding: 10px 30px; 14 | flex-basis: 30px; 15 | flex: 0 0 25em; 16 | overflow: hidden; 17 | } 18 | 19 | .gui { 20 | height: 90px; 21 | margin-bottom: 30px; 22 | text-align: center; 23 | } 24 | 25 | .map { 26 | border: 6px solid rgba(0, 0, 0, 0.5); 27 | bottom: 0; 28 | display: flex; 29 | flex-direction: column; 30 | height: calc(100vmin - 13em); 31 | left: 0; 32 | margin: auto; 33 | right: 0; 34 | text-align: center; 35 | top: 0; 36 | width: calc(100vmin - 13em); 37 | } 38 | 39 | .row { 40 | display: flex; 41 | flex-wrap: nowrap; 42 | flex: 1; 43 | justify-content: center; 44 | justify-content: space-evenly; 45 | } 46 | 47 | h1 { 48 | height: 1.5em; 49 | overflow: hidden; 50 | text-overflow: ellipsis; 51 | white-space: nowrap; 52 | } 53 | 54 | .load-bee { 55 | background-image: url("/images/bees/bees.gif"); 56 | } 57 | 58 | .load-bee-1 { 59 | background-image: url("/images/bees/bees-1.gif"); 60 | } 61 | 62 | .bee { 63 | background-image: url("/images/bees/bees.gif"); 64 | 65 | &:before { 66 | content: ""; 67 | display: block; 68 | height: 100%; 69 | flex: 1; 70 | position: absolute; 71 | width: 100%; 72 | background-image: url("/images/bees/bees-1.gif"); 73 | background-size: cover; 74 | background-repeat: no-repeat; 75 | transform: rotate(90deg); 76 | } 77 | } 78 | 79 | /* 80 | Tiles of map (terain, border) 81 | */ 82 | .tile { 83 | background-size: cover; 84 | flex: 1; 85 | height: 100%; 86 | image-rendering: pixelated; 87 | position: relative; 88 | 89 | &.grass { 90 | &-1 { 91 | background-color: #44d873; 92 | background-image: url("/images/terain/grass-1.gif"); 93 | } 94 | 95 | &-2 { 96 | background-color: #44d873; 97 | background-image: url("/images/terain/grass-2.gif"); 98 | } 99 | 100 | &-3 { 101 | background-color: #44d873; 102 | background-image: url("/images/terain/grass-3.gif"); 103 | } 104 | 105 | &-4 { 106 | background-color: #44d873; 107 | background-image: url("/images/terain/grass-4.gif"); 108 | } 109 | } 110 | 111 | &.nothing { 112 | background-color: gray; 113 | background-image: url("/images/terain/nothing.gif"); 114 | } 115 | } 116 | 117 | /* 118 | Items that are displayed inside tiles (bears, trees, honey, etc) 119 | */ 120 | .item { 121 | background-position: center center; 122 | background-repeat: no-repeat; 123 | background-size: cover; 124 | height: 100%; 125 | image-rendering: pixelated; 126 | width: 100%; 127 | 128 | /* 129 | Honey 130 | */ 131 | &.honey { 132 | background-image: url("/images/honey/drop.gif"); 133 | background-size: 75%; 134 | } 135 | 136 | /* 137 | Trees 138 | */ 139 | &.tree { 140 | background-image: url("/images/trees/tree-1.gif"); 141 | 142 | &.hive { 143 | background-image: url("/images/trees/treehive.gif"); 144 | } 145 | } 146 | 147 | /* 148 | Bears 149 | */ 150 | &.bear { 151 | &.self {} 152 | 153 | &.opponent { 154 | filter: grayscale(60%); 155 | } 156 | 157 | &.dead { 158 | background-image: url("/images/bear/dead.gif"); 159 | } 160 | 161 | &.up { 162 | background-image: url("/images/bear/up.gif"); 163 | 164 | &.idle { 165 | background-image: url("/images/bear/up-idle.gif"); 166 | } 167 | 168 | &.clawing { 169 | background-image: url("/images/bear/up-claw.gif"); 170 | } 171 | } 172 | 173 | &.left { 174 | background-image: url("/images/bear/left.gif"); 175 | 176 | &.idle { 177 | background-image: url("/images/bear/left-idle.gif"); 178 | } 179 | 180 | &.clawing { 181 | background-image: url("/images/bear/left-claw.gif"); 182 | } 183 | } 184 | 185 | &.right { 186 | background-image: url("/images/bear/right.gif"); 187 | 188 | &.idle { 189 | background-image: url("/images/bear/right-idle.gif"); 190 | } 191 | 192 | &.clawing { 193 | background-image: url("/images/bear/right-claw.gif"); 194 | } 195 | } 196 | 197 | &.down { 198 | background-image: url("/images/bear/down.gif"); 199 | 200 | &.idle { 201 | background-image: url("/images/bear/down-idle.gif"); 202 | } 203 | 204 | &.clawing { 205 | background-image: url("/images/bear/down-claw.gif"); 206 | } 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 3 | "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 4 | "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, 5 | "db_connection": {:hex, :db_connection, "2.1.1", "a51e8a2ee54ef2ae6ec41a668c85787ed40cb8944928c191280fe34c15b76ae5", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, 7 | "ecto": {:hex, :ecto, "3.1.7", "fa21d06ef56cdc2fdaa62574e8c3ba34a2751d44ea34c30bc65f0728421043e5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, 8 | "ecto_sql": {:hex, :ecto_sql, "3.1.6", "1e80e30d16138a729c717f73dcb938590bcdb3a4502f3012414d0cbb261045d8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0 or ~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 9 | "file_system": {:hex, :file_system, "0.2.7", "e6f7f155970975789f26e77b8b8d8ab084c59844d8ecfaf58cbda31c494d14aa", [:mix], [], "hexpm"}, 10 | "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, 11 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 12 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, 13 | "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [: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"}, 15 | "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.1", "274a4b07c4adbdd7785d45a8b0bb57634d0b4f45b18d2c508b26c0344bd59b8f", [: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"}, 17 | "phoenix_live_view": {:hex, :phoenix_live_view, "0.2.0", "9c0e8bcdb0c2006bc295ef6cd7ee5bcec286e420050cb1449fd4de4e02fb20d2", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.4.9", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}], "hexpm"}, 18 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, 19 | "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, 20 | "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 21 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, 22 | "postgrex": {:hex, :postgrex, "0.15.0", "dd5349161019caeea93efa42f9b22f9d79995c3a86bdffb796427b4c9863b0f0", [: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"}, 23 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, 24 | "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, 25 | } 26 | -------------------------------------------------------------------------------- /apps/bear_necessities_web/lib/bear_necessities_web/live/game.ex: -------------------------------------------------------------------------------- 1 | defmodule BearNecessitiesWeb.Game do 2 | use Phoenix.LiveView 3 | require Logger 4 | 5 | @action_map %{ 6 | "right" => "ArrowRight", 7 | "left" => "ArrowLeft", 8 | "up" => "ArrowUp", 9 | "down" => "ArrowDown" 10 | } 11 | @action_keys Map.keys(@action_map) 12 | @arrow_keys Map.values(@action_map) 13 | 14 | alias BearNecessitiesWeb.Playfield 15 | 16 | def render(assigns) do 17 | Playfield.render("template.html", assigns) 18 | end 19 | 20 | def mount(_session, %{id: id} = socket) do 21 | field = Game.get_field(id) 22 | players = Game.get_players() 23 | 24 | socket = 25 | socket 26 | |> assign(:id, id) 27 | |> assign(:viewport, []) 28 | |> assign(:pos_x, nil) 29 | |> assign(:pos_y, nil) 30 | |> assign(:play_sounds, false) 31 | |> assign(:field, field) 32 | |> assign(:players, players) 33 | |> assign(:bear, %Bear{started: false, display_name: "Player#{Enum.count(players)}"}) 34 | 35 | {:ok, socket} 36 | end 37 | 38 | def handle_event("start", %{"player" => %{"display_name" => display_name}}, %{id: id} = socket) do 39 | reference = set_updates(connected?(socket)) 40 | {player_pid, bear} = Player.start(display_name, id) 41 | viewport = ViewPort.get_viewport(id) 42 | 43 | socket = 44 | socket 45 | |> assign(:id, id) 46 | |> assign(:viewport, viewport) 47 | |> assign(:pos_x, bear.pos_x) 48 | |> assign(:reference, reference) 49 | |> assign(:autoplay, false) 50 | |> assign(:pos_y, bear.pos_y) 51 | |> assign(:bear, bear) 52 | |> assign(:player_pid, player_pid) 53 | 54 | {:noreply, socket} 55 | end 56 | 57 | def handle_event(_, "Meta", socket) do 58 | {:noreply, socket} 59 | end 60 | 61 | def handle_event("sounds_on", _, %{id: id} = socket) do 62 | socket = 63 | socket 64 | |> assign(:play_sounds, true) 65 | |> assign(:autoplay, true) 66 | 67 | {:noreply, socket} 68 | end 69 | 70 | def handle_event("sounds_off", _, %{id: id} = socket) do 71 | socket = assign(socket, :play_sounds, false) 72 | {:noreply, socket} 73 | end 74 | 75 | def handle_event(direction, _, %{id: id} = socket) when direction in @action_keys do 76 | socket = move_player(id, Map.get(@action_map, direction), socket) 77 | {:noreply, socket} 78 | end 79 | 80 | def handle_event("key_move", %{"key" => key}, %{id: id} = socket) 81 | when key in @arrow_keys do 82 | socket = move_player(id, key, socket) 83 | {:noreply, socket} 84 | end 85 | 86 | def handle_event("key_up", %{"key" => key}, %{id: id} = socket) 87 | when key in @arrow_keys do 88 | Bear.stop(id) 89 | {:noreply, socket} 90 | end 91 | 92 | def handle_event("key_move", %{"key" => key}, %{id: id} = socket) do 93 | {:noreply, socket} 94 | end 95 | 96 | def handle_event( 97 | "key_up", 98 | %{"key" => " "}, 99 | %{id: id, assigns: %{player_pid: player_pid}} = socket 100 | ) do 101 | socket = 102 | socket 103 | |> assign(:bear, Player.claw(player_pid, id)) 104 | |> assign(:viewport, ViewPort.get_viewport(id)) 105 | 106 | {:noreply, socket} 107 | end 108 | 109 | def handle_event("key_up", _, %{id: id} = socket) do 110 | {:noreply, socket} 111 | end 112 | 113 | def handle_info(:update, %{id: nil} = socket) do 114 | {:noreply, socket} 115 | end 116 | 117 | def handle_info(:update, %{id: id, assigns: %{bear: %Bear{dead: nil, started: true}}} = socket) 118 | when not is_nil(id) do 119 | players = Game.get_players() 120 | viewport = ViewPort.get_viewport(id) 121 | bear = Game.get_bear(id) 122 | 123 | socket = 124 | socket 125 | |> assign(:viewport, viewport) 126 | |> assign(:players, players) 127 | |> assign(:autoplay, false) 128 | |> assign(:field, %Field{}) 129 | |> assign(:bear, bear) 130 | 131 | {:noreply, socket} 132 | end 133 | 134 | def handle_info( 135 | :update, 136 | %{id: id, assigns: %{bear: %Bear{dead: dead} = bear}} = socket 137 | ) 138 | when not is_nil(dead) and dead > 0 do 139 | socket = assign(socket, :bear, %{bear | dead: bear.dead - 51}) 140 | {:noreply, socket} 141 | end 142 | 143 | def handle_info( 144 | :update, 145 | %{id: id, assigns: %{reference: reference, bear: %Bear{dead: dead} = bear}} = socket 146 | ) 147 | when dead < 50 do 148 | :timer.cancel(reference) 149 | Game.remove_bear(id) 150 | players = Game.get_players() 151 | 152 | socket = 153 | socket 154 | |> assign(:bear, %{bear | started: false, dead: nil}) 155 | |> assign(:viewport, []) 156 | |> assign(:players, players) 157 | 158 | {:noreply, socket} 159 | end 160 | 161 | def handle_info(:update, socket) do 162 | {:noreply, socket} 163 | end 164 | 165 | def terminate(reason, %{id: id} = socket) do 166 | Game.remove_bear(id) 167 | 168 | reason 169 | end 170 | 171 | def set_updates(false), do: nil 172 | 173 | def set_updates(true) do 174 | {:ok, ref} = :timer.send_interval(50, self(), :update) 175 | ref 176 | end 177 | 178 | def move_player(id, direction, %{assigns: %{player_pid: player_pid}} = socket) do 179 | bear = Player.move(player_pid, id, move_to(direction)) 180 | 181 | viewport = ViewPort.get_viewport(id) 182 | 183 | socket 184 | |> assign(:pos_x, bear.pos_x) 185 | |> assign(:pos_y, bear.pos_y) 186 | |> assign(:viewport, viewport) 187 | end 188 | 189 | def move_to("ArrowRight"), do: :right_arrow 190 | def move_to("ArrowLeft"), do: :left_arrow 191 | def move_to("ArrowUp"), do: :up_arrow 192 | def move_to("ArrowDown"), do: :down_arrow 193 | end 194 | -------------------------------------------------------------------------------- /apps/game/lib/game_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Game do 2 | use GenServer 3 | 4 | @vertical_view_distance 5 5 | @horizontal_view_distance 5 6 | @number_of_trees 20 7 | @hive_spawn_time 3000 8 | @field_height 40 9 | @field_width 40 10 | @miliseconds_dead_screen 2900 11 | @circular_trajectory %{ 12 | left: :left_up, 13 | left_up: :up, 14 | up: :right_up, 15 | right_up: :right, 16 | right: :right_down, 17 | right_down: :down, 18 | down: :left_down, 19 | left_down: :left 20 | } 21 | 22 | defstruct [:field, :bears, :bees, :trees, :honey_drops] 23 | 24 | def start_link(default) when is_list(default) do 25 | GenServer.start_link(__MODULE__, default, name: __MODULE__) 26 | end 27 | 28 | @impl true 29 | def init([]) do 30 | field = Field.create_field(@field_height, @field_width) 31 | :timer.send_interval(@hive_spawn_time, self(), :spawn_hive) 32 | 33 | game = %Game{ 34 | field: field, 35 | bears: [], 36 | bees: [], 37 | honey_drops: [], 38 | trees: spawn_trees() 39 | } 40 | 41 | {:ok, game} 42 | end 43 | 44 | @impl true 45 | def handle_info(:spawn_hive, %{trees: trees} = state) do 46 | new_state = 47 | case Enum.filter(trees, &(&1.hive == nil)) do 48 | [] -> state 49 | _ -> add_hive_to_tree(state) 50 | end 51 | 52 | {:noreply, new_state} 53 | end 54 | 55 | def handle_info(_, state) do 56 | {:noreply, state} 57 | end 58 | 59 | defp add_hive_to_tree(%{trees: trees} = state) do 60 | target_tree = Enum.random(trees) 61 | new_tree = %{target_tree | hive: %Hive{hp: 5, honey: 5}} 62 | 63 | update_state_with(state, new_tree) 64 | end 65 | 66 | @impl true 67 | def handle_call( 68 | {:get_viewport, id}, 69 | _pid, 70 | %{bears: bears} = state 71 | ) do 72 | case get_bear_from_list(id, bears) do 73 | %{pos_x: x, pos_y: y} -> 74 | position = {x, y} 75 | viewport = create_viewport(position, state) 76 | {:reply, viewport, state} 77 | 78 | nil -> 79 | {:reply, [], state} 80 | end 81 | end 82 | 83 | @impl true 84 | def handle_call( 85 | {:create_bear, [display_name: display_name, id: id, started: started]}, 86 | _pid, 87 | state 88 | ) do 89 | field = Map.get(state, :field) 90 | bear = Bear.create_bear(field, id, display_name, started) 91 | {:reply, bear, %{state | bears: [bear | Map.get(state, :bears)]}} 92 | end 93 | 94 | @impl true 95 | def handle_call( 96 | {:move, %Bear{id: id} = bear, direction, [to: {pos_x, pos_y} = position]}, 97 | _pid, 98 | %{honey_drops: honey_drops} = state 99 | ) do 100 | bear = %{bear | direction: direction, moving: true} 101 | 102 | {bear, honey_drops} = 103 | if move_to?(position, id, state) do 104 | {bear, honey_drops} = fetch_honey_drop(bear, position, honey_drops) 105 | bear = %{bear | pos_x: pos_x, pos_y: pos_y} 106 | {bear, honey_drops} 107 | else 108 | {bear, honey_drops} 109 | end 110 | 111 | state = update_state_with(state, bear) 112 | 113 | {:reply, bear, %{state | honey_drops: honey_drops}} 114 | end 115 | 116 | @impl true 117 | def handle_call({:stop, id}, _pid, %{bears: bears} = state) do 118 | bear = get_bear_from_list(id, bears) 119 | bear = %{bear | moving: false, clawing: false} 120 | state = update_state_with(state, bear) 121 | 122 | {:reply, bear, state} 123 | end 124 | 125 | @impl true 126 | def handle_call( 127 | {:claw, 128 | %Bear{honey: bear_honey, direction: direction, pos_x: bear_x, pos_y: bear_y} = bear}, 129 | _pid, 130 | state 131 | ) do 132 | bear = %{bear | clawing: true} 133 | 134 | {bear, state} = 135 | case target(direction, bear_x, bear_y, state) do 136 | %Tree{hive: %Hive{hp: hp}} = tree when hp == 1 -> 137 | dropped_honey = honey_drop(bear, tree) 138 | 139 | new_state = 140 | state 141 | |> start_new_bee(bear) 142 | |> update_state_with(%{tree | hive: nil}) 143 | |> update_state_with(dropped_honey) 144 | 145 | {bear, new_state} 146 | 147 | %Tree{hive: nil} -> 148 | {bear, update_state_with(state, bear)} 149 | 150 | %Tree{hive: %Hive{hp: hp} = hive} = tree when hp > 0 -> 151 | new_state = 152 | state 153 | |> update_state_with(%{tree | hive: %{hive | hp: hp - 1}}) 154 | |> start_new_bee(bear) 155 | 156 | {bear, new_state} 157 | 158 | %Bear{honey: other_bear_honey} = other_bear when other_bear_honey > 0 -> 159 | new_bear = %{bear | honey: bear_honey + 1} 160 | other_bear = remove_honey_from_bear(other_bear) 161 | 162 | new_state = 163 | state 164 | |> update_state_with(new_bear) 165 | |> update_state_with(other_bear) 166 | 167 | {new_bear, new_state} 168 | 169 | %Bear{honey: 0} -> 170 | {bear, update_state_with(state, bear)} 171 | 172 | _ -> 173 | {bear, update_state_with(state, bear)} 174 | end 175 | 176 | {:reply, bear, state} 177 | end 178 | 179 | @impl true 180 | def handle_call({:get_bear, id}, _pid, %{bears: bears} = state) do 181 | bear = get_bear_from_list(id, bears) 182 | {:reply, bear, state} 183 | end 184 | 185 | @impl true 186 | def handle_call({:get_bee, id}, _pid, %{bees: bees} = state) do 187 | bee = get_bee_from_list(id, bees) 188 | {:reply, bee, state} 189 | end 190 | 191 | @impl true 192 | def handle_call({:get_field, _}, _pid, %{field: field} = state) do 193 | {:reply, field, state} 194 | end 195 | 196 | @impl true 197 | def handle_call(:get_players, _pid, %{bears: bears} = state) do 198 | bears = Enum.sort(bears, &(&1.honey >= &2.honey)) 199 | 200 | {:reply, bears, state} 201 | end 202 | 203 | @impl true 204 | def handle_call({:try_to_sting, bee}, _, %{bears: bears} = state) do 205 | bears = Bee.try_to_sting(bee, bears) 206 | 207 | {:reply, true, %{state | bears: bears}} 208 | end 209 | 210 | defp honey_drop(%Bear{pos_x: bear_x, pos_y: bear_y}, %Tree{pos_x: tree_x, pos_y: tree_y}) do 211 | relative_bear_location = relative_location({bear_x - tree_x, bear_y - tree_y}) 212 | 213 | {_, honey_drops} = 214 | Enum.reduce(1..5, {relative_bear_location, []}, fn _, {direction, honey_drops} -> 215 | new_direction = Map.get(@circular_trajectory, direction) 216 | honey_drop = honey_drop_location(new_direction, tree_x, tree_y) 217 | 218 | {new_direction, [honey_drop | honey_drops]} 219 | end) 220 | 221 | honey_drops 222 | end 223 | 224 | def honey_drop_location(:left_up, x, y), do: %HoneyDrop{pos_x: x - 1, pos_y: y - 1} 225 | def honey_drop_location(:up, x, y), do: %HoneyDrop{pos_x: x - 1, pos_y: y} 226 | def honey_drop_location(:right_up, x, y), do: %HoneyDrop{pos_x: x - 1, pos_y: y + 1} 227 | def honey_drop_location(:right, x, y), do: %HoneyDrop{pos_x: x, pos_y: y + 1} 228 | def honey_drop_location(:right_down, x, y), do: %HoneyDrop{pos_x: x + 1, pos_y: y + 1} 229 | def honey_drop_location(:down, x, y), do: %HoneyDrop{pos_x: x + 1, pos_y: y} 230 | def honey_drop_location(:left_down, x, y), do: %HoneyDrop{pos_x: x + 1, pos_y: y - 1} 231 | def honey_drop_location(:left, x, y), do: %HoneyDrop{pos_x: x, pos_y: y - 1} 232 | 233 | defp relative_location({-1, 0}), do: :up 234 | defp relative_location({0, -1}), do: :left 235 | defp relative_location({0, 1}), do: :right 236 | defp relative_location({1, 0}), do: :down 237 | 238 | def remove_honey_from_bear(%{honey: honey} = bear) do 239 | bear = %{bear | honey: honey - 1} 240 | 241 | if bear.honey < 1, 242 | do: %{bear | dead: @miliseconds_dead_screen}, 243 | else: bear 244 | end 245 | 246 | @impl true 247 | def handle_cast({:move_bee, bee, directions}, state) do 248 | {x, y} = 249 | Enum.find(directions, fn direction -> 250 | direction 251 | |> position_for_direction(bee) 252 | |> move_to?(bee.id, state) 253 | end) 254 | |> position_for_direction(bee) 255 | 256 | state = update_state_with(state, %{bee | pos_x: x, pos_y: y}) 257 | 258 | {:noreply, state} 259 | end 260 | 261 | @impl true 262 | def handle_cast({:remove_bee, id}, %{bees: bees} = state) do 263 | bees = Enum.reject(bees, &(&1.id == id)) 264 | 265 | {:noreply, %{state | bees: bees}} 266 | end 267 | 268 | @impl true 269 | def handle_cast({:remove_bear, id}, %{bears: bears} = state) do 270 | bears = Enum.reject(bears, &(&1.id == id)) 271 | 272 | {:noreply, %{state | bears: bears}} 273 | end 274 | 275 | def position_for_direction(:up, %{pos_x: pos_x, pos_y: pos_y}), do: {pos_x - 1, pos_y} 276 | def position_for_direction(:down, %{pos_x: pos_x, pos_y: pos_y}), do: {pos_x + 1, pos_y} 277 | def position_for_direction(:right, %{pos_x: pos_x, pos_y: pos_y}), do: {pos_x, pos_y + 1} 278 | def position_for_direction(:left, %{pos_x: pos_x, pos_y: pos_y}), do: {pos_x, pos_y - 1} 279 | def position_for_direction(nil, %{pos_x: pos_x, pos_y: pos_y}), do: {pos_x, pos_y} 280 | 281 | def target(direction, x, y, %{bears: bears, trees: trees}) do 282 | {target_x, target_y} = 283 | case direction do 284 | :down -> {x + 1, y} 285 | :up -> {x - 1, y} 286 | :left -> {x, y - 1} 287 | :right -> {x, y + 1} 288 | end 289 | 290 | Enum.find(bears, &(&1.pos_x == target_x and &1.pos_y == target_y)) || 291 | Enum.find(trees, &(&1.pos_x == target_x and &1.pos_y == target_y)) 292 | end 293 | 294 | def update_state_with(%{honey_drops: honey_drops} = state, [%HoneyDrop{} | _] = new_honey_drops) do 295 | %{state | honey_drops: honey_drops ++ new_honey_drops} 296 | end 297 | 298 | def update_state_with(%{bees: bees} = state, bee = %Bee{}) do 299 | bees = 300 | Enum.map(bees, fn list_bee -> 301 | if list_bee.id == bee.id, 302 | do: bee, 303 | else: list_bee 304 | end) 305 | 306 | %{state | bees: bees} 307 | end 308 | 309 | def update_state_with(%{bears: bears} = state, bear = %Bear{}) do 310 | bears = 311 | Enum.map(bears, fn list_bear -> 312 | if list_bear.id == bear.id, 313 | do: bear, 314 | else: list_bear 315 | end) 316 | 317 | %{state | bears: bears} 318 | end 319 | 320 | def update_state_with(%{trees: trees} = state, tree = %Tree{}) do 321 | trees = 322 | Enum.map(trees, fn list_tree -> 323 | if list_tree.pos_y == tree.pos_y and list_tree.pos_x == tree.pos_x, 324 | do: tree, 325 | else: list_tree 326 | end) 327 | 328 | %{state | trees: trees} 329 | end 330 | 331 | def move(bear, direction, position) do 332 | GenServer.call(Game, {:move, bear, direction, position}) 333 | end 334 | 335 | def claw(bear) do 336 | GenServer.call(Game, {:claw, bear}) 337 | end 338 | 339 | def get_bear(id) do 340 | GenServer.call(Game, {:get_bear, id}) 341 | end 342 | 343 | defp get_bee_from_list(id, bees) do 344 | bees 345 | |> Enum.find(fn bee -> 346 | bee.id == id 347 | end) 348 | end 349 | 350 | defp get_bear_from_list(id, bears) do 351 | bears 352 | |> Enum.filter(fn bear -> 353 | bear.id == id 354 | end) 355 | |> List.last() 356 | end 357 | 358 | def get_field(id) do 359 | GenServer.call(Game, {:get_field, id}) 360 | end 361 | 362 | defp move_to?(position, id, %{trees: trees, bears: bears, bees: bees, field: field}) do 363 | id_bees = 364 | Task.async(fn -> 365 | not Enum.any?(bees, fn bee -> {bee.pos_x, bee.pos_y} == position and bee.id != id end) 366 | end) 367 | 368 | id_trees = 369 | Task.async(fn -> 370 | not Enum.any?(trees, fn tree -> {tree.pos_x, tree.pos_y} == position end) 371 | end) 372 | 373 | id_bears = 374 | Task.async(fn -> 375 | not Enum.any?(bears, fn bear -> 376 | {bear.pos_x, bear.pos_y} == position and bear.id != id 377 | end) 378 | end) 379 | 380 | Task.await(id_bees) and Task.await(id_bears) and Task.await(id_trees) and 381 | pos_within_field?(position, field) 382 | end 383 | 384 | defp fetch_honey_drop(%{honey: honey} = bear, position, honey_drops) do 385 | with %HoneyDrop{honey: dropped_honey} = honey_drop <- 386 | Enum.find(honey_drops, fn honey_drop -> 387 | {honey_drop.pos_x, honey_drop.pos_y} == position 388 | end) do 389 | new_bear = %{bear | honey: honey + dropped_honey} 390 | new_honey_drops = honey_drops -- [honey_drop] 391 | 392 | {new_bear, new_honey_drops} 393 | else 394 | nil -> 395 | {bear, honey_drops} 396 | end 397 | end 398 | 399 | def pos_within_field?({pos_x, pos_y}, %{height: height, width: width}) do 400 | pos_x >= 0 and pos_y >= 0 and pos_x <= height and pos_y <= width 401 | end 402 | 403 | def create_bear(display_name: display_name, id: id, started: started) do 404 | GenServer.call(Game, {:create_bear, display_name: display_name, id: id, started: started}) 405 | end 406 | 407 | def get_from_list_task({item_x, item_y}, list) do 408 | Task.async(fn -> 409 | Enum.filter(list, fn %{pos_x: pos_x, pos_y: pos_y} -> 410 | pos_x <= item_x + @horizontal_view_distance and 411 | pos_x >= item_x - @horizontal_view_distance and 412 | pos_y <= item_y + @vertical_view_distance and pos_y >= item_y - @vertical_view_distance 413 | end) 414 | end) 415 | end 416 | 417 | def item_from_list({row, column}, list) do 418 | list 419 | |> Enum.filter(fn %{pos_x: x, pos_y: y} -> x == row and y == column end) 420 | |> List.last() 421 | end 422 | 423 | def create_viewport({bear_x, bear_y} = position, %Game{ 424 | field: field, 425 | bears: bears, 426 | bees: bees, 427 | trees: trees, 428 | honey_drops: honey_drops 429 | }) do 430 | bears_task = get_from_list_task(position, bears) 431 | trees_task = get_from_list_task(position, trees) 432 | honey_drops_task = get_from_list_task(position, honey_drops) 433 | bees_task = get_from_list_task(position, bees) 434 | 435 | list = 436 | Task.await(bears_task) ++ 437 | Task.await(trees_task) ++ Task.await(honey_drops_task) ++ Task.await(bees_task) 438 | 439 | Enum.reduce( 440 | (bear_x - @horizontal_view_distance)..(bear_x + @horizontal_view_distance), 441 | [], 442 | fn row, outer -> 443 | List.insert_at( 444 | outer, 445 | -1, 446 | Enum.reduce( 447 | (bear_y - @vertical_view_distance)..(bear_y + @vertical_view_distance), 448 | [], 449 | fn column, inner -> 450 | inner ++ 451 | [ 452 | cond do 453 | not pos_within_field?({row, column}, field) -> 454 | {%Tile{type: :nothing}, nil} 455 | 456 | not is_nil(item = item_from_list({row, column}, list)) -> 457 | {get_tile(field, row, column), item} 458 | 459 | pos_within_field?({row, column}, field) -> 460 | {get_tile(field, row, column), nil} 461 | 462 | true -> 463 | {%Tile{type: :nothing}, nil} 464 | end 465 | ] 466 | end 467 | ) 468 | ) 469 | end 470 | ) 471 | end 472 | 473 | def start_new_bee(%{bees: bees} = state, bear) do 474 | if Enum.random([true, false, false, false, false, false]) do 475 | {:ok, pid} = Bee.start_link([]) 476 | 477 | %{ 478 | state 479 | | bees: [ 480 | %Bee{id: pid, pos_x: bear.pos_x - 1, pos_y: bear.pos_y - 1, catching: bear.id} | bees 481 | ] 482 | } 483 | else 484 | state 485 | end 486 | end 487 | 488 | def get_tile(field, row, column) do 489 | field.tiles 490 | |> Enum.at(row - 1) 491 | |> Enum.at(column - 1) 492 | end 493 | 494 | def get_players() do 495 | GenServer.call(Game, :get_players) 496 | end 497 | 498 | @doc """ 499 | This will remove the bear from the game, only use this when player is disconnected. It is a cast and will not return return anything. 500 | """ 501 | def remove_bear(id) do 502 | GenServer.cast(Game, {:remove_bear, id}) 503 | end 504 | 505 | defp spawn_trees() do 506 | all_x = Enum.to_list(0..40) 507 | all_y = Enum.to_list(0..40) 508 | 509 | create_tree(all_x, all_y) 510 | end 511 | 512 | # creates a tree on and returns a list of trees 513 | defp create_tree(possible_x, possible_y, trees \\ []) do 514 | if Enum.count(trees) < @number_of_trees do 515 | x = Enum.random(possible_x) 516 | y = Enum.random(possible_y) 517 | tree = %Tree{pos_x: x, pos_y: y} 518 | 519 | create_tree(possible_x -- [x], possible_y -- [y], [tree | trees]) 520 | else 521 | trees 522 | end 523 | end 524 | end 525 | --------------------------------------------------------------------------------