defmodule Buzzword.Bingo.Summary.Formatter do
@moduledoc """
Writes a summary or game struct as a formatted table to `:stdio`.
"""
alias Buzzword.Bingo.{Game, Player, Square, Summary}
alias IO.ANSI.Plus, as: ANSI
@dark :black
@light :light_white
@doc """
Writes the given summary or game struct as a formatted table to `:stdio`.
"""
@spec print(Summary.t() | Game.t()) :: :ok
def print(summary_or_game)
def print(%Summary{} = summary) do
print_squares(summary.squares)
print_scores(summary.scores, length(summary.squares))
print_bingo(summary.winner)
end
def print(%Game{} = game), do: Summary.new(game) |> print()
## Private functions
@spec print_squares([[Square.t()]]) :: :ok
defp print_squares(squares) do
IO.write("\n")
column_width = column_width(squares)
game_size = length(squares)
Enum.each(squares, &print_row(&1, column_width, game_size))
end
@spec print_row([Square.t()], pos_integer, Game.size()) :: :ok
defp print_row(squares, column_width, game_size) do
squares
|> Enum.with_index(1)
|> Enum.each(&print_square(&1, column_width, game_size))
end
@spec print_square({Square.t(), pos_integer}, pos_integer, Game.size()) :: :ok
defp print_square({square, index}, column_width, game_size) do
[
color_of_square(square),
text_in_square_padded(square, column_width),
:reset,
if(index < game_size, do: " | ", else: "\n")
]
|> ANSI.write()
end
@spec color_of_square(Square.t()) :: atom | [atom]
defp color_of_square(square) do
case square.marked_by do
nil -> @light
player -> [:"#{adapt(player.color)}_background", @dark]
end
end
@spec adapt(Player.color()) :: adapted_color :: String.t()
defp adapt("#a4deff"), do: "aqua"
defp adapt("rgb(164, 222, 255)"), do: "aqua"
defp adapt("#f9cedf"), do: "orchid"
defp adapt("rgb(249, 206, 223)"), do: "orchid"
defp adapt("#d3c5f1"), do: "moon_raker"
defp adapt("rgb(211, 197, 241)"), do: "moon_raker"
defp adapt("#acc9f5"), do: "malibu"
defp adapt("rgb(172, 201, 245)"), do: "malibu"
defp adapt("#aeeace"), do: "pale_green"
defp adapt("rgb(174, 234, 206)"), do: "pale_green"
defp adapt("#96d7b9"), do: "bondi_blue"
defp adapt("rgb(150, 215, 185)"), do: "bondi_blue"
defp adapt("#fce8bd"), do: "canary"
defp adapt("rgb(252, 232, 189)"), do: "canary"
defp adapt("#fcd8ac"), do: "dandelion"
defp adapt("rgb(252, 216, 172)"), do: "dandelion"
defp adapt(color), do: color
@spec text_in_square_padded(Square.t(), pos_integer) :: String.t()
defp text_in_square_padded(square, column_width) do
square
|> text_in_square()
|> String.pad_trailing(column_width)
end
@spec text_in_square(Square.t()) :: String.t()
defp text_in_square(square), do: "#{square.phrase} (#{square.points})"
@spec column_width([[Square.t()]]) :: pos_integer
defp column_width(squares) do
squares
|> List.flatten()
|> Enum.map(&text_in_square/1)
|> Enum.map(&String.length/1)
|> Enum.max()
end
@spec print_scores(Summary.scores(), Game.size()) :: :ok
defp print_scores(scores, game_size) do
["\n", :underline, @light, "Scores:", :reset, " "] |> ANSI.write()
scores
|> Enum.sort()
|> Enum.chunk_every(game_size)
|> Enum.each(&print_score_chunk/1)
map_size(scores) |> skip() |> IO.write()
end
@spec skip(non_neg_integer) :: String.t()
defp skip(0 = _size), do: "\n\n"
defp skip(_size), do: "\n"
@spec print_score_chunk([Summary.score()]) :: :ok
defp print_score_chunk(scores) do
Enum.each(scores, &print_score/1)
IO.write("\n ")
end
@spec print_score(Summary.score()) :: :ok
defp print_score({name, %{color: color, score: score, marked: marked}}) do
[
:"#{adapt(color)}_background",
@dark,
"#{name}: #{score} (#{marked} square#{(marked == 1 && "") || "s"})",
:reset,
"\t"
]
|> ANSI.write()
end
@spec print_bingo(Player.t() | nil) :: :ok
defp print_bingo(%Player{name: name} = _winner) do
[:gold_background, @dark, " ⭐⭐⭐ BINGO! #{name} wins! ", :reset]
|> ANSI.puts()
end
defp print_bingo(nil) do
# [:deco_background, @dark, " 🙁 No Bingo (yet) ", :reset]
[:deco_background, @dark, " ☹ No Bingo (yet) ", :reset]
|> ANSI.puts()
end
end