Packages
hangman_game
0.1.21
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`, `game_state`, `turns_left`,
`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__
@enforce_keys [:game_name, :letters]
defstruct game_name: "",
game_state: :initializing,
turns_left: 7,
letters: [],
used: MapSet.new()
@typedoc "Letter from `a` to `z`"
@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,
game_state: state,
turns_left: turns_left,
letters: [letter],
used: used
}
@typedoc "A tally map for the Hangman Game"
@type tally :: %{
game_state: state,
turns_left: turns_left,
letters: [letter | underline | charlist],
guesses: [letter]
}
@typedoc "Turns left from 7 to 0"
@type turns_left :: 0..7
@typedoc "Underline: `_`"
@type underline :: String.codepoint()
@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 \\ Hangman.Dictionary.random_word()),
do: %Game{game_name: game_name, letters: String.codepoints(word)}
@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(padding: false)
# 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.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_letters(game_state, game.letters, game.used),
guesses: MapSet.to_list(game.used)
}
end
## Private functions
@spec reveal_guessed_letters(state, [letter], used) ::
[letter | underline | charlist]
defp reveal_guessed_letters(:lost = _game_state, letters, used),
do: letters |> Enum.map(&if MapSet.member?(used, &1), do: &1, else: '#{&1}')
defp reveal_guessed_letters(_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, _already_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
MapSet.new(game.letters)
|> MapSet.subset?(game.used)
|> if(do: :won, else: :good_guess)
|> then(&put_in(game.game_state, &1))
end
defp score_guess(%Game{turns_left: 1} = game, _good_guess?),
do: %Game{game | game_state: :lost, turns_left: 0}
defp score_guess(%Game{turns_left: turns_left} = game, _good_guess?),
do: %Game{game | game_state: :bad_guess, turns_left: turns_left - 1}
end