# ┌───────────────────────────────────────────────────────────────────────┐
# │ Inspired by the book "Functional Web Development" by Lance Halvorsen. │
# └───────────────────────────────────────────────────────────────────────┘
defmodule Islands.Score do
@moduledoc """
A score struct and functions for the _Game of Islands_.
The score struct contains the fields `name`, `gender`, `hits`, `misses` and
`forested_types` representing the characteristics of a score in the
_Game of Islands_.
##### Inspired by the book [Functional Web Development](https://pragprog.com/titles/lhelph/functional-web-development-with-elixir-otp-and-phoenix/) by Lance Halvorsen.
"""
alias __MODULE__
alias IO.ANSI.Plus, as: ANSI
alias Islands.Client.IslandType
alias Islands.{Board, Game, Island, Player, PlayerID}
@island_type_codes ["a", "d", "l", "s", "q"]
@player_ids [:player1, :player2]
@score_width 21
@sp ANSI.cursor_right()
@sp_gender 2
@symbols [f: "♀", m: "♂"]
@derive Jason.Encoder
@enforce_keys [:name, :gender, :hits, :misses, :forested_types]
defstruct [:name, :gender, :hits, :misses, :forested_types]
@typedoc "A score struct for the Game of Islands"
@type t :: %Score{
name: Player.name(),
gender: Player.gender(),
hits: non_neg_integer,
misses: non_neg_integer,
forested_types: [Island.type()]
}
@doc """
Creates a score struct from a player's board struct.
"""
@spec board_score(Game.t(), PlayerID.t()) :: t
def board_score(%Game{} = game, player_id) when player_id in @player_ids do
player = game[player_id]
board = player.board
new(player, board)
end
@doc """
Creates a score struct from an opponent's board struct.
"""
@spec guesses_score(Game.t(), PlayerID.t()) :: t
def guesses_score(%Game{} = game, player_id) when player_id in @player_ids do
opponent = game[Game.opponent_id(player_id)]
board = opponent.board
new(opponent, board)
end
@doc """
Prints `score` formatted with embedded ANSI escapes.
"""
@spec format(t, Keyword.t()) :: :ok
def format(%Score{} = score, options) do
{up, right} = {options[:up], options[:right]}
[
[cursor_up(up), ANSI.cursor_right(right), player(score)],
["\n", ANSI.cursor_right(right), top_score(score)],
["\n", ANSI.cursor_right(right), bottom_score(score)]
]
|> ANSI.puts()
end
## Private functions
@spec new(Player.t(), Board.t()) :: t
defp new(player, board) do
%Score{
name: player.name,
gender: player.gender,
hits: Board.hits(board),
misses: Board.misses(board),
forested_types: Board.forested_types(board)
}
end
@spec cursor_up(non_neg_integer) :: String.t()
defp cursor_up(up) when up > 0, do: ANSI.cursor_up(up)
defp cursor_up(_up), do: ""
@spec player(t) :: ANSI.ansilist()
defp player(%Score{name: name, gender: gender}) do
name = String.slice(name, 0, @score_width - @sp_gender)
span = div(@score_width + String.length(name) + @sp_gender, 2) - @sp_gender
# The "visible" width of `player/1` (ignoring ANSI escapes) is 11 to 21...
[
[:chartreuse_yellow, String.pad_leading(name, span)],
[:reset, @sp, :spring_green, "#{@symbols[gender]}"]
]
end
@spec top_score(t) :: ANSI.ansilist()
defp top_score(%Score{hits: hits, misses: misses}) do
# The "visible" width of `top_score/1` (ignoring ANSI escapes) is 21...
[
[:chartreuse_yellow, "hits: "],
[:spring_green, String.pad_leading("#{hits}", 2)],
[:chartreuse_yellow, " misses: "],
[:spring_green, String.pad_leading("#{misses}", 2)]
]
end
@spec bottom_score(t) :: ANSI.ansilist()
defp bottom_score(score) do
# The "visible" width of `bottom_score/1` (ignoring ANSI escapes) is 20...
[
[:reset, :spring_green, :underline, "forested"],
[:reset, @sp, :chartreuse_yellow, "➔", forested_codes(score)]
]
end
@spec forested_codes(t) :: ANSI.ansilist()
defp forested_codes(%Score{forested_types: forested_types}) do
for code <- @island_type_codes do
[attr(IslandType.new(code) in forested_types), code]
end
end
@spec attr(boolean) :: ANSI.ansilist()
defp attr(_forested? = true), do: [:reset, @sp, :spring_green, :underline]
defp attr(_forested?), do: [:reset, @sp, :chartreuse_yellow]
end