This is a work in progress. I am by no means an expert in either Go or Python, so don’t be too harsh on me if you find some mistakes. But you can point out anything in the comment section.
First of all, If you’re really unfamiliar with even the name, this post may not be for you. You can take a look at the Wikipedia page to get some basic ideas.
Some of you may have heard of it from the famous match between AlphaGo and Lee Sedol. And you may be wondering if this game would be hard to implement in Python. Nah, I guess the hardest part is only about the AI, that is, how to decide which move to go, among the many valid moves. But we are not going that far. A basic implementation of the game is quite a standard practice in Python.
Let’s start!
I have no plan to implement all of the game today. At this moment let’s quickly go thorough several ideas playing a center role in the logic of the game.
Assuming the board is $n\times n$, where n is a positive integer, for example $n=9, 13, 19.$ Now let’s denote the set $\{1,\dots,9\}$ by $N$.
A state or configuration is, mathematically speaking, a mapping $\phi$: $N\times N$ to $\{-1, 0, 1\}$, where $-1$ means black, $1$ means white, and $0$ means no stones. Note that is just a definition, and not all such states are legal. Therefore:
A valid state is only proper subset of all states. The rules are too detailed as it’s about the different rule sets of the game (Chinese rules, Japanese rules…).
so at this moment let’s just list some most important ones:
Stones which are neighbors and of the same color are called clusters (that is, a connected component, to put it mathematically). When counting liberties, stones in the same cluster are regarded as sharing the same liberties.
A cluster having no liberties is called dead, and they cannot appear in a valid state.
Therefore, the liberty of a given single stone is defined to be the liberty of the cluster it belongs to.
Now let’s translate the 3 things listed to programming language. We need to:
Define a function to tell if two stones are neighbors (regardless of color). This is very easy.
Input: a stone, Output: the cluster it belongs to. This is not easy. I believe it should has some important algorithm (I used Breadth-first search for this attempt, but I have no idea on if it’s the best for our purpose, nor, if there exist some pre-build libraries for doing this) behind it. Naively, we may use some for-loops to do it but it can be very non-efficient. This would be a key part of the main program.
Then: given a state, we need to find all clusters. This is based on the previous item.
Given a cluster, we need to count its liberties. This is less difficult than 2.
Today, let’s try a little bit. First of all we need to denote a state. The most math way of denoting a state should be a matrix of $n\times n$ whose entries are $-1, 0, 1$. But in Python, we can use a list of lists to denote it, thanks to the numpy library.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
importnumpyasnp# first let's look at an example of a 9x9 state.# we can represent the board as a 9x9 numpy arraystate=np.array([[0,0,0,0,0,0,0,0,-1],[0,1,1,0,0,-1,-1,-1,0],[0,0,1,-1,-1,-1,-1,0,0],[0,0,0,1,-1,-1,0,0,0],[0,0,1,1,-1,0,0,0,0],[1,1,1,1,0,-1,0,0,0],[0,0,1,0,0,-1,1,0,0],[0,1,1,0,0,0,0,1,0],[-1,0,0,0,0,0,0,0,-1]])print(state)print(state[(0,0)])print(state[(0,8)])print(state[(2,2)])
To precisely implement a state would be out of today’s range. Now we only need to remember this kind of structure and write a function to tell if two coordinates are neighbors. Input: $x=(x_1,x_2)$ and $y=(y_1,y_2)$, output: True or False.
First we need to check if a coordinate $(x_1,x_2)$ is valid. Two things to check:
Now we do is_neighbor. We say two valid coordinates are neighbors if they are adjacent in the horizontal or vertical direction, that is, if $|x_1-y_1|+|x_2-y_2|=1$. (Note that one stone has no way to neighbor itself.)
Then we define a function to tell if two stones of a state is of the same color. Input: a state and two coordinates, output: True or False.
1
2
3
4
5
6
7
8
9
10
defis_same_color(coord_x,coord_y,state):ifnotis_valid_coordinate(coord_x)ornotis_valid_coordinate(coord_y):raiseValueError("Invalid coordinates.")# note that two coordinates should be differentifcoord_x==coord_y:raiseValueError("Two coordinates should be different.")# note that 'no stone' is not a colorifstate[coord_x]==0orstate[coord_y]==0:raiseValueError("No stone at the given coordinates.")returnstate[coord_x]==state[coord_y]
defis_valid_coordinates(coords,state):ifnotisinstance(coords,set):raiseValueError("Input should be a set of coordinates.")returnFalseiflen(coords)==0:raiseValueError("Input should not be empty.")returnFalseforcoordincoords:ifnotis_valid_coordinate(coord):raiseValueError("Invalid coordinates.")returnFalsereturnTrue# the difference here is, the next function checks if all stones belong to the same player, while the previous function only checks if all stones are legal on the board, regardless of color.defis_valid_one_color_coordinates(coords,state):ifnotisinstance(coords,set):raiseValueError("Input should be a set of coordinates.")returnFalseiflen(coords)==0:raiseValueError("Input should not be empty.")returnFalsecolor=state[next(iter(coords))]ifcolor==0:raiseValueError("No stone at some given coordinates.")returnFalseforcoordincoords:ifstate[coord]!=color:raiseValueError("All stones should have the same color.")returnFalseifnotis_valid_coordinate(coord):raiseValueError("Invalid coordinates.")returnFalsereturnTrue
Neighbors: frontiers, enemy frontiers and empty neighbors#
For each stone on the board, define its frontier as the all coordinates which shares the same color and is a neighbor of the stone.
To do this, it’s convenient for us to define a more general function.
Define find_neighbors, where neighbors are all coordinates which are valid, regardless of the color.
Among the neighbors, filter the coordinates with the same color as the given coordinate. Then according to different color, we can find find_frontiers (which are the neighbors with the same color as the given coordinate), find_enemy_frontiers (which are the neighbors with the different color as the given coordinate), and find_empty_neighbors (which are the neighbors with no stone).
# testprint(find_neighbors({(0,0)},state))# test for a set of coordinates of many isolated stonesprint(find_neighbors({(0,0),(1,1),(8,8)},state))print(find_neighbors({(0,0),(1,1)},state))
find_frontiers is nothing but the coordinates with the same color as the given coordinates in the neighbors. Only thing you need to check is, if the input set is of the same color or not.
When we say cluster, we means one maximal path-connected component. That is, a cluster is
Whose size cannot be further increased: if you try to find frontier of a cluster, than you can find nothing.
Whose stones are closely connected: that is, from any stone A and stone B inside a cluster, there is always a path connecting A to B.
This gives us an idea to find a cluster given a starting point. You try to find frontiers of the starting point, visit it, then set the visited point as the new starting point, keeping doing this until you have no where to go: that is, no unvisited frontiers for you to go. This is the time to exit the process.
What described is in fact, the Breadth-first search. Let’s do this.
deffind_cluster(coord,state):ifnotis_valid_coordinate(coord):raiseValueError("Invalid coordinate.")ifstate[coord]==0:raiseValueError("the starting coordinate has no stone.")visited=set()visitable=find_frontiers({coord},state)whilelen(visitable)>0:current=visitablevisited.update(current)frontiers=find_frontiers(current,state)visitable=frontiers-visited# lastly, the cluster should include the starting pointsvisited.update({coord})returnvisited
So what’s the difference from empty-neighbor to liberty? Well, when the set we input is a cluster, that is, a connected component of stones, the empty-neighbor is the liberty of the cluster.
If we start from any set of stone of the same color, to get its liberties,
find_cluster;
find_empty_neighbors;
count the number of empty neighbors.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
deffind_liberties(coord,state):ifnotis_valid_coordinate(coord):raiseValueError("Invalid coordinate.")ifstate[coord]==0:raiseValueError("the starting coordinate has no stone.")color=state[coord]# 1. find the clustercluster=find_cluster(coord,state)# 2. find the empty neighbors of the clusteremtpy_neighbors=find_empty_neighbors(cluster,state)# 3. count the number of empty neighborsreturnlen(emtpy_neighbors)