From 1425d35280b82f36a17a846571c75327483c1ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joa=CC=83o=20Ota=CC=81vio=20Biondo?= Date: Sat, 5 Oct 2024 18:29:23 -0300 Subject: [PATCH] Add clear and random button --- lib/game_of_life/grid.ex | 29 ++++++++----- .../components/core_components.ex | 3 +- lib/game_of_life_web/live/home_live.ex | 38 ++++++++++------ test/game_of_life/grid_test.exs | 43 +++++++++++++++++-- 4 files changed, 83 insertions(+), 30 deletions(-) diff --git a/lib/game_of_life/grid.ex b/lib/game_of_life/grid.ex index 1790439..39a1340 100644 --- a/lib/game_of_life/grid.ex +++ b/lib/game_of_life/grid.ex @@ -4,7 +4,6 @@ defmodule GameOfLife.Grid do """ alias GameOfLife.Cell - alias GameOfLife.Grid @type position :: {row :: integer, columns :: integer} @type cells :: %{required(position) => Cell.t()} @@ -30,7 +29,7 @@ defmodule GameOfLife.Grid do cells = matrix_to_cells(cell_matrix, rows, cols) - %Grid{cells: cells, rows: rows, cols: cols, generation: 1} + %__MODULE__{cells: cells, rows: rows, cols: cols, generation: 1} else {:error, reason} -> raise(ArgumentError, reason) end @@ -70,8 +69,15 @@ defmodule GameOfLife.Grid do |> new!() end + @spec clear(t()) :: t + def clear(%__MODULE__{} = grid) do + new_cells = Map.new(grid.cells, fn {position, _value} -> {position, :dead} end) + + %__MODULE__{grid | cells: new_cells, generation: 1} + end + @spec neighbors(t(), position()) :: MapSet.t(position) - def neighbors(grid, {cell_x, cell_y}) do + def neighbors(%__MODULE__{} = grid, {cell_x, cell_y}) do for offset_x <- [-1, 0, 1], offset_y <- [-1, 0, 1], neighbor = {offset_x + cell_x, offset_y + cell_y}, @@ -82,10 +88,11 @@ defmodule GameOfLife.Grid do end @spec inside_grid?(t(), position()) :: boolean() - defp inside_grid?(grid, {x, y}), do: x in 0..(grid.rows - 1) and y in 0..(grid.cols - 1) + defp inside_grid?(%__MODULE__{} = grid, {x, y}), + do: x in 0..(grid.rows - 1) and y in 0..(grid.cols - 1) @spec count_neighbors_alive(t(), position()) :: non_neg_integer - def count_neighbors_alive(grid, cell_position) do + def count_neighbors_alive(%__MODULE__{} = grid, cell_position) do grid |> neighbors(cell_position) |> Enum.map(fn neighbor -> get_cell(grid, neighbor) end) @@ -93,10 +100,10 @@ defmodule GameOfLife.Grid do end @spec get_cell(t(), position()) :: Cell.t() - def get_cell(grid, cell_position), do: Map.get(grid.cells, cell_position, :dead) + def get_cell(%__MODULE__{} = grid, cell_position), do: Map.get(grid.cells, cell_position, :dead) @spec toggle_cell(t(), position()) :: t() - def toggle_cell(grid, cell_position) do + def toggle_cell(%__MODULE__{} = grid, cell_position) do if not inside_grid?(grid, cell_position) do raise ArgumentError, "position given is outside the grid. Got '#{inspect(cell_position)}'" end @@ -111,15 +118,15 @@ defmodule GameOfLife.Grid do %__MODULE__{grid | cells: updated_cells} end - defimpl String.Chars, for: Grid do - @spec to_string(Grid.t()) :: String.t() - def to_string(%Grid{} = grid) do + defimpl String.Chars do + @spec to_string(GameOfLife.Grid.t()) :: String.t() + def to_string(%GameOfLife.Grid{} = grid) do grid_str = grid.cells |> Map.keys() |> Enum.sort() |> Enum.map_join("|", fn position -> - cell_str = grid |> Grid.get_cell(position) |> Cell.to_string() + cell_str = grid |> GameOfLife.Grid.get_cell(position) |> Cell.to_string() last_row_index = grid.rows - 1 last_col_index = grid.cols - 1 diff --git a/lib/game_of_life_web/components/core_components.ex b/lib/game_of_life_web/components/core_components.ex index 500c206..d11076d 100644 --- a/lib/game_of_life_web/components/core_components.ex +++ b/lib/game_of_life_web/components/core_components.ex @@ -218,7 +218,7 @@ defmodule GameOfLifeWeb.CoreComponents do <.button>Send! <.button phx-click="go" class="ml-2">Send! """ - attr :type, :string, default: nil + attr :type, :string, default: "button" attr :class, :string, default: nil attr :rest, :global, include: ~w(disabled form name value) @@ -231,6 +231,7 @@ defmodule GameOfLifeWeb.CoreComponents do class={[ "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3", "text-sm font-semibold leading-6 text-white active:text-white/80", + "disabled:opacity-50 disabled:pointer-events-none", @class ]} {@rest} diff --git a/lib/game_of_life_web/live/home_live.ex b/lib/game_of_life_web/live/home_live.ex index 8c8e4dc..29b7258 100644 --- a/lib/game_of_life_web/live/home_live.ex +++ b/lib/game_of_life_web/live/home_live.ex @@ -25,22 +25,20 @@ defmodule GameOfLifeWeb.HomeLive do
- <.button - type="button" - phx-click="start" - disabled={@state == :running} - class="disabled:opacity-50 disabled:pointer-events-none" - > - Start + <.button :if={@state == :paused} phx-click="start"> + <.icon name="hero-play" class="size-5" /> Start - <.button - type="button" - phx-click="stop" - disabled={@state == :paused} - class="disabled:opacity-50 disabled:pointer-events-none" - > - Pause + <.button :if={@state == :running} phx-click="stop"> + <.icon name="hero-pause" class="size-5" /> Pause + + + <.button phx-click="random_grid" disabled={@state == :running}> + Random + + + <.button phx-click="clear_grid" disabled={@state == :running}> + Clear
@@ -87,6 +85,18 @@ defmodule GameOfLifeWeb.HomeLive do {:noreply, socket} end + @impl true + def handle_event("random_grid", _params, %Socket{} = socket) do + socket = assign(socket, grid: Grid.random(@grid_size, @grid_size)) + {:noreply, socket} + end + + @impl true + def handle_event("clear_grid", _params, %Socket{} = socket) do + socket = update(socket, :grid, &Grid.clear/1) + {:noreply, socket} + end + @impl true def handle_event("cell_clicked", %{"row" => row, "col" => col} = _params, %Socket{} = socket) do socket = update(socket, :grid, fn grid -> Grid.toggle_cell(grid, {row, col}) end) diff --git a/test/game_of_life/grid_test.exs b/test/game_of_life/grid_test.exs index 68577af..34d10b5 100644 --- a/test/game_of_life/grid_test.exs +++ b/test/game_of_life/grid_test.exs @@ -85,6 +85,29 @@ defmodule GameOfLife.GridTest do |> Enum.all?(&(&1 in [:alive, :dead])) end + describe "clear/1" do + test "should set all cells to :dead" do + matrix = [ + [1, 1], + [1, 1] + ] + + grid = matrix |> Grid.new!() |> struct!(generation: 7) + + assert %Grid{ + rows: 2, + cols: 2, + generation: 1, + cells: %{ + {0, 0} => :dead, + {0, 1} => :dead, + {1, 0} => :dead, + {1, 1} => :dead + } + } = Grid.clear(grid) + end + end + test "neighbors/2" do grid = Grid.random(3, 3) @@ -180,10 +203,22 @@ defmodule GameOfLife.GridTest do end test "should raise when position is outside the grid", %{grid: grid} do - assert_raise(ArgumentError, "position given is outside the grid. Got '{-1, 0}'", fn -> Grid.toggle_cell(grid, {-1, 0}) end) - assert_raise(ArgumentError, "position given is outside the grid. Got '{0, -1}'", fn -> Grid.toggle_cell(grid, {0, -1}) end) - assert_raise(ArgumentError, "position given is outside the grid. Got '{2, 0}'", fn -> Grid.toggle_cell(grid, {2, 0}) end) - assert_raise(ArgumentError, "position given is outside the grid. Got '{0, 2}'", fn -> Grid.toggle_cell(grid, {0, 2}) end) + assert_raise(ArgumentError, "position given is outside the grid. Got '{-1, 0}'", fn -> + Grid.toggle_cell(grid, {-1, 0}) + end) + + assert_raise(ArgumentError, "position given is outside the grid. Got '{0, -1}'", fn -> + Grid.toggle_cell(grid, {0, -1}) + end) + + assert_raise(ArgumentError, "position given is outside the grid. Got '{2, 0}'", fn -> + Grid.toggle_cell(grid, {2, 0}) + end) + + assert_raise(ArgumentError, "position given is outside the grid. Got '{0, 2}'", fn -> + Grid.toggle_cell(grid, {0, 2}) + end) + assert %Grid{} = Grid.toggle_cell(grid, {0, 0}) assert %Grid{} = Grid.toggle_cell(grid, {0, 1}) assert %Grid{} = Grid.toggle_cell(grid, {1, 0})