Packages
hangman_game
0.1.8
0.1.56
0.1.55
0.1.54
0.1.52
0.1.51
0.1.50
0.1.49
0.1.48
0.1.47
0.1.46
0.1.45
0.1.44
0.1.43
0.1.42
0.1.41
0.1.40
0.1.39
0.1.38
0.1.37
0.1.36
0.1.35
0.1.34
0.1.33
0.1.32
0.1.31
0.1.30
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.22
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.13
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
A game struct and functions for the Hangman Game.
Current section
Files
Jump to
Current section
Files
lib/hangman/game.ex
defmodule Hangman.Game do
@moduledoc """
A game struct and functions for the _Hangman Game_.
The game struct contains the fields `game_name`, `turns_left`, `game_state`,
`letters` and `used` representing the characteristics of a game in the
_Hangman Game_.
##### Based on the course [Elixir for Programmers](https://codestool.coding-gnome.com/courses/elixir-for-programmers) by Dave Thomas.
"""
alias __MODULE__
alias Hangman.Dictionary
@enforce_keys [:game_name, :letters]
defstruct game_name: "",
turns_left: 7,
game_state: :initializing,
letters: [],
used: MapSet.new()
@typedoc "Letter from `a` to `z` or `_` (underscore)"
@type letter :: String.codepoint()
@typedoc "Game name"
@type name :: String.t()
@typedoc "Game state"
@type state ::
:initializing
| :good_guess
| :bad_guess
| :already_used
| :lost
| :won
@typedoc "A game struct for the Hangman Game"
@type t :: %Game{
game_name: name,
turns_left: turns_left,
game_state: state,
letters: [letter],
used: used
}
@typedoc "A tally map for the Hangman Game"
@type tally :: %{
game_state: state,
turns_left: turns_left,
letters: [letter | charlist],
guesses: [letter]
}
@typedoc "Turns left from 7 to 0"
@type turns_left :: 0..7
@typedoc "A set of used (guessed) letters"
@type used :: MapSet.t(letter)
@doc """
Creates a game struct from a `game_name` and a `word` to be guessed. The default value for `game_name` is provided by function `random_name/0` and for
`word` by function `Hangman.Dictionary.random_word/0`.
## Examples
iex> alias Hangman.Game
iex> game = Game.new()
iex> game_name_length = String.length(game.game_name)
iex> {game.game_state, game.turns_left, game_name_length in 4..10}
{:initializing, 7, true}
iex> alias Hangman.Game
iex> game = Game.new("Mr Smith")
iex> {game.game_state, game.turns_left, game.game_name}
{:initializing, 7, "Mr Smith"}
iex> alias Hangman.Game
iex> game = Game.new("Wibble", "wibble")
iex> {game.game_state, game.turns_left, game.game_name, game.letters}
{:initializing, 7, "Wibble", ~w[w i b b l e]}
"""
@spec new(name, String.t()) :: t
def new(game_name \\ random_name(), word \\ Dictionary.random_word()) do
%Game{game_name: game_name, letters: String.codepoints(word)}
end
@doc """
Returns a random name of 4 to 10 characters.
## Examples
iex> alias Hangman.Game
iex> for _ <- 0..99, uniq: true do
iex> length = Game.random_name() |> String.length()
iex> length in 4..10
iex> end
[true]
"""
@spec random_name :: name
def random_name do
length = Enum.random(4..10)
:crypto.strong_rand_bytes(length)
|> Base.url_encode64()
# Starting at 0 with length "length"...
|> binary_part(0, length)
end
@doc """
Makes a move by guessing a letter.
## Examples
iex> alias Hangman.Game
iex> game = Game.random_name() |> Game.new()
iex> Game.make_move(game, "a").game_state in [:good_guess, :bad_guess]
true
"""
@spec make_move(t, guess :: letter) :: t
def make_move(%Game{game_state: state} = game, _) when state in [:won, :lost],
do: game
# Guess not validated here; should be done in client interface...
def make_move(%Game{used: used} = game, guess),
do: make_move(game, guess, MapSet.member?(used, guess))
@doc """
Returns a tally map externalizing `game`.
## Examples
iex> alias Hangman.Game
iex> game = Game.random_name() |> Game.new("anaconda")
iex> game = Game.make_move(game, "a")
iex> tally = Game.tally(game)
iex> {tally.game_state, tally.turns_left, tally.letters, tally.guesses}
{:good_guess, 7, ~w[a _ a _ _ _ _ a], ~w[a]}
"""
@spec tally(t) :: tally
def tally(%Game{game_state: game_state, turns_left: turns_left} = game) do
%{
game_state: game_state,
turns_left: turns_left,
letters: reveal_guessed(game_state, game.letters, game.used),
guesses: MapSet.to_list(game.used)
}
end
## Private functions
@spec reveal_guessed(state, [letter], used) :: [letter | charlist]
defp reveal_guessed(:lost = _game_state, letters, used),
do: letters |> Enum.map(&if MapSet.member?(used, &1), do: &1, else: '#{&1}')
defp reveal_guessed(_game_state, letters, used),
do: letters |> Enum.map(&if MapSet.member?(used, &1), do: &1, else: "_")
@spec make_move(t, letter, boolean) :: t
defp make_move(game, _guess, _already_used = true),
do: put_in(game.game_state, :already_used)
defp make_move(game, guess, _never_used) do
update_in(game.used, &MapSet.put(&1, guess))
|> score_guess(guess in game.letters)
end
@spec score_guess(t, boolean) :: t
defp score_guess(game, _good_guess = true) do
state =
if MapSet.new(game.letters) |> MapSet.subset?(game.used),
do: :won,
else: :good_guess
put_in(game.game_state, state)
end
defp score_guess(%Game{turns_left: 1} = game, _bad_guess),
do: %Game{game | game_state: :lost, turns_left: 0}
defp score_guess(%Game{turns_left: 0} = game, _bad_guess), do: game
defp score_guess(%Game{turns_left: turns_left} = game, _bad_guess),
do: %Game{game | game_state: :bad_guess, turns_left: turns_left - 1}
end