This repository has been archived by the owner on Apr 11, 2023. It is now read-only.
/
state.ex
244 lines (205 loc) · 7.89 KB
/
state.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
defmodule Hayago.State do
@moduledoc """
A struct to describe the current state in the game, and functions to update
the state by placing stones and to check if a certain move is legal.
## Attributes
### Positions
The *positions* attribute is a list of positions on the board. Initially,
it's generated as a list of 81 `nil` values, which represent all positions on
an empty 9 × 9 board. When a stone is added on one of the positions, the
value corresponding to that position gets updated to either `:black`, or
`:white`.
### Current
The *current* attribute holds the current player's color, and switches to the
other color after every successful move. The player with the black stones
always starts, so the initial value is `:black`.
### Captures
A stone is captured when it has no more liberties, meaning it's surrounded by
the opponent's stones. A captured stone is removed from the board, and the
*captures* list is incremented for the captured stone's color.
"""
alias Hayago.State
defstruct positions: Enum.map(1..81, fn _ -> nil end),
current: :black,
captures: %{black: 0, white: 0}
@doc """
Places a new stone on the board, captures any surrounded stones, and swaps
the current attribute to switch the turn to the other player.
`place/2` takes a `Hayago.State` struct and an *index* to place a new stone.
When called, it replaces the state's positions list by replacing the value at
the passed index with the current value in the state.
After placing the stone on the board, the current player is swapped, to
prepare the state for the other player's move.
iex> State.place(%State{positions: [nil, nil, nil, nil], current: :black}, 0)
%State{positions: [:black, nil, nil, nil], current: :white}
Stones are captured if they're surrounded by the opponent's stones. After
placing a new stone, all stones on the board are checked to see if they have
any liberties left. If they don't they're removed from the board.
> A *liberty* is an empty position adjacent to a stone. If a stone is
surrounded by the opponent's stones, it has no liberties left. If two stones
of the same color are in adjacent positions, they form a group and share
their liberties.
After removing a stone from the board, `place/2` increments the key
corresponding to the captured stone in the captures counter.
iex> State.place(
...> %State{
...> positions: [
...> :white, :black, nil,
...> nil, nil, :white,
...> nil, nil, nil
...> ],
...> current: :black
...> },
...> 3)
%State{
positions: [
nil, :black, nil,
:black, nil, :white,
nil, nil, nil
],
current: :white,
captures: %{black: 0, white: 1}
}
When placing a stone `place/2` iterates over the opponents' stones to check
for captures first. After that, it checks all of the current player's stones.
Moves aren't validated in the `place/2` function. This means a placing a
stone on a position without liberties will immediately remove it from the
board.
iex> State.place(
...> %State{
...> positions: [
...> nil, :black, nil,
...> :black, nil, :white,
...> nil, nil, nil
...> ],
...> current: :white
...> },
...> 0)
%State{
positions: [
nil, :black, nil,
:black, nil, :white,
nil, nil, nil
],
current: :white,
captures: %{black: 0, white: 0}
}
Making a move that captures one of your own stones is illegal in Go. The
`legal?/2` function, which validates moves before they happen, uses the
`place/2` function to check if making a move actually results in a stone in
the corect position. If it doesn't, the move is illegal.
"""
def place(%State{positions: positions, current: current, captures: captures} = state, index) do
opponent = next(current)
new_positions = List.replace_at(positions, index, current)
{new_positions, fresh_captures} = capture(new_positions, opponent)
{new_positions, _} = capture(new_positions, current)
{_, new_captures} =
Map.get_and_update(captures, opponent, fn current ->
{current, current + fresh_captures}
end)
new_current =
case new_positions do
^positions -> current
_ -> opponent
end
%{state | positions: new_positions, current: new_current, captures: new_captures}
end
@doc """
Validates a potential move by trying it on the current state, and evaluating
the result.
The `legal?/2` function does two checks to make sure a move is legal. First,
it checks if the position in the current state is empty, making sure there
position a new stone is placed on is empty.
iex> State.legal?(%State{positions: [nil, nil, nil, nil]}, 0)
true
iex> State.legal?(%State{positions: [:black, nil, nil, nil]}, 0)
false
The next check makes sure the position has liberties for the placed stone,
meaning the stone can be placed there with at least one liberty, or part of a
group that has at least one liberty.
To determine if a newly placed stone will have liberties, `legal?/2` uses the
`place/2` function to try placing a stone on the position that's being
validated. Since placing a stone will remove any stones without liberties
from the board, it checks if the stone is still there after placing it. If it
is, the move is legal.
iex> State.legal?(%State{positions: [nil, :black, :black, nil], current: :black}, 0)
true
iex> State.legal?(%State{positions: [nil, :white, :white, nil], current: :black}, 0)
false
Because the `place/2` function captures enemy stones first, moves on places
that don't have any liberties but gain them by capturing the opponent's
stones are legal as well.
iex> State.legal?(
...> %State{
...> positions: [
...> nil, :black, :white,
...> :black, :white, nil,
...> :white, nil, nil
...> ],
...> current: :white
...> },
...> 0)
true
iex> State.legal?(
...> %State{
...> positions: [
...> nil, :black, :white,
...> :black, :white, nil,
...> :white, nil, nil
...> ],
...> current: :black
...> },
...> 0)
false
"""
def legal?(%State{positions: positions, current: current} = state, index) do
%State{positions: tentative_positions} = State.place(state, index)
Enum.at(positions, index) == nil and Enum.at(tentative_positions, index) == current
end
defp capture(positions, color) do
positions
|> Enum.with_index()
|> Enum.map_reduce(0, fn {value, index}, captures ->
case {value, liberties?(positions, index, color)} do
{^color, false} -> {nil, captures + 1}
{_, _} -> {value, captures}
end
end)
end
defp liberties?(positions, index, color, checked \\ []) do
size =
positions
|> length()
|> :math.sqrt()
|> round()
index
|> liberty_indexes(size)
|> Enum.reject(&(&1 in checked))
|> Enum.any?(fn liberty ->
case Enum.at(positions, liberty) do
^color -> liberties?(positions, liberty, color, [index | checked])
nil -> true
_ -> false
end
end)
end
defp liberty_indexes(index, size) do
row = div(index, size)
column = rem(index, size)
[
{row - 1, column},
{row, column + 1},
{row + 1, column},
{row, column - 1}
]
|> Enum.reduce([], fn {row, column}, acc ->
case row < 0 or row >= size or column < 0 or column >= size do
true -> acc
false -> [row * size + column | acc]
end
end)
end
defp next(:black), do: :white
defp next(:white), do: :black
end