Permalink
Browse files

Move is_eyeish, is_legal to be Position instance methods

1 parent b7f514b commit 89783fdad500fb04ebfac89815b8fc27acb2ef14 @brilee committed Oct 30, 2016
Showing with 98 additions and 99 deletions.
  1. +34 −39 go.py
  2. +6 −11 strategies.py
  3. +25 −49 tests/test_go.py
  4. +33 −0 tests/test_strategies.py
View
@@ -88,35 +88,6 @@ def is_eyeish(board, c):
else:
return color
-def is_suicidal(position, move):
- potential_libs = set()
- for n in NEIGHBORS[move]:
- neighbor_group_id = position.lib_tracker.group_index[n]
- if neighbor_group_id == MISSING_GROUP_ID:
- # at least one liberty after playing here, so not a suicide
- return False
- neighbor_group = position.lib_tracker.groups[neighbor_group_id]
- if neighbor_group.color == position.to_play:
- potential_libs |= neighbor_group.liberties
- elif len(neighbor_group.liberties) == 1:
- # would capture an opponent group if they only had one lib.
- return False
- # it's possible to suicide by connecting several friendly groups
- # each of which had one liberty.
- potential_libs -= set([move])
- return not potential_libs
-
-
-def is_reasonable(position, move):
- 'Checks that a move is both legal and not self-eye filling'
- if is_eyeish(position.board, move) == position.to_play:
- return False
- try:
- position.play_move(position.to_play, move)
- return True
- except IllegalMove:
- return False
-
class Group(namedtuple('Group', ['id', 'stones', 'liberties', 'color'])):
'''
stones: a set of Coordinates belonging to this group
@@ -313,6 +284,35 @@ def __str__(self):
details = "\nMove: {}. Captures X: {} O: {}\n".format(self.n, *captures)
return annotated_board + details
+ def is_move_suicidal(self, move):
+ potential_libs = set()
+ for n in NEIGHBORS[move]:
+ neighbor_group_id = self.lib_tracker.group_index[n]
+ if neighbor_group_id == MISSING_GROUP_ID:
+ # at least one liberty after playing here, so not a suicide
+ return False
+ neighbor_group = self.lib_tracker.groups[neighbor_group_id]
+ if neighbor_group.color == self.to_play:
+ potential_libs |= neighbor_group.liberties
+ elif len(neighbor_group.liberties) == 1:
+ # would capture an opponent group if they only had one lib.
+ return False
+ # it's possible to suicide by connecting several friendly groups
+ # each of which had one liberty.
+ potential_libs -= set([move])
+ return not potential_libs
+
+ def is_move_legal(self, move):
+ 'Checks that a move is on an empty space, not on ko, and not suicide'
+ if self.board[move] != EMPTY:
+ return False
+ if move == self.ko:
+ return False
+ if self.is_move_suicidal(move):
+ return False
+
+ return True
+
def pass_move(self, mutate=False):
pos = self if mutate else copy.deepcopy(self)
pos.n += 1
@@ -336,19 +336,14 @@ def play_move(self, color, c, mutate=False):
# Chinese/area scoring
# Positional superko (this is very crudely approximate at the moment.)
- # Checking a move for legality is actually very expensive, because
- # the only way to reliably handle all suicide/capture situations is to
- # actually play the move and see if any issues arise.
- # Thus, there is no "is_legal(self, move)" or "get_legal_moves(self)".
- # You can only play the move and check if the return value is None.
pos = self if mutate else copy.deepcopy(self)
+
+ if not self.is_move_legal(c):
+ raise IllegalMove()
+
if c is None:
- pos.pass_move(mutate=mutate)
+ pos = pos.pass_move(mutate=mutate)
return pos
- if c == pos.ko:
- raise IllegalMove
- if pos.board[c] != EMPTY:
- raise IllegalMove
place_stones(pos.board, color, [c])
captured_stones = pos.lib_tracker.add_stone(color, c)
View
@@ -22,6 +22,9 @@ def translate_gtp_colors(gtp_color):
else:
return go.EMPTY
+def is_move_reasonable(position, move):
+ return position.is_move_legal(move) and go.is_eyeish(position.board, move) != position.to_play
+
class GtpInterface(object):
def __init__(self):
self.size = 9
@@ -61,14 +64,11 @@ def suggest_move(self, position):
class RandomPlayer(GtpInterface):
def suggest_move(self, position):
- possible_moves = go.ALL_COORDS
+ possible_moves = go.ALL_COORDS[:]
random.shuffle(possible_moves)
for move in possible_moves:
- try:
- position.play_move(position.to_play, move)
+ if is_move_reasonable(position, move):
return move
- except go.IllegalMove:
- pass
return None
class PolicyNetworkBestMovePlayer(GtpInterface):
@@ -92,13 +92,8 @@ def suggest_move(self, position):
return None
move_probabilities = self.policy_network.run(position)
for move in sorted_moves(move_probabilities):
- if go.is_eyeish(position.board, move):
- continue
- try:
- position.play_move(position.to_play, move)
+ if go.is_reasonable(position, move):
return move
- except go.IllegalMove:
- pass
return None
# Exploration constant
View
@@ -67,55 +67,6 @@ def test_is_eyeish(self):
for ne in not_eyes:
self.assertEqual(go.is_eyeish(board, ne), None, str(ne))
- def test_is_reasonable(self):
- board = load_board('''
- .XXOOOXXX
- X.XO.OX.X
- XXXOOOXX.
- ...XXX..X
- XXXX.....
- OOOX....O
- X.OXX.OO.
- .XO.X.O.O
- XXO.X.OO.
- ''')
- position = Position(
- board=board,
- to_play=BLACK,
- )
- reasonable_moves = pc_set('E8 B3')
- unreasonable_moves = pc_set('A9 B8 H8 J7 A2 J3 H2 J1')
- for move in reasonable_moves:
- self.assertTrue(go.is_reasonable(position, move), str(move))
- for move in unreasonable_moves:
- self.assertFalse(go.is_reasonable(position, move), str(move))
-
- def test_is_suicidal(self):
- board = load_board('''
- ...O.O...
- ....O....
- XO.....O.
- OXO...OXO
- O.XO.OX.O
- OXO...OOX
- XO.......
- ......XXO
- .....XOO.
- ''')
- position = Position(
- board=board,
- to_play=BLACK,
- )
- suicidal_moves = pc_set('E9 H5')
- nonsuicidal_moves = pc_set('B5 J1 A9')
- for move in suicidal_moves:
- assert(position.board[move] == go.EMPTY) #sanity check my coordinate input
- self.assertTrue(go.is_suicidal(position, move), str(move))
- for move in nonsuicidal_moves:
- assert(position.board[move] == go.EMPTY) #sanity check my coordinate input
- self.assertFalse(go.is_suicidal(position, move), str(move))
-
-
class TestLibertyTracker(unittest.TestCase):
def test_lib_tracker_init(self):
board = load_board('X........' + EMPTY_ROW * 8)
@@ -336,6 +287,31 @@ def test_flipturn(self):
flip_position = start_position.flip_playerturn()
self.assertEqualPositions(flip_position, expected_position)
+ def test_is_move_suicidal(self):
+ board = load_board('''
+ ...O.O...
+ ....O....
+ XO.....O.
+ OXO...OXO
+ O.XO.OX.O
+ OXO...OOX
+ XO.......
+ ......XXO
+ .....XOO.
+ ''')
+ position = Position(
+ board=board,
+ to_play=BLACK,
+ )
+ suicidal_moves = pc_set('E9 H5')
+ nonsuicidal_moves = pc_set('B5 J1 A9')
+ for move in suicidal_moves:
+ assert(position.board[move] == go.EMPTY) #sanity check my coordinate input
+ self.assertTrue(position.is_move_suicidal(move), str(move))
+ for move in nonsuicidal_moves:
+ assert(position.board[move] == go.EMPTY) #sanity check my coordinate input
+ self.assertFalse(position.is_move_suicidal(move), str(move))
+
def test_legal_moves(self):
board = load_board('''
.XXXXXXXO
@@ -0,0 +1,33 @@
+import unittest
+from go import Position, BLACK
+from strategies import is_move_reasonable
+from test_utils import load_board
+from utils import parse_kgs_coords as pc
+
+def pc_set(string):
+ return set(map(pc, string.split()))
+
+class TestHelperFunctions(unittest.TestCase):
+ def test_is_move_reasonable(self):
+ board = load_board('''
+ .XXOOOXXX
+ X.XO.OX.X
+ XXXOOOXX.
+ ...XXX..X
+ XXXX.....
+ OOOX....O
+ X.OXX.OO.
+ .XO.X.O.O
+ XXO.X.OO.
+ ''')
+ position = Position(
+ board=board,
+ to_play=BLACK,
+ )
+ reasonable_moves = pc_set('E8 B3')
+ unreasonable_moves = pc_set('A9 B8 H8 J7 A2 J3 H2 J1')
+ for move in reasonable_moves:
+ self.assertTrue(is_move_reasonable(position, move), str(move))
+ for move in unreasonable_moves:
+ self.assertFalse(is_move_reasonable(position, move), str(move))
+

0 comments on commit 89783fd

Please sign in to comment.