defmodule Adventurous.Point do defmodule Point2D do defstruct x: 0, y: 0 end @spec move(atom | %{:x => number, :y => number, optional(any) => any}, map) :: %Adventurous.Point.Point2D{x: number, y: number} @doc """ Moves Point coordinates by given values. """ def move(point, adjust_coords) do adj = Map.merge(%{x: 0, y: 0}, adjust_coords) %Point2D{ x: point.x + adj[:x], y: point.y + adj[:y] } end @doc """ Returnes coordinates of adjacent points """ def adjacent(point, type \\ :direct) do top = move(point, %{x: -1}) bottom = move(point, %{x: 1}) left = move(point, %{y: -1}) right = move(point, %{y: 1}) if type == :direct do [top, left, right, bottom] else top_left = move(point, %{x: -1, y: -1}) top_right = move(point, %{x: -1, y: 1}) bottom_left = move(point, %{x: 1, y: -1}) bottom_rigth = move(point, %{x: 1, y: 1}) [top_left, top, top_right, left, right, bottom_left, bottom, bottom_rigth] end end @spec read_num_grid(binary) :: {map, non_neg_integer, non_neg_integer} @doc """ Reads array of integers and convers it into map of point->value and dimensions of grid. """ def read_num_grid(input) do lines = String.split(input, "\n") map = Enum.with_index(lines) |> Enum.flat_map(fn {line, row_idx} -> String.graphemes(line) |> Enum.with_index |> Enum.map(fn {letter, idx} -> { %Point2D{x: row_idx, y: idx}, String.to_integer(letter) } end) end) |> Map.new width = String.length(hd(lines)) height = Enum.count(lines) {map, width, height} end @spec string_grid_with_marks(list(), {integer(), integer()}, binary()) :: binary() @doc """ Creates string representation of grid with marks (#) on given points. Blanks are filled with gap_char. """ def string_grid_with_marks(points, {max_x, max_y}, gap_char \\ ".") do sorted_points = MapSet.new(points) |> Enum.sort_by(fn %{x: x, y: y} -> {y, x} end) {string_arr, last_point} = Enum.reduce(sorted_points, {"", %Point2D{x: -1,y: 0}}, fn point, {string, prev_point} -> new_string_arr = string <> fill_gaps(prev_point, point, max_x, gap_char) <> "#" {new_string_arr, point} end) string_arr <> fill_gaps(last_point, %Point2D{x: max_x + 1, y: max_y}, max_x, gap_char) end @spec print_num_grid(map(), integer(), integer()) :: :ok @doc """ Prints point grid where map is a Map and every point has assigned integer value. """ def print_num_grid(map, width, height) do IO.puts(num_grid_to_string(map, width, height)) end @doc """ Converts num grid to array of ints """ def num_grid_to_string(map, width, height) do Enum.map(0..(height-1), fn x -> Enum.map(0..(width-1), fn y -> Map.get(map, %Point2D{x: x, y: y}) |> Integer.to_string() end) |> Enum.join() end) |> Enum.join("\n") end defp fill_gaps(prev_point, next_point, max_x, gap_char) do %{x: x, y: y} = next_point cond do y <= prev_point.y and x > prev_point.x + 1 -> String.duplicate(gap_char,x-(prev_point.x+1)) y > prev_point.y -> String.duplicate(gap_char, (if prev_point.x == max_x, do: 0, else: max_x - prev_point.x)) <> "\n" <> fill_empty_lines(y - prev_point.y - 1, max_x, gap_char) <> String.duplicate(gap_char, x) true -> "" end end defp fill_empty_lines(number, max_x, gap_char) do empty_line = String.duplicate(gap_char, max_x + 1) <> "\n" String.duplicate(empty_line, number) end defimpl Inspect, for: Point2D do import Inspect.Algebra def inspect(point, _) do concat(["(x: #{point.x}, y: #{point.y})"]) end end end