-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit aaa0d3f
Showing
23 changed files
with
572 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 = "*" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.