Now while it’s nice to not have the logic conflated with the GenServer
ceremony it can get a bit boilerplate-y when you are dealing with numerous GenServers
.
One compromise:
- be more organized inside the
GenServer
callback module so that it is very clear what is what
- in the test suite define some helper functions that make it easier to test the callback functions.
That way the “conflation” can be a bit less distracting.
# alias IslandsEngine.{Demo, Rules}
# state = Demo.init("Miles")
# {:reply, :error, state} = Demo.guess_coordinate(state, :player1, 1, 1)
# {:reply, :ok, state} = Demo.add_player(state, "Trane")
# {:reply, :ok, state} = Demo.position_island(state, :player1, :dot, 1, 1)
# {:reply, :ok, state} = Demo.position_island(state, :player2, :square, 1, 1)
# state = %{state | rules: %Rules{state: :player1_turn}}
# {:reply, {:miss, :none, :no_win}, state} = Demo.guess_coordinate(state, :player1, 5, 5)
# {:reply, :error, state} = Demo.guess_coordinate(state, :player1, 3, 1)
# {:reply, {:hit, :dot, :win}, state} = Demo.guess_coordinate(state, :player2, 1, 1)
defmodule IslandsEngine.Demo do
alias IslandsEngine.Game
### helper functions for testing - i.e. should be under "test"" ###
def init(name) do
{:ok, state} = Game.init(name)
state
end
def add_player(state, name),
do: Game.handle_call({:add_player, name}, self(), state)
def position_island(state, player, key, row, col),
do: Game.handle_call({:position_island, player, key, row, col}, self(), state)
def set_islands(state, player),
do: Game.handle_call({:set_islands, player}, self(), state)
def guess_coordinate(state, player, row, col),
do: Game.handle_call({:guess_coordinate, player, row, col}, self(), state)
end
defmodule IslandsEngine.Game do
use GenServer
alias IslandsEngine.{Board, Coordinate, Guesses, Island, Rules}
# --- GenServer Client API ---
@players [:player1, :player2]
def add_player(game, name) when is_binary(name),
do: GenServer.call(game, {:add_player, name})
def position_island(game, player, key, row, col) when player in @players,
do: GenServer.call(game, {:position_island, player, key, row, col})
def set_islands(game, player) when player in @players,
do: GenServer.call(game, {:set_islands, player})
def guess_coordinate(game, player, row, col) when player in @players,
do: GenServer.call(game, {:guess_coordinate, player, row, col})
# --- GenServer Ceremony ---
def via_tuple(name),
do: {:via, Registry, {Registry.Game, name}}
def start_link(name) when is_binary(name),
do: GenServer.start_link(__MODULE__, name, name: via_tuple(name))
def init(name),
do: {:ok, game_init(name)}
def handle_call({:add_player, name}, _from, state),
do: handle_add_player(state, name)
def handle_call({:position_island, player, key, row, col}, _from, state),
do: handle_position_island(state, player, key, row, col)
def handle_call({:set_islands, player}, _from, state),
do: handle_set_islands(state, player)
def handle_call({:guess_coordinate, player, row, col}, _from, state),
do: handle_guess_coordinate(state, player, row, col)
def handle_info(:first, state) do
IO.puts("This message has been handled by handle_info/2, matching on :first.")
{:noreply, state}
end
defp reply_success(state_data, reply), do: {:reply, reply, state_data}
# --- Game Module Logic
defp game_init(name) do
player1 = %{name: name, board: Board.new(), guesses: Guesses.new()}
player2 = %{name: nil, board: Board.new(), guesses: Guesses.new()}
%{player1: player1, player2: player2, rules: %Rules{}}
end
defp handle_add_player(state, name) do
with {:ok, rules} <- Rules.check(state.rules, :add_player) do
state
|> update_player2_name(name)
|> update_rules(rules)
|> reply_success(:ok)
else
:error -> {:reply, :error, state}
end
end
defp handle_position_island(state, player, key, row, col) do
board = player_board(state, player)
with {:ok, rules} <-
Rules.check(state.rules, {:position_islands, player}),
{:ok, coordinate} <-
Coordinate.new(row, col),
{:ok, island} <-
Island.new(key, coordinate),
%{} = board <-
Board.position_island(board, key, island) do
state
|> update_board(player, board)
|> update_rules(rules)
|> reply_success(:ok)
else
:error ->
{:reply, :error, state}
{:error, :invalid_coordinate} ->
{:reply, {:error, :invalid_coordinate}, state}
{:error, :invalid_island_type} ->
{:reply, {:error, :invalid_island_type}, state}
end
end
defp handle_set_islands(state, player) do
board = player_board(state, player)
with {:ok, rules} <- Rules.check(state.rules, {:set_islands, player}),
true <- Board.all_islands_positioned?(board) do
state
|> update_rules(rules)
|> reply_success({:ok, board})
else
:error -> {:reply, :error, state}
false -> {:reply, {:error, :not_all_islands_positioned}, state}
end
end
defp handle_guess_coordinate(state, player, row, col) do
opponent_key = opponent(player)
opponent_board = player_board(state, opponent_key)
with {:ok, rules} <-
Rules.check(state.rules, {:guess_coordinate, player}),
{:ok, coordinate} <-
Coordinate.new(row, col),
{hit_or_miss, forested_island, win_status, opponent_board} <-
Board.guess(opponent_board, coordinate),
{:ok, rules} <-
Rules.check(rules, {:win_check, win_status}) do
state
|> update_board(opponent_key, opponent_board)
|> update_guesses(player, hit_or_miss, coordinate)
|> update_rules(rules)
|> reply_success({hit_or_miss, forested_island, win_status})
else
:error ->
{:reply, :error, state}
{:error, :invalid_coordinate} ->
{:reply, {:error, :invalid_coordinate}, state}
end
end
defp player_board(state_data, player), do: Map.get(state_data, player).board
defp opponent(:player1), do: :player2
defp opponent(:player2), do: :player1
defp update_player2_name(state_data, name), do: put_in(state_data.player2.name, name)
defp update_board(state_data, player, board),
do: Map.update!(state_data, player, fn player -> %{player | board: board} end)
defp update_rules(state_data, rules), do: %{state_data | rules: rules}
defp update_guesses(state_data, player_key, hit_or_miss, coordinate) do
update_in(state_data[player_key].guesses, fn guesses ->
Guesses.add(guesses, hit_or_miss, coordinate)
end)
end
end