Skip to content

Commit f05a1e3

Browse files
[#768] New practice exercise go-counting (#776)
Co-authored-by: Angelika Tyborska <[email protected]>
1 parent ff91466 commit f05a1e3

File tree

10 files changed

+488
-0
lines changed

10 files changed

+488
-0
lines changed

config.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2445,6 +2445,31 @@
24452445
],
24462446
"difficulty": 4
24472447
},
2448+
{
2449+
"slug": "go-counting",
2450+
"name": "Go Counting",
2451+
"uuid": "560347a2-9f20-4bad-b932-72a75ee08e14",
2452+
"prerequisites": [
2453+
"atoms",
2454+
"if",
2455+
"case",
2456+
"cond",
2457+
"strings",
2458+
"enum",
2459+
"default-arguments",
2460+
"guards",
2461+
"integers",
2462+
"list-comprehensions",
2463+
"maps",
2464+
"pattern-matching",
2465+
"pipe-operator",
2466+
"recursion"
2467+
],
2468+
"practices": [
2469+
"list-comprehensions"
2470+
],
2471+
"difficulty": 9
2472+
},
24482473
{
24492474
"slug": "yacht",
24502475
"name": "Yacht",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Description
2+
3+
Count the scored points on a Go board.
4+
5+
In the game of go (also known as baduk, igo, cờ vây and wéiqí) points
6+
are gained by completely encircling empty intersections with your
7+
stones. The encircled intersections of a player are known as its
8+
territory.
9+
10+
Write a function that determines the territory of each player. You may
11+
assume that any stones that have been stranded in enemy territory have
12+
already been taken off the board.
13+
14+
Write a function that determines the territory which includes a specified coordinate.
15+
16+
Multiple empty intersections may be encircled at once and for encircling
17+
only horizontal and vertical neighbours count. In the following diagram
18+
the stones which matter are marked "O" and the stones that don't are
19+
marked "I" (ignored). Empty spaces represent empty intersections.
20+
21+
```text
22+
+----+
23+
|IOOI|
24+
|O O|
25+
|O OI|
26+
|IOI |
27+
+----+
28+
```
29+
30+
To be more precise an empty intersection is part of a player's territory
31+
if all of its neighbours are either stones of that player or empty
32+
intersections that are part of that player's territory.
33+
34+
For more information see
35+
[wikipedia](https://en.wikipedia.org/wiki/Go_%28game%29) or [Sensei's
36+
Library](http://senseis.xmp.net/).
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"authors": ["jiegillet"],
3+
"contributors": [
4+
"angelikatyborska"
5+
],
6+
"files": {
7+
"example": [
8+
".meta/example.ex"
9+
],
10+
"solution": [
11+
"lib/go_counting.ex"
12+
],
13+
"test": [
14+
"test/go_counting_test.exs"
15+
]
16+
},
17+
"blurb": "Count the scored points on a Go board."
18+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
[94d0c01a-17d0-424c-aab5-2736d0da3939]
12+
description = "Black corner territory on 5x5 board"
13+
14+
[b33bec54-356a-485c-9c71-1142a9403213]
15+
description = "White center territory on 5x5 board"
16+
17+
[def7d124-422e-44ae-90e5-ceda09399bda]
18+
description = "Open corner territory on 5x5 board"
19+
20+
[57d79036-2618-47f4-aa87-56c06d362e3d]
21+
description = "A stone and not a territory on 5x5 board"
22+
23+
[0c84f852-e032-4762-9010-99f6a001da96]
24+
description = "Invalid because X is too low for 5x5 board"
25+
26+
[6f867945-9b2c-4bdd-b23e-b55fe2069a68]
27+
description = "Invalid because X is too high for 5x5 board"
28+
29+
[d67aaffd-fdf1-4e7f-b9e9-79897402b64a]
30+
description = "Invalid because Y is too low for 5x5 board"
31+
32+
[14f23c25-799e-4371-b3e5-777a2c30357a]
33+
description = "Invalid because Y is too high for 5x5 board"
34+
35+
[37fb04b5-98c1-4b96-8c16-af2d13624afd]
36+
description = "One territory is the whole board"
37+
38+
[9a1c59b7-234b-495a-8d60-638489f0fc0a]
39+
description = "Two territory rectangular board"
40+
41+
[d1645953-1cd5-4221-af6f-8164f96249e1]
42+
description = "Two region rectangular board"
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
@spec territory(board :: String.t(), position :: position) ::
10+
{:ok, owner} | {:error, String.t()}
11+
def territory(board, {x, y} = pos) do
12+
end
13+
14+
@doc """
15+
Return all white, black and neutral territories
16+
"""
17+
@spec territories(board :: String.t()) :: territories
18+
def territories(board) do
19+
end
20+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule GoCounting.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :go_counting,
7+
version: "0.1.0",
8+
# elixir: "~> 1.8",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
# {:dep_from_hexpm, "~> 0.3.0"},
25+
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
26+
]
27+
end
28+
end

0 commit comments

Comments
 (0)