Sergidev commited on
Commit
8752997
·
1 Parent(s): b3b10c5
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ _build/
2
+ deps/
3
+ .git/
4
+ .gitignore
5
+ Dockerfile
6
+ README.md
7
+ test/
8
+ priv/static/
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ /_build/
2
+ /deps/
3
+ /priv/static/
4
+ .elixir_ls/
5
+ .env
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM elixir:1.14-alpine
2
+
3
+ # Install build dependencies
4
+ RUN apk add --no-cache build-base npm git python3
5
+
6
+ # Install hex and rebar
7
+ RUN mix local.hex --force && \
8
+ mix local.rebar --force
9
+
10
+ # Set working directory
11
+ WORKDIR /app
12
+
13
+ # Copy mix files
14
+ COPY mix.exs mix.lock ./
15
+
16
+ # Install mix dependencies
17
+ RUN mix deps.get
18
+
19
+ # Copy assets
20
+ COPY assets assets
21
+ COPY priv priv
22
+ COPY config config
23
+ COPY lib lib
24
+
25
+ # Compile the project
26
+ RUN mix do compile
27
+
28
+ # Build assets
29
+ RUN cd assets && npm install && npm run deploy
30
+ RUN mix phx.digest
31
+
32
+ ENV MIX_ENV=prod
33
+ ENV PORT=7860
34
+
35
+ # Run the Phoenix app
36
+ CMD ["mix", "phx.server"]
Plan.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Hexalixir/
2
+ ├── .dockerignore
3
+ ├── .gitignore
4
+ ├── Dockerfile
5
+ ├── README.md
6
+ ├── config/
7
+ │ ├── config.exs
8
+ │ ├── dev.exs
9
+ │ └── prod.exs
10
+ ├── lib/
11
+ │ ├── hexalixir/
12
+ │ │ ├── game.ex
13
+ │ │ └── grid.ex
14
+ │ ├── hexalixir_web/
15
+ │ │ ├── channels/
16
+ │ │ │ └── game_channel.ex
17
+ │ │ ├── controllers/
18
+ │ │ │ └── page_controller.ex
19
+ │ │ ├── templates/
20
+ │ │ │ └── game.html.heex
21
+ │ │ └── assets/
22
+ │ │ ├── css/
23
+ │ │ │ └── app.css
24
+ │ │ └── js/
25
+ │ │ └── game.js
26
+ ├── mix.exs
27
+ └── mix.lock
assets/css/app.css ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule HexalixirWeb.GameLive do
2
+ use HexalixirWeb, :live_view
3
+ alias Hexalixir.{Game, Grid}
4
+
5
+ @impl true
6
+ def mount(_params, _session, socket) do
7
+ {:ok, assign(socket,
8
+ grid: Grid.new(),
9
+ saved_colors: %{1 => {nil, 6}, 2 => {nil, 6}},
10
+ game_won: false
11
+ )}
12
+ end
13
+
14
+ @impl true
15
+ def handle_event("click_tile", %{"x" => x, "y" => y}, socket) do
16
+ coords = {String.to_integer(x), String.to_integer(y)}
17
+
18
+ case Game.click_tile(coords) do
19
+ {:ok, new_grid, won} ->
20
+ {:noreply, assign(socket, grid: new_grid, game_won: won)}
21
+
22
+ {:error, _reason} ->
23
+ {:noreply, socket}
24
+ end
25
+ end
26
+
27
+ @impl true
28
+ def handle_event("save_color", %{"color" => color, "slot" => slot}, socket) do
29
+ slot = String.to_integer(slot)
30
+ Game.save_color(color, slot)
31
+
32
+ {:noreply, socket}
33
+ end
34
+
35
+ @impl true
36
+ def handle_event("use_saved_color", %{"x" => x, "y" => y, "slot" => slot}, socket) do
37
+ coords = {String.to_integer(x), String.to_integer(y)}
38
+ slot = String.to_integer(slot)
39
+
40
+ case Game.use_saved_color(coords, slot) do
41
+ {:ok, new_grid, won} ->
42
+ {:noreply, assign(socket, grid: new_grid, game_won: won)}
43
+
44
+ {:error, _reason} ->
45
+ {:noreply, socket}
46
+ end
47
+ end
48
+ end
assets/js/app.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import "phoenix_html"
2
+ import {Socket} from "phoenix"
3
+ import {LiveSocket} from "phoenix_live_view"
4
+
5
+ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
6
+ let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}})
7
+
8
+ liveSocket.connect()
9
+ window.liveSocket = liveSocket
10
+ ```
11
+
12
+ 10. lib/hexalixir/application.ex:
13
+ ```elixir
14
+ defmodule Hexalixir.Application do
15
+ use Application
16
+
17
+ @impl true
18
+ def start(_type, _args) do
19
+ children = [
20
+ HexalixirWeb.Telemetry,
21
+ {Phoenix.PubSub, name: Hexalixir.PubSub},
22
+ HexalixirWeb.Endpoint,
23
+ Hexalixir.Game
24
+ ]
25
+
26
+ opts = [strategy: :one_for_one, name: Hexalixir.Supervisor]
27
+ Supervisor.start_link(children, opts)
28
+ end
29
+
30
+ @impl true
31
+ def config_change(changed, _new, removed) do
32
+ HexalixirWeb.Endpoint.config_change(changed, removed)
33
+ :ok
34
+ end
35
+ end
config/config.exs ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Config
2
+
3
+ config :hexalixir, HexalixirWeb.Endpoint,
4
+ url: [host: "localhost"],
5
+ render_errors: [
6
+ formats: [html: HexalixirWeb.ErrorHTML, json: HexalixirWeb.ErrorJSON],
7
+ layout: false
8
+ ],
9
+ pubsub_server: Hexalixir.PubSub,
10
+ live_view: [signing_salt: "YOUR_SECRET_SALT"]
11
+
12
+ config :esbuild,
13
+ version: "0.14.41",
14
+ default: [
15
+ args: ~w(js/app.js --bundle --target=es2017 --outdir=../priv/static/assets
16
+ --external:/fonts/* --external:/images/*),
17
+ cd: Path.expand("../assets", __DIR__),
18
+ env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
19
+ ]
20
+
21
+ config :tailwind,
22
+ version: "3.2.4",
23
+ default: [
24
+ args: ~w(
25
+ --config=tailwind.config.js
26
+ --input=css/app.css
27
+ --output=../priv/static/assets/app.css
28
+ ),
29
+ cd: Path.expand("../assets", __DIR__)
30
+ ]
31
+
32
+ config :phoenix, :json_library, Jason
33
+
34
+ import_config "#{config_env()}.exs"
config/prod.exs ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import Config
2
+
3
+ config :hexalixir, HexalixirWeb.Endpoint,
4
+ url: [host: "0.0.0.0"],
5
+ http: [port: 7860],
6
+ cache_static_manifest: "priv/static/cache_manifest.json"
7
+
8
+ config :logger, level: :info
lib/hexalixir/game.ex ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule Hexalixir.Game do
2
+ use GenServer
3
+ alias Hexalixir.Grid
4
+
5
+ # Client API
6
+
7
+ def start_link(_opts) do
8
+ GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
9
+ end
10
+
11
+ def click_tile(coords) do
12
+ GenServer.call(__MODULE__, {:click_tile, coords})
13
+ end
14
+
15
+ def save_color(color, slot) do
16
+ GenServer.call(__MODULE__, {:save_color, color, slot})
17
+ end
18
+
19
+ def use_saved_color(coords, slot) do
20
+ GenServer.call(__MODULE__, {:use_saved_color, coords, slot})
21
+ end
22
+
23
+ # Server Callbacks
24
+
25
+ @impl true
26
+ def init(:ok) do
27
+ {:ok, %{
28
+ grid: Grid.new(),
29
+ saved_colors: %{1 => {nil, 6}, 2 => {nil, 6}}
30
+ }}
31
+ end
32
+
33
+ @impl true
34
+ def handle_call({:click_tile, coords}, _from, state) do
35
+ if Map.get(state.grid, coords) do
36
+ {:reply, {:error, :tile_already_colored}, state}
37
+ else
38
+ new_grid = color_tiles([coords | Grid.get_adjacent_coords(coords)], state.grid)
39
+ new_state = %{state | grid: new_grid}
40
+
41
+ {:reply, {:ok, new_grid, Grid.check_win(new_grid)}, new_state}
42
+ end
43
+ end
44
+
45
+ @impl true
46
+ def handle_call({:save_color, color, slot}, _from, state) do
47
+ new_state = put_in(state.saved_colors[slot], {color, 6})
48
+ {:reply, :ok, new_state}
49
+ end
50
+
51
+ @impl true
52
+ def handle_call({:use_saved_color, coords, slot}, _from, state) do
53
+ case state.saved_colors[slot] do
54
+ {nil, _} ->
55
+ {:reply, {:error, :no_color_saved}, state}
56
+
57
+ {color, 0} ->
58
+ {:reply, {:error, :no_uses_left}, state}
59
+
60
+ {color, uses} ->
61
+ if Map.get(state.grid, coords) do
62
+ {:reply, {:error, :tile_already_colored}, state}
63
+ else
64
+ new_grid = Map.put(state.grid, coords, color)
65
+ new_state = %{state |
66
+ grid: new_grid,
67
+ saved_colors: Map.put(state.saved_colors, slot, {color, uses - 1})
68
+ }
69
+
70
+ {:reply, {:ok, new_grid, Grid.check_win(new_grid)}, new_state}
71
+ end
72
+ end
73
+ end
74
+
75
+ # Private Functions
76
+
77
+ defp color_tiles(coords, grid) do
78
+ Enum.reduce(coords, grid, fn coord, acc ->
79
+ if Map.get(acc, coord) do
80
+ acc
81
+ else
82
+ Map.put(acc, coord, Grid.random_color())
83
+ end
84
+ end)
85
+ end
86
+ end
lib/hexalixir/grid.ex ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule Hexalixir.Grid do
2
+ @grid_size 20
3
+
4
+ def new do
5
+ for x <- 0..(@grid_size - 1),
6
+ y <- 0..(@grid_size - 1),
7
+ into: %{} do
8
+ {{x, y}, nil}
9
+ end
10
+ end
11
+
12
+ def get_adjacent_coords({x, y}) do
13
+ [
14
+ {x + 1, y}, {x - 1, y}, # right, left
15
+ {x, y + 1}, {x, y - 1}, # up, down
16
+ {x + 1, y - 1}, {x - 1, y + 1} # diagonals
17
+ ]
18
+ |> Enum.filter(fn {x, y} ->
19
+ x >= 0 && x < @grid_size && y >= 0 && y < @grid_size
20
+ end)
21
+ end
22
+
23
+ def random_color do
24
+ ["#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF"]
25
+ |> Enum.random()
26
+ end
27
+
28
+ def check_win(grid) do
29
+ grid
30
+ |> Map.values()
31
+ |> Enum.reject(&is_nil/1)
32
+ |> case do
33
+ [] -> false
34
+ [color | rest] -> Enum.all?(rest, &(&1 == color))
35
+ end
36
+ end
37
+ end
lib/hexalixir_web/live/game_live.ex ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule HexalixirWeb.GameLive do
2
+ use HexalixirWeb, :live_view
3
+ alias Hexalixir.{Game, Grid}
4
+
5
+ @impl true
6
+ def mount(_params, _session, socket) do
7
+ {:ok, assign(socket,
8
+ grid: Grid.new(),
9
+ saved_colors: %{1 => {nil, 6}, 2 => {nil, 6}},
10
+ game_won: false
11
+ )}
12
+ end
13
+
14
+ @impl true
15
+ def handle_event("click_tile", %{"x" => x, "y" => y}, socket) do
16
+ coords = {String.to_integer(x), String.to_integer(y)}
17
+
18
+ case Game.click_tile(coords) do
19
+ {:ok, new_grid, won} ->
20
+ {:noreply, assign(socket, grid: new_grid, game_won: won)}
21
+
22
+ {:error, _reason} ->
23
+ {:noreply, socket}
24
+ end
25
+ end
26
+
27
+ @impl true
28
+ def handle_event("save_color", %{"color" => color, "slot" => slot}, socket) do
29
+ slot = String.to_integer(slot)
30
+ Game.save_color(color, slot)
31
+
32
+ {:noreply, socket}
33
+ end
34
+
35
+ @impl true
36
+ def handle_event("use_saved_color", %{"x" => x, "y" => y, "slot" => slot}, socket) do
37
+ coords = {String.to_integer(x), String.to_integer(y)}
38
+ slot = String.to_integer(slot)
39
+
40
+ case Game.use_saved_color(coords, slot) do
41
+ {:ok, new_grid, won} ->
42
+ {:noreply, assign(socket, grid: new_grid, game_won: won)}
43
+
44
+ {:error, _reason} ->
45
+ {:noreply, socket}
46
+ end
47
+ end
48
+ end
lib/hexalixir_web/templates/game/index.html.heex ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule HexalixirWeb.GameLive do
2
+ use HexalixirWeb, :live_view
3
+ alias Hexalixir.{Game, Grid}
4
+
5
+ @impl true
6
+ def mount(_params, _session, socket) do
7
+ {:ok, assign(socket,
8
+ grid: Grid.new(),
9
+ saved_colors: %{1 => {nil, 6}, 2 => {nil, 6}},
10
+ game_won: false
11
+ )}
12
+ end
13
+
14
+ @impl true
15
+ def handle_event("click_tile", %{"x" => x, "y" => y}, socket) do
16
+ coords = {String.to_integer(x), String.to_integer(y)}
17
+
18
+ case Game.click_tile(coords) do
19
+ {:ok, new_grid, won} ->
20
+ {:noreply, assign(socket, grid: new_grid, game_won: won)}
21
+
22
+ {:error, _reason} ->
23
+ {:noreply, socket}
24
+ end
25
+ end
26
+
27
+ @impl true
28
+ def handle_event("save_color", %{"color" => color, "slot" => slot}, socket) do
29
+ slot = String.to_integer(slot)
30
+ Game.save_color(color, slot)
31
+
32
+ {:noreply, socket}
33
+ end
34
+
35
+ @impl true
36
+ def handle_event("use_saved_color", %{"x" => x, "y" => y, "slot" => slot}, socket) do
37
+ coords = {String.to_integer(x), String.to_integer(y)}
38
+ slot = String.to_integer(slot)
39
+
40
+ case Game.use_saved_color(coords, slot) do
41
+ {:ok, new_grid, won} ->
42
+ {:noreply, assign(socket, grid: new_grid, game_won: won)}
43
+
44
+ {:error, _reason} ->
45
+ {:noreply, socket}
46
+ end
47
+ end
48
+ end
mix.exs ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule Hexalixir.MixProject do
2
+ use Mix.Project
3
+
4
+ def project do
5
+ [
6
+ app: :hexalixir,
7
+ version: "0.1.0",
8
+ elixir: "~> 1.14",
9
+ elixirc_paths: elixirc_paths(Mix.env()),
10
+ start_permanent: Mix.env() == :prod,
11
+ aliases: aliases(),
12
+ deps: deps()
13
+ ]
14
+ end
15
+
16
+ def application do
17
+ [
18
+ mod: {Hexalixir.Application, []},
19
+ extra_applications: [:logger, :runtime_tools]
20
+ ]
21
+ end
22
+
23
+ defp deps do
24
+ [
25
+ {:phoenix, "~> 1.7.0"},
26
+ {:phoenix_html, "~> 3.3"},
27
+ {:phoenix_live_reload, "~> 1.4", only: :dev},
28
+ {:phoenix_live_view, "~> 0.18.16"},
29
+ {:floki, ">= 0.30.0", only: :test},
30
+ {:esbuild, "~> 0.5", runtime: Mix.env() == :dev},
31
+ {:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
32
+ {:telemetry_metrics, "~> 0.6"},
33
+ {:telemetry_poller, "~> 1.0"},
34
+ {:jason, "~> 1.2"},
35
+ {:plug_cowboy, "~> 2.5"}
36
+ ]
37
+ end
38
+
39
+ defp aliases do
40
+ [
41
+ setup: ["deps.get", "assets.setup", "assets.build"],
42
+ "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
43
+ "assets.build": ["tailwind default", "esbuild default"],
44
+ "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
45
+ ]
46
+ end
47
+ end