|
| 1 | +defmodule GoCounting do |
| 2 | + @type position :: {integer, integer} |
| 3 | + @type owner :: %{owner: atom, territory: [position]} |
| 4 | + @type territories :: %{white: [position], black: [position], none: [position]} |
| 5 | + |
| 6 | + @doc """ |
| 7 | + Return the owner and territory around a position |
| 8 | +
|
| 9 | + """ |
| 10 | + @spec territory(board :: String.t(), position :: position) :: |
| 11 | + {:ok, owner} | {:error, String.t()} |
| 12 | + def territory(board, {x, y} = pos) do |
| 13 | + size_x = String.split(board, "\n") |> hd |> String.length() |
| 14 | + size_y = String.split(board, "\n", trim: true) |> length() |
| 15 | + |
| 16 | + if x < 0 or x >= size_x or y < 0 or y >= size_y do |
| 17 | + {:error, "Invalid coordinate"} |
| 18 | + else |
| 19 | + owner = |
| 20 | + board |
| 21 | + |> make_graph |
| 22 | + |> expand_territory([pos]) |
| 23 | + |> get_owner |
| 24 | + |
| 25 | + {:ok, owner} |
| 26 | + end |
| 27 | + end |
| 28 | + |
| 29 | + @doc """ |
| 30 | + Return all white, black and neutral territories |
| 31 | + """ |
| 32 | + @spec territories(board :: String.t()) :: territories |
| 33 | + def territories(board) do |
| 34 | + graph = make_graph(board) |
| 35 | + |
| 36 | + empties = for {pos, {:none, _neighbors}} <- graph, do: pos |
| 37 | + |
| 38 | + territories(graph, empties, %{white: [], black: [], none: []}) |
| 39 | + end |
| 40 | + |
| 41 | + def territories(_graph, [], territories), do: territories |
| 42 | + |
| 43 | + def territories(graph, [pos | positions], territories) do |
| 44 | + %{owner: owner, territory: territory} = |
| 45 | + graph |
| 46 | + |> expand_territory([pos]) |
| 47 | + |> get_owner |
| 48 | + |
| 49 | + positions = Enum.reject(positions, &(&1 in territory)) |
| 50 | + territories = %{territories | owner => Enum.sort(territories[owner] ++ territory)} |
| 51 | + |
| 52 | + territories(graph, positions, territories) |
| 53 | + end |
| 54 | + |
| 55 | + def to_color(?W), do: :white |
| 56 | + def to_color(?B), do: :black |
| 57 | + def to_color(?_), do: :none |
| 58 | + |
| 59 | + def make_graph(board) do |
| 60 | + board = |
| 61 | + board |
| 62 | + |> String.split("\n", trim: true) |
| 63 | + |> Enum.map(fn row -> row |> to_charlist |> Enum.map(&to_color/1) end) |
| 64 | + |
| 65 | + left_right_edges = |
| 66 | + for {[color], r} <- Enum.with_index(board) do |
| 67 | + # For rows with a single column we cannot use zip |
| 68 | + %{{0, r} => {color, []}} |
| 69 | + end ++ |
| 70 | + for {row, r} <- Enum.with_index(board), |
| 71 | + {{cell, right_cell}, c} <- Enum.zip(row, tl(row)) |> Enum.with_index() do |
| 72 | + # For rows with multiple columns, we zip |
| 73 | + %{{c, r} => {cell, [{c + 1, r}]}, {c + 1, r} => {right_cell, [{c, r}]}} |
| 74 | + end |
| 75 | + |
| 76 | + top_down_edges = |
| 77 | + case board do |
| 78 | + [row] -> |
| 79 | + for {color, c} <- Enum.with_index(row), do: %{{c, 0} => {color, []}} |
| 80 | + |
| 81 | + _ -> |
| 82 | + for {{row, row_below}, r} <- Enum.zip(board, tl(board)) |> Enum.with_index(), |
| 83 | + {{cell, below_cell}, c} <- Enum.zip(row, row_below) |> Enum.with_index() do |
| 84 | + %{{c, r} => {cell, [{c, r + 1}]}, {c, r + 1} => {below_cell, [{c, r}]}} |
| 85 | + end |
| 86 | + end |
| 87 | + |
| 88 | + Enum.reduce( |
| 89 | + left_right_edges ++ top_down_edges, |
| 90 | + %{}, |
| 91 | + &Map.merge(&1, &2, fn _key, {cell, n1}, {cell, n2} -> {cell, n1 ++ n2} end) |
| 92 | + ) |
| 93 | + end |
| 94 | + |
| 95 | + def expand_territory(graph, positions, visited \\ MapSet.new()) |
| 96 | + def expand_territory(_graph, [], _visited), do: [] |
| 97 | + |
| 98 | + def expand_territory(graph, [pos | positions], visited) do |
| 99 | + {color, neighbors} = |
| 100 | + case graph[pos] do |
| 101 | + {:white, _neighbors} -> |
| 102 | + {:white, []} |
| 103 | + |
| 104 | + {:black, _neighbors} -> |
| 105 | + {:black, []} |
| 106 | + |
| 107 | + {:none, neighbors} -> |
| 108 | + {:none, Enum.reject(neighbors, &(&1 in visited))} |
| 109 | + end |
| 110 | + |
| 111 | + [{pos, color} | expand_territory(graph, neighbors ++ positions, MapSet.put(visited, pos))] |
| 112 | + end |
| 113 | + |
| 114 | + def get_owner(territory) do |
| 115 | + empties = for {pos, :none} <- territory, do: pos |
| 116 | + colors = for {_pos, color} when color != :none <- territory, do: color |
| 117 | + |
| 118 | + %{ |
| 119 | + territory: Enum.sort(empties), |
| 120 | + owner: |
| 121 | + case {Enum.empty?(empties), Enum.uniq(colors)} do |
| 122 | + {false, [:white]} -> :white |
| 123 | + {false, [:black]} -> :black |
| 124 | + _ -> :none |
| 125 | + end |
| 126 | + } |
| 127 | + end |
| 128 | +end |
0 commit comments