Hangman - Protocols
Modulos requeridos
defmodule Hangman.State do
@moduledoc """
The Hangman game state
"""
@enforce_keys [:word, :goal]
defstruct [
:word,
:goal,
misses: MapSet.new(),
matches: MapSet.new(),
limit: 5,
mask: "_",
completed?: false
]
@type t :: %__MODULE__{
word: String.t(),
goal: MapSet.t(),
misses: MapSet.t(),
matches: MapSet.t(),
limit: pos_integer(),
mask: String.t(),
completed?: boolean()
}
@doc """
Creates the initial game state
"""
@spec new(String.t()) :: t()
def new(word) when is_binary(word) do
word = String.downcase(word)
goal = word |> String.codepoints() |> MapSet.new()
struct!(__MODULE__, word: word, goal: goal)
end
end
defmodule Hangman.GameLogic do
@moduledoc """
Main logic for the game
"""
alias Hangman.State
@doc """
Returns the game state after the user takes a guess
## Examples
iex> state = Hangman.State.new("hangman")
iex> guess("a", state)
%Hangman.State{matches: MapSet.new(["a"]), word: "hangman", goal: MapSet.new(["a", "g", "h", "m", "n"])}
"""
@spec guess(String.t(), State.t()) :: State.t()
def guess(letter, %State{} = state) do
%{goal: goal, matches: matches, misses: misses, limit: limit} = state
if MapSet.member?(goal, letter) do
matches = MapSet.put(matches, letter)
completed? = MapSet.equal?(matches, goal)
%{state | matches: matches, completed?: completed?}
else
%{state | misses: MapSet.put(misses, letter), limit: limit - 1}
end
end
end
defmodule Hangman.Goal.Api do
@moduledoc """
Word generator API
"""
@doc """
Generates a word, phrase, or sentence.
"""
@callback generate() :: String.t()
end
defmodule Hangman.Goal do
@moduledoc """
Goal (word, phrase, sentence) generator entry point
"""
@behaviour Hangman.Goal.Api
@impl true
def generate do
client = Application.get_env(:hangman, :goal_generator, Hangman.Goal.DummyGenerator)
client.generate()
end
end
defmodule Hangman.Goal.DummyGenerator do
@behaviour Hangman.Goal.Api
@impl true
def generate do
Enum.random(["hangman", "letterbox", "wheel of fortune"])
end
end
Protocols
Introducción
TODO
defimpl Inspect, for: Hangman.State do
def inspect(%Hangman.State{limit: limit, completed?: false} = state, _opts) when limit > 0 do
"#{mask_word(state)}, remaining attempts: #{limit}"
end
def inspect(%Hangman.State{limit: limit, word: word}, _opts) when limit > 0 do
"You won, word was: #{word}"
end
def inspect(%Hangman.State{word: word}, _opts) do
"Game Over, word was: #{word}"
end
## Helpers
defp mask_word(%{matches: matches, mask: mask, word: word} = _state) do
if MapSet.size(matches) > 0 do
matches = Enum.join(matches)
String.replace(word, ~r/[^#{matches}]/, mask)
else
String.replace(word, ~r/./, mask)
end
end
end
Tras implementar el protocolo previo, podemos eliminar el modulo Hangman.View
y las referencias al mismo.
defmodule Hangman do
@moduledoc """
The famous Hangman game
"""
use GenServer
alias Hangman.GameLogic
alias Hangman.Goal
alias Hangman.State
require Logger
@idle_timeout :timer.seconds(15)
## Client
@doc """
Starts the game
"""
@spec start_link(atom()) :: GenServer.on_start()
def start_link(player) when is_atom(player) do
GenServer.start_link(__MODULE__, player, name: player)
end
@doc """
Returns the masked word
"""
@spec get(atom() | pid()) :: String.t()
def get(player) do
GenServer.call(player, :get)
end
@doc """
Lets the user to take a guess
"""
@spec take_a_guess(atom() | pid(), String.t()) :: String.t()
def take_a_guess(player, letter) do
GenServer.call(player, {:guess, letter})
end
@doc """
Stops the game for the given player
"""
def stop(player), do: GenServer.stop(player)
## Callbacks
@impl GenServer
def init(player) do
game_state = State.new(Goal.generate())
{:ok, %{name: player, game_state: game_state}, @idle_timeout}
end
@impl GenServer
def handle_call(:get, _from, state) do
{:reply, Map.get(state, :game_state), state, @idle_timeout}
end
def handle_call({:guess, letter}, _from, %{game_state: game_state} = state) do
new_game_state =
letter
|> String.downcase()
|> GameLogic.guess(game_state)
{:reply, new_game_state, %{state | game_state: new_game_state}, @idle_timeout}
end
@impl GenServer
def handle_info(:timeout, %{name: player} = state) do
Logger.info("Stopping game session for #{player}")
{:stop, :normal, state}
end
end
player = :milton
{:ok, pid} = Hangman.start_link(player)
Hangman.get(player)
for letter <- ["h", "a", "n", "g", "m"] do
Hangman.take_a_guess(player, letter)
end
if Process.alive?(pid), do: Hangman.stop(player)
Hangman.start_link(player)
for letter <- ["q", "w", "e", "r", "t"] do
Hangman.take_a_guess(player, letter)
end