Skip to content

Commit

Permalink
Bootstrap G engine
Browse files Browse the repository at this point in the history
  • Loading branch information
tychota committed Mar 4, 2018
0 parents commit aaa0d3f
Show file tree
Hide file tree
Showing 23 changed files with 572 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[[source]]

url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"


[packages]


[dev-packages]
six = "*"
39 changes: 39 additions & 0 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# G

G is a Go engine.

It is based on the Book https://www.manning.com/books/deep-learning-and-the-game-of-go and for now the code
mostly follow https://github.com/maxpumperla/deep_learning_and_the_game_of_go, licenced under MIT.

## Goal

The Goal of G is to learn more about how to implement a Go engine, like Alpha Go.

Performance is not the primary goal (if you want a good go engine go see https://github.com/gcp/leela-zero).
When I will estimate that I learned enough, I may rewrite the engine in Rust and use GPU optimized library for ML.

## Run it


```python
python3 ./play.py
```
Empty file added gagent/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions gagent/gagent_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from gboard.gstate import GState


class GAgent:
def select_move(self, game_state: GState):
raise NotImplementedError()
20 changes: 20 additions & 0 deletions gagent/gagent_naive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import random
from gagent.gagent_base import GAgent
from gagent.helper import is_point_an_eye
from gboard.gmove import GMove
from gboard.gstate import GState
from gtypes.gpoint import GPoint


class GAgentRandom(GAgent):
def select_move(self, game_state: GState):
candidates = []
for r in range(1, game_state.gboard.num_rows + 1):
for c in range(1, game_state.gboard.num_cols + 1):
candidate = GPoint(row=r, col=c)
if game_state.is_valid_move(GMove.play(candidate)) and \
not is_point_an_eye(game_state.gboard, candidate, game_state.next_gplayer):
candidates.append(candidate)
if not candidates:
return GMove.pass_turn()
return GMove.play(random.choice(candidates))
36 changes: 36 additions & 0 deletions gagent/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from gboard.gboard import GBoard
from gtypes.gplayer import GPlayer
from gtypes.gpoint import GPoint


def is_point_an_eye(gboard: GBoard, gpoint: GPoint, color: GPlayer):
# Eye is by definition an empty point
if gboard.get(gpoint) is not None:
return False
# It is not an eye where there is mixed stone
for neighbour in gpoint.neighbours():
if gboard.is_on_grid(neighbour):
neighbour_color = gboard.get(neighbour)
if neighbour_color != color:
return False
# We have to control at least 3 corners for the eye to be settled on the middle of the board
# On the edge, we must control every corner
friendly_corners = 0
off_board_corners = 0
corners = [
GPoint(gpoint.row - 1, gpoint.col - 1),
GPoint(gpoint.row - 1, gpoint.col + 1),
GPoint(gpoint.row + 1, gpoint.col - 1),
GPoint(gpoint.row + 1, gpoint.col + 1),
]
for corner in corners:
if gboard.is_on_grid(corner):
corner_color = gboard.get(corner)
if corner_color == color:
friendly_corners += 1
else:
off_board_corners += 1

if off_board_corners > 0:
return off_board_corners + friendly_corners == 4
return friendly_corners >= 3
Empty file added gboard/__init__.py
Empty file.
79 changes: 79 additions & 0 deletions gboard/gboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from gboard.gstring import GString


class GBoard:
def __init__(self, num_rows, num_cols):
self.num_rows = num_rows
self.num_cols = num_cols
self._grid = {}

def place_stone(self, gplayer, gpoint):
# safety nets
assert self.is_on_grid(gpoint) # prevents playing outside of the board: illegal move
assert self._grid.get(gpoint) is None # prevents playing on existing stone: illegal move

adjacent_same_color = []
adjacent_opposite_color = []
liberties = []

for neighbour in gpoint.neighbours():
# Do not explore the outside world: stay on board !
if not self.is_on_grid(neighbour):
continue
# Get the value of the neighbour gpoint
neighbour_gstring = self._grid.get(neighbour)
# If none, it is a liberty
if neighbour_gstring is None:
liberties.append(neighbour)
# Else, if it is is same color, it is part of our string
elif neighbour_gstring.color == gplayer:
if neighbour_gstring not in adjacent_same_color:
adjacent_same_color.append(neighbour_gstring)
# Else, if it is the enemy
else:
if neighbour_gstring not in adjacent_opposite_color:
adjacent_opposite_color.append(neighbour_gstring)

new_gstring = GString(gplayer, [gpoint], liberties)

# Merge adjacent string
for same_color_gstring in adjacent_same_color:
new_gstring = new_gstring.merged_with(same_color_gstring)
# And update the grid for it
for new_gstring_gpoint in new_gstring.stones:
self._grid[new_gstring_gpoint] = new_gstring
# Reduce liberties of adjacent opposite string
for other_color_gstring in adjacent_opposite_color:
other_color_gstring.remove_liberty(gpoint)
# And capture if the adjacent stones have no more liberties
for other_color_gstring in adjacent_opposite_color:
if other_color_gstring.num_liberties == 0:
self._remove_string(other_color_gstring)

def _remove_string(self, gstring):
"""Extracted method to remove string: not mean to be used directly"""
for gpoint in gstring.stones:
for neighbour in gpoint.neighbours():
neighbour_string = self._grid.get(neighbour)
if neighbour_string is None:
continue
# Removing string add liberties to neighbour string of opposite colour
if neighbour_string is not gstring:
neighbour_string.add_liberty(gpoint)
self._grid[gpoint] = None

def is_on_grid(self, gpoint):
return 1 <= gpoint.row <= self.num_rows and \
1 <= gpoint.col <= self.num_cols

def get(self, gpoint):
gstring = self._grid.get(gpoint)
if gstring is None:
return None
return gstring.color

def get_gstring(self, gpoint):
gstring = self._grid.get(gpoint)
if gstring is None:
return None
return gstring
29 changes: 29 additions & 0 deletions gboard/gmove.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from gtypes.gpoint import GPoint


class GMove:
"""A move represent the action of a player at some point.
It can either be playing by placing a stone on board or passing or resigning
"""
def __init__(self, gpoint: GPoint = None, is_pass: bool = False, is_resign: bool = False) -> None:
assert (gpoint is not None) ^ is_pass ^ is_resign # Safety net: ensure that move makes sense
self.gpoint = gpoint
self.is_play = (self.gpoint is not None)
self.is_pass = is_pass
self.is_resign = is_resign

@classmethod
def play(cls, gpoint):
"""Generate a play move: putting a stone on board"""
return GMove(gpoint=gpoint)

@classmethod
def pass_turn(cls):
"""Generate a pass move"""
return GMove(is_pass=True)

@classmethod
def resign(cls):
"""Generate a resign move"""
return GMove(is_resign=True)
85 changes: 85 additions & 0 deletions gboard/gstate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import copy

from typing import TypeVar, Optional

from gboard.gboard import GBoard
from gboard.gmove import GMove
from gtypes.gplayer import GPlayer

T = TypeVar('T', bound='GState')


class GState:
"""The Game State is a state machine that prevent wrong move"""
def __init__(self, gboard: GBoard, next_gplayer: GPlayer, previous_gstate: T, gmove: Optional[GMove]):
self.gboard = gboard
self.next_gplayer = next_gplayer
self.previous_gstate = previous_gstate
self.last_gmove = gmove

def apply_move(self, gmove: GMove):
"""Prevent a player playing twice"""
if gmove.is_play:
next_board = copy.deepcopy(self.gboard)
next_board.place_stone(self.next_gplayer, gmove.gpoint)
else:
next_board = self.gboard
return GState(next_board, self.next_gplayer.other, self, gmove)

@classmethod
def new_game(cls, board_size):
if isinstance(board_size, int):
board_size = (board_size, board_size)
board = GBoard(*board_size)
return GState(board, GPlayer.black, None, None)

def is_move_self_capture(self, gplayer: GPlayer, gmove: GMove):
"""Prevent self capture"""
if not gmove.is_play:
return False
next_gboard = copy.deepcopy(self.gboard)
next_gboard.place_stone(gplayer, gmove.gpoint)
new_string = next_gboard.get_gstring(gmove.gpoint)
return new_string.num_liberties == 0

@property
def situation(self):
return self.next_gplayer, self.gboard

def does_move_violate_ko(self, gplayer: GPlayer, gmove: GMove):
"""Test Ko rule"""
if not gmove.is_play:
return False

next_board = copy.deepcopy(self.gboard) # slow, need Zobrist hash
next_board.place_stone(gplayer, gmove.gpoint)
next_situation = (gplayer.other, next_board)

past_gstate = self.previous_gstate
while past_gstate is not None:
if past_gstate.situation == next_situation:
return True
past_gstate = past_gstate.previous_gstate
return False

def is_valid_move(self, gmove: GMove):
"""Return true if the move is valid"""
if self.is_over():
return False
if gmove.is_pass or gmove.is_resign:
return True
return (
self.gboard.get(gmove.gpoint) is None and
not self.is_move_self_capture(self.next_gplayer, gmove) and
not self.does_move_violate_ko(self.next_gplayer, gmove))

def is_over(self):
"""Handle the end of the game"""
if self.last_gmove is None:
return False
if self.last_gmove.is_resign:
return True
second_last_move = self.previous_gstate.last_gmove
if second_last_move is None:
return False
return self.last_gmove.is_pass and second_last_move.is_pass
Loading

0 comments on commit aaa0d3f

Please sign in to comment.