update directory structure
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import copy
|
||||
from game_logic.gotypes import Player, Point
|
||||
from game_logic.scoring import compute_game_result
|
||||
from game_logic import zobrist_hash
|
||||
|
||||
|
||||
class IllegalMoveError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# tag::fast_go_strings[]
|
||||
class GoString:
|
||||
def __init__(self, color, stones, liberties):
|
||||
self.color = color
|
||||
self.stones = frozenset(stones)
|
||||
self.liberties = frozenset(liberties) # <1>
|
||||
|
||||
def without_liberty(self, point): # <2>
|
||||
new_liberties = self.liberties - set([point])
|
||||
return GoString(self.color, self.stones, new_liberties)
|
||||
|
||||
def with_liberty(self, point):
|
||||
new_liberties = self.liberties | set([point])
|
||||
return GoString(self.color, self.stones, new_liberties)
|
||||
# <1> `stones` and `liberties` are now immutable `frozenset` instances
|
||||
# <2> The `without_liberty` methods replaces the previous `remove_liberty` method...
|
||||
# <3> ... and `with_liberty` replaces `add_liberty`.
|
||||
# end::fast_go_strings[]
|
||||
|
||||
def merged_with(self, string):
|
||||
"""Return a new string containing all stones in both strings."""
|
||||
assert string.color == self.color
|
||||
combined_stones = self.stones | string.stones
|
||||
return GoString(
|
||||
self.color,
|
||||
combined_stones,
|
||||
(self.liberties | string.liberties) - combined_stones)
|
||||
|
||||
@property
|
||||
def num_liberties(self):
|
||||
return len(self.liberties)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, GoString) and \
|
||||
self.color == other.color and \
|
||||
self.stones == other.stones and \
|
||||
self.liberties == other.liberties
|
||||
|
||||
def __deepcopy__(self, memodict={}):
|
||||
return GoString(self.color, self.stones, copy.deepcopy(self.liberties))
|
||||
|
||||
|
||||
# tag::init_zobrist[]
|
||||
class Board:
|
||||
def __init__(self, num_rows, num_cols):
|
||||
self.num_rows = num_rows
|
||||
self.num_cols = num_cols
|
||||
self._grid = {}
|
||||
self._hash = zobrist_hash.EMPTY_BOARD
|
||||
# end::init_zobrist[]
|
||||
|
||||
def place_stone(self, player, point):
|
||||
assert self.is_on_grid(point)
|
||||
if self._grid.get(point) is not None:
|
||||
print('Illegal play on %s' % str(point))
|
||||
assert self._grid.get(point) is None
|
||||
# 0. Examine the adjacent points.
|
||||
adjacent_same_color = []
|
||||
adjacent_opposite_color = []
|
||||
liberties = []
|
||||
for neighbor in point.neighbors():
|
||||
if not self.is_on_grid(neighbor):
|
||||
continue
|
||||
neighbor_string = self._grid.get(neighbor)
|
||||
if neighbor_string is None:
|
||||
liberties.append(neighbor)
|
||||
elif neighbor_string.color == player:
|
||||
if neighbor_string not in adjacent_same_color:
|
||||
adjacent_same_color.append(neighbor_string)
|
||||
else:
|
||||
if neighbor_string not in adjacent_opposite_color:
|
||||
adjacent_opposite_color.append(neighbor_string)
|
||||
new_string = GoString(player, [point], liberties)
|
||||
# tag::apply_zobrist[]
|
||||
new_string = GoString(player, [point], liberties) # <1>
|
||||
|
||||
for same_color_string in adjacent_same_color: # <2>
|
||||
new_string = new_string.merged_with(same_color_string)
|
||||
for new_string_point in new_string.stones:
|
||||
self._grid[new_string_point] = new_string
|
||||
|
||||
self._hash ^= zobrist_hash.HASH_CODE[point, player] # <3>
|
||||
|
||||
for other_color_string in adjacent_opposite_color:
|
||||
replacement = other_color_string.without_liberty(point) # <4>
|
||||
if replacement.num_liberties:
|
||||
self._replace_string(other_color_string.without_liberty(point))
|
||||
else:
|
||||
self._remove_string(other_color_string) # <5>
|
||||
# <1> Until this line `place_stone` remains the same.
|
||||
# <2> You merge any adjacent strings of the same color.
|
||||
# <3> Next, you apply the hash code for this point and player
|
||||
# <4> Then you reduce liberties of any adjacent strings of the opposite color.
|
||||
# <5> If any opposite color strings now have zero liberties, remove them.
|
||||
# end::apply_zobrist[]
|
||||
|
||||
|
||||
# tag::unapply_zobrist[]
|
||||
def _replace_string(self, new_string): # <1>
|
||||
for point in new_string.stones:
|
||||
self._grid[point] = new_string
|
||||
|
||||
def _remove_string(self, string):
|
||||
for point in string.stones:
|
||||
for neighbor in point.neighbors(): # <2>
|
||||
neighbor_string = self._grid.get(neighbor)
|
||||
if neighbor_string is None:
|
||||
continue
|
||||
if neighbor_string is not string:
|
||||
self._replace_string(neighbor_string.with_liberty(point))
|
||||
self._grid[point] = None
|
||||
|
||||
self._hash ^= zobrist_hash.HASH_CODE[point, string.color] # <3>
|
||||
# <1> This new helper method updates our Go board grid.
|
||||
# <2> Removing a string can create liberties for other strings.
|
||||
# <3> With Zobrist hashing, you need to unapply the hash for this move.
|
||||
# end::unapply_zobrist[]
|
||||
|
||||
def is_on_grid(self, point):
|
||||
return 1 <= point.row <= self.num_rows and \
|
||||
1 <= point.col <= self.num_cols
|
||||
|
||||
def get(self, point):
|
||||
"""Return the content of a point on the board.
|
||||
|
||||
Returns None if the point is empty, or a Player if there is a
|
||||
stone on that point.
|
||||
"""
|
||||
string = self._grid.get(point)
|
||||
if string is None:
|
||||
return None
|
||||
return string.color
|
||||
|
||||
def get_go_string(self, point):
|
||||
"""Return the entire string of stones at a point.
|
||||
|
||||
Returns None if the point is empty, or a GoString if there is
|
||||
a stone on that point.
|
||||
"""
|
||||
string = self._grid.get(point)
|
||||
if string is None:
|
||||
return None
|
||||
return string
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Board) and \
|
||||
self.num_rows == other.num_rows and \
|
||||
self.num_cols == other.num_cols and \
|
||||
self._hash() == other._hash()
|
||||
|
||||
def __deepcopy__(self, memodict={}):
|
||||
copied = Board(self.num_rows, self.num_cols)
|
||||
# Can do a shallow copy b/c the dictionary maps tuples
|
||||
# (immutable) to GoStrings (also immutable)
|
||||
copied._grid = copy.copy(self._grid)
|
||||
copied._hash = self._hash
|
||||
return copied
|
||||
|
||||
# tag::return_zobrist[]
|
||||
def zobrist_hash(self):
|
||||
return self._hash
|
||||
# end::return_zobrist[]
|
||||
|
||||
|
||||
class Move:
|
||||
"""Any action a player can play on a turn.
|
||||
Exactly one of is_play, is_pass, is_resign will be set.
|
||||
"""
|
||||
def __init__(self, point=None, is_pass=False, is_resign=False):
|
||||
assert (point is not None) ^ is_pass ^ is_resign
|
||||
self.point = point
|
||||
self.is_play = (self.point is not None)
|
||||
self.is_pass = is_pass
|
||||
self.is_resign = is_resign
|
||||
|
||||
@classmethod
|
||||
def play(cls, point):
|
||||
"""A move that places a stone on the board."""
|
||||
return Move(point=point)
|
||||
|
||||
@classmethod
|
||||
def pass_turn(cls):
|
||||
return Move(is_pass=True)
|
||||
|
||||
@classmethod
|
||||
def resign(cls):
|
||||
return Move(is_resign=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.is_pass:
|
||||
return 'pass'
|
||||
if self.is_resign:
|
||||
return 'resign'
|
||||
return '(r %d, c %d)' % (self.point.row, self.point.col)
|
||||
|
||||
|
||||
# tag::init_state_zobrist[]
|
||||
class GameState:
|
||||
def __init__(self, board, next_player, previous, move):
|
||||
self.board = board
|
||||
self.next_player = next_player
|
||||
self.previous_state = previous
|
||||
if self.previous_state is None:
|
||||
self.previous_states = frozenset()
|
||||
else:
|
||||
self.previous_states = frozenset(
|
||||
previous.previous_states |
|
||||
{(previous.next_player, previous.board.zobrist_hash())})
|
||||
self.last_move = move
|
||||
# end::init_state_zobrist[]
|
||||
|
||||
def apply_move(self, move):
|
||||
"""Return the new GameState after applying the move."""
|
||||
if move.is_play:
|
||||
next_board = copy.deepcopy(self.board)
|
||||
next_board.place_stone(self.next_player, move.point)
|
||||
else:
|
||||
next_board = self.board
|
||||
return GameState(next_board, self.next_player.other, self, move)
|
||||
|
||||
@classmethod
|
||||
def new_game(cls, board_size):
|
||||
if isinstance(board_size, int):
|
||||
board_size = (board_size, board_size)
|
||||
board = Board(*board_size)
|
||||
return GameState(board, Player.black, None, None)
|
||||
|
||||
def is_move_self_capture(self, player, move):
|
||||
if not move.is_play:
|
||||
return False
|
||||
next_board = copy.deepcopy(self.board)
|
||||
next_board.place_stone(player, move.point)
|
||||
new_string = next_board.get_go_string(move.point)
|
||||
return new_string.num_liberties == 0
|
||||
|
||||
@property
|
||||
def situation(self):
|
||||
return (self.next_player, self.board)
|
||||
|
||||
# tag::ko_zobrist[]
|
||||
def does_move_violate_ko(self, player, move):
|
||||
if not move.is_play:
|
||||
return False
|
||||
next_board = copy.deepcopy(self.board)
|
||||
next_board.place_stone(player, move.point)
|
||||
next_situation = (player.other, next_board.zobrist_hash())
|
||||
return next_situation in self.previous_states
|
||||
# end::ko_zobrist[]
|
||||
|
||||
def is_valid_move(self, move):
|
||||
if self.is_over():
|
||||
return False
|
||||
if move.is_pass or move.is_resign:
|
||||
return True
|
||||
return (
|
||||
self.board.get(move.point) is None and
|
||||
not self.is_move_self_capture(self.next_player, move) and
|
||||
not self.does_move_violate_ko(self.next_player, move))
|
||||
|
||||
def is_over(self):
|
||||
if self.last_move is None:
|
||||
return False
|
||||
if self.last_move.is_resign:
|
||||
return True
|
||||
second_last_move = self.previous_state.last_move
|
||||
if second_last_move is None:
|
||||
return False
|
||||
return self.last_move.is_pass and second_last_move.is_pass
|
||||
|
||||
def legal_moves(self):
|
||||
moves = []
|
||||
for row in range(1, self.board.num_rows + 1):
|
||||
for col in range(1, self.board.num_cols + 1):
|
||||
move = Move.play(Point(row, col))
|
||||
if self.is_valid_move(move):
|
||||
moves.append(move)
|
||||
# These two moves are always legal.
|
||||
moves.append(Move.pass_turn())
|
||||
moves.append(Move.resign())
|
||||
|
||||
return moves
|
||||
|
||||
def winner(self):
|
||||
if not self.is_over():
|
||||
return None
|
||||
if self.last_move.is_resign:
|
||||
return self.next_player
|
||||
game_result = compute_game_result(self)
|
||||
return game_result.winner
|
||||
@@ -0,0 +1,394 @@
|
||||
import copy
|
||||
from game_logic.gotypes import Player, Point
|
||||
from game_logic.scoring import compute_game_result
|
||||
from game_logic import zobrist_hash
|
||||
from tugo.print_utils import MoveAge
|
||||
|
||||
neighbor_tables = {}
|
||||
corner_tables = {}
|
||||
|
||||
|
||||
def init_neighbor_table(dim):
|
||||
rows, cols = dim
|
||||
new_table = {}
|
||||
for r in range(1, rows + 1):
|
||||
for c in range(1, cols + 1):
|
||||
p = Point(row=r, col=c)
|
||||
full_neighbors = p.neighbors()
|
||||
true_neighbors = [
|
||||
n for n in full_neighbors
|
||||
if 1 <= n.row <= rows and 1 <= n.col <= cols]
|
||||
new_table[p] = true_neighbors
|
||||
neighbor_tables[dim] = new_table
|
||||
|
||||
|
||||
def init_corner_table(dim):
|
||||
rows, cols = dim
|
||||
new_table = {}
|
||||
for r in range(1, rows + 1):
|
||||
for c in range(1, cols + 1):
|
||||
p = Point(row=r, col=c)
|
||||
full_corners = [
|
||||
Point(row=p.row - 1, col=p.col - 1),
|
||||
Point(row=p.row - 1, col=p.col + 1),
|
||||
Point(row=p.row + 1, col=p.col - 1),
|
||||
Point(row=p.row + 1, col=p.col + 1),
|
||||
]
|
||||
true_corners = [
|
||||
n for n in full_corners
|
||||
if 1 <= n.row <= rows and 1 <= n.col <= cols]
|
||||
new_table[p] = true_corners
|
||||
corner_tables[dim] = new_table
|
||||
|
||||
|
||||
class IllegalMoveError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GoString():
|
||||
"""Stones that are linked by a chain of connected stones of the
|
||||
same color.
|
||||
"""
|
||||
def __init__(self, color, stones, liberties):
|
||||
self.color = color
|
||||
self.stones = frozenset(stones)
|
||||
self.liberties = frozenset(liberties)
|
||||
|
||||
def without_liberty(self, point):
|
||||
new_liberties = self.liberties - set([point])
|
||||
return GoString(self.color, self.stones, new_liberties)
|
||||
|
||||
def with_liberty(self, point):
|
||||
new_liberties = self.liberties | set([point])
|
||||
return GoString(self.color, self.stones, new_liberties)
|
||||
|
||||
def merged_with(self, string):
|
||||
"""Return a new string containing all stones in both strings."""
|
||||
assert string.color == self.color
|
||||
combined_stones = self.stones | string.stones
|
||||
return GoString(
|
||||
self.color,
|
||||
combined_stones,
|
||||
(self.liberties | string.liberties) - combined_stones)
|
||||
|
||||
@property
|
||||
def num_liberties(self):
|
||||
return len(self.liberties)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, GoString) and \
|
||||
self.color == other.color and \
|
||||
self.stones == other.stones and \
|
||||
self.liberties == other.liberties
|
||||
|
||||
def __deepcopy__(self, memodict={}):
|
||||
return GoString(self.color, self.stones, copy.deepcopy(self.liberties))
|
||||
|
||||
|
||||
class Board():
|
||||
def __init__(self, num_rows, num_cols):
|
||||
self.num_rows = num_rows
|
||||
self.num_cols = num_cols
|
||||
self._grid = {}
|
||||
self._hash = zobrist_hash.EMPTY_BOARD
|
||||
|
||||
global neighbor_tables
|
||||
dim = (num_rows, num_cols)
|
||||
if dim not in neighbor_tables:
|
||||
init_neighbor_table(dim)
|
||||
if dim not in corner_tables:
|
||||
init_corner_table(dim)
|
||||
self.neighbor_table = neighbor_tables[dim]
|
||||
self.corner_table = corner_tables[dim]
|
||||
self.move_ages = MoveAge(self)
|
||||
|
||||
|
||||
def neighbors(self, point):
|
||||
return self.neighbor_table[point]
|
||||
|
||||
def corners(self, point):
|
||||
return self.corner_table[point]
|
||||
|
||||
def place_stone(self, player, point):
|
||||
assert self.is_on_grid(point)
|
||||
if self._grid.get(point) is not None:
|
||||
print('Illegal play on %s' % str(point))
|
||||
assert self._grid.get(point) is None
|
||||
# 0. Examine the adjacent points.
|
||||
adjacent_same_color = []
|
||||
adjacent_opposite_color = []
|
||||
liberties = []
|
||||
self.move_ages.increment_all()
|
||||
self.move_ages.add(point)
|
||||
for neighbor in self.neighbor_table[point]:
|
||||
neighbor_string = self._grid.get(neighbor)
|
||||
if neighbor_string is None:
|
||||
liberties.append(neighbor)
|
||||
elif neighbor_string.color == player:
|
||||
if neighbor_string not in adjacent_same_color:
|
||||
adjacent_same_color.append(neighbor_string)
|
||||
else:
|
||||
if neighbor_string not in adjacent_opposite_color:
|
||||
adjacent_opposite_color.append(neighbor_string)
|
||||
new_string = GoString(player, [point], liberties)
|
||||
# tag::apply_zobrist[]
|
||||
# 1. Merge any adjacent strings of the same color.
|
||||
for same_color_string in adjacent_same_color:
|
||||
new_string = new_string.merged_with(same_color_string)
|
||||
for new_string_point in new_string.stones:
|
||||
self._grid[new_string_point] = new_string
|
||||
# Remove empty-point hash code.
|
||||
self._hash ^= zobrist_hash.HASH_CODE[point, None]
|
||||
# Add filled point hash code.
|
||||
self._hash ^= zobrist_hash.HASH_CODE[point, player]
|
||||
# end::apply_zobrist[]
|
||||
|
||||
# 2. Reduce liberties of any adjacent strings of the opposite
|
||||
# color.
|
||||
# 3. If any opposite color strings now have zero liberties,
|
||||
# remove them.
|
||||
for other_color_string in adjacent_opposite_color:
|
||||
replacement = other_color_string.without_liberty(point)
|
||||
if replacement.num_liberties:
|
||||
self._replace_string(other_color_string.without_liberty(point))
|
||||
else:
|
||||
self._remove_string(other_color_string)
|
||||
|
||||
def _replace_string(self, new_string):
|
||||
for point in new_string.stones:
|
||||
self._grid[point] = new_string
|
||||
|
||||
def _remove_string(self, string):
|
||||
for point in string.stones:
|
||||
self.move_ages.reset_age(point)
|
||||
# Removing a string can create liberties for other strings.
|
||||
for neighbor in self.neighbor_table[point]:
|
||||
neighbor_string = self._grid.get(neighbor)
|
||||
if neighbor_string is None:
|
||||
continue
|
||||
if neighbor_string is not string:
|
||||
self._replace_string(neighbor_string.with_liberty(point))
|
||||
self._grid[point] = None
|
||||
# Remove filled point hash code.
|
||||
self._hash ^= zobrist_hash.HASH_CODE[point, string.color]
|
||||
# Add empty point hash code.
|
||||
self._hash ^= zobrist_hash.HASH_CODE[point, None]
|
||||
|
||||
def is_self_capture(self, player, point):
|
||||
friendly_strings = []
|
||||
for neighbor in self.neighbor_table[point]:
|
||||
neighbor_string = self._grid.get(neighbor)
|
||||
if neighbor_string is None:
|
||||
# This point has a liberty. Can't be self capture.
|
||||
return False
|
||||
elif neighbor_string.color == player:
|
||||
# Gather for later analysis.
|
||||
friendly_strings.append(neighbor_string)
|
||||
else:
|
||||
if neighbor_string.num_liberties == 1:
|
||||
# This move is real capture, not a self capture.
|
||||
return False
|
||||
if all(neighbor.num_liberties == 1 for neighbor in friendly_strings):
|
||||
return True
|
||||
return False
|
||||
|
||||
def will_capture(self, player, point):
|
||||
for neighbor in self.neighbor_table[point]:
|
||||
neighbor_string = self._grid.get(neighbor)
|
||||
if neighbor_string is None:
|
||||
continue
|
||||
elif neighbor_string.color == player:
|
||||
continue
|
||||
else:
|
||||
if neighbor_string.num_liberties == 1:
|
||||
# This move would capture.
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_on_grid(self, point):
|
||||
return 1 <= point.row <= self.num_rows and \
|
||||
1 <= point.col <= self.num_cols
|
||||
|
||||
def get(self, point):
|
||||
"""Return the content of a point on the board.
|
||||
|
||||
Returns None if the point is empty, or a Player if there is a
|
||||
stone on that point.
|
||||
"""
|
||||
string = self._grid.get(point)
|
||||
if string is None:
|
||||
return None
|
||||
return string.color
|
||||
|
||||
def get_go_string(self, point):
|
||||
"""Return the entire string of stones at a point.
|
||||
|
||||
Returns None if the point is empty, or a GoString if there is
|
||||
a stone on that point.
|
||||
"""
|
||||
string = self._grid.get(point)
|
||||
if string is None:
|
||||
return None
|
||||
return string
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, Board) and \
|
||||
self.num_rows == other.num_rows and \
|
||||
self.num_cols == other.num_cols and \
|
||||
self._hash() == other._hash()
|
||||
|
||||
def __deepcopy__(self, memodict={}):
|
||||
copied = Board(self.num_rows, self.num_cols)
|
||||
# Can do a shallow copy b/c the dictionary maps tuples
|
||||
# (immutable) to GoStrings (also immutable)
|
||||
copied._grid = copy.copy(self._grid)
|
||||
copied._hash = self._hash
|
||||
return copied
|
||||
|
||||
# tag::return_zobrist[]
|
||||
def zobrist_hash(self):
|
||||
return self._hash
|
||||
# end::return_zobrist[]
|
||||
|
||||
|
||||
class Move():
|
||||
"""Any action a player can play on a turn.
|
||||
|
||||
Exactly one of is_play, is_pass, is_resign will be set.
|
||||
"""
|
||||
def __init__(self, point=None, is_pass=False, is_resign=False):
|
||||
assert (point is not None) ^ is_pass ^ is_resign
|
||||
self.point = point
|
||||
self.is_play = (self.point is not None)
|
||||
self.is_pass = is_pass
|
||||
self.is_resign = is_resign
|
||||
|
||||
@classmethod
|
||||
def play(cls, point):
|
||||
"""A move that places a stone on the board."""
|
||||
return Move(point=point)
|
||||
|
||||
@classmethod
|
||||
def pass_turn(cls):
|
||||
return Move(is_pass=True)
|
||||
|
||||
@classmethod
|
||||
def resign(cls):
|
||||
return Move(is_resign=True)
|
||||
|
||||
def __str__(self):
|
||||
if self.is_pass:
|
||||
return 'pass'
|
||||
if self.is_resign:
|
||||
return 'resign'
|
||||
return '(r %d, c %d)' % (self.point.row, self.point.col)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.is_play,
|
||||
self.is_pass,
|
||||
self.is_resign,
|
||||
self.point))
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
self.is_play,
|
||||
self.is_pass,
|
||||
self.is_resign,
|
||||
self.point) == (
|
||||
other.is_play,
|
||||
other.is_pass,
|
||||
other.is_resign,
|
||||
other.point)
|
||||
|
||||
|
||||
class GameState():
|
||||
def __init__(self, board, next_player, previous, move):
|
||||
self.board = board
|
||||
self.next_player = next_player
|
||||
self.previous_state = previous
|
||||
if previous is None:
|
||||
self.previous_states = frozenset()
|
||||
else:
|
||||
self.previous_states = frozenset(
|
||||
previous.previous_states |
|
||||
{(previous.next_player, previous.board.zobrist_hash())})
|
||||
self.last_move = move
|
||||
|
||||
def apply_move(self, move):
|
||||
"""Return the new GameState after applying the move."""
|
||||
if move.is_play:
|
||||
next_board = copy.deepcopy(self.board)
|
||||
next_board.place_stone(self.next_player, move.point)
|
||||
else:
|
||||
next_board = self.board
|
||||
return GameState(next_board, self.next_player.other, self, move)
|
||||
|
||||
@classmethod
|
||||
def new_game(cls, board_size):
|
||||
if isinstance(board_size, int):
|
||||
board_size = (board_size, board_size)
|
||||
board = Board(*board_size)
|
||||
return GameState(board, Player.black, None, None)
|
||||
|
||||
def is_move_self_capture(self, player, move):
|
||||
if not move.is_play:
|
||||
return False
|
||||
return self.board.is_self_capture(player, move.point)
|
||||
|
||||
@property
|
||||
def situation(self):
|
||||
return (self.next_player, self.board)
|
||||
|
||||
def does_move_violate_ko(self, player, move):
|
||||
if not move.is_play:
|
||||
return False
|
||||
if not self.board.will_capture(player, move.point):
|
||||
return False
|
||||
next_board = copy.deepcopy(self.board)
|
||||
next_board.place_stone(player, move.point)
|
||||
next_situation = (player.other, next_board.zobrist_hash())
|
||||
return next_situation in self.previous_states
|
||||
|
||||
def is_valid_move(self, move):
|
||||
if self.is_over():
|
||||
return False
|
||||
if move.is_pass or move.is_resign:
|
||||
return True
|
||||
return (
|
||||
self.board.get(move.point) is None and
|
||||
not self.is_move_self_capture(self.next_player, move) and
|
||||
not self.does_move_violate_ko(self.next_player, move))
|
||||
|
||||
def is_over(self):
|
||||
if self.last_move is None:
|
||||
return False
|
||||
if self.last_move.is_resign:
|
||||
return True
|
||||
if self.previous_state is None:
|
||||
return False
|
||||
second_last_move = self.previous_state.last_move
|
||||
if second_last_move is None:
|
||||
return False
|
||||
return self.last_move.is_pass and second_last_move.is_pass
|
||||
|
||||
def legal_moves(self):
|
||||
moves = []
|
||||
for row in range(1, self.board.num_rows + 1):
|
||||
for col in range(1, self.board.num_cols + 1):
|
||||
move = Move.play(Point(row, col))
|
||||
if self.is_valid_move(move):
|
||||
moves.append(move)
|
||||
# These two moves are always legal.
|
||||
moves.append(Move.pass_turn())
|
||||
moves.append(Move.resign())
|
||||
|
||||
return moves
|
||||
|
||||
def winner(self):
|
||||
if not self.is_over():
|
||||
return None
|
||||
if self.last_move.is_resign:
|
||||
return self.next_player
|
||||
game_result = compute_game_result(self)
|
||||
return game_result.winner
|
||||
@@ -0,0 +1,30 @@
|
||||
import enum
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
|
||||
# tag::color[]
|
||||
class Player(enum.Enum):
|
||||
black = 1
|
||||
white = 2
|
||||
|
||||
@property
|
||||
def other(self):
|
||||
return Player.black if self == Player.white else Player.white
|
||||
# end::color[]
|
||||
|
||||
|
||||
# tag::points[]
|
||||
class Point(namedtuple('Point', 'row col')):
|
||||
def neighbors(self):
|
||||
return [
|
||||
Point(self.row - 1, self.col),
|
||||
Point(self.row + 1, self.col),
|
||||
Point(self.row, self.col - 1),
|
||||
Point(self.row, self.col + 1),
|
||||
]
|
||||
# end::points[]
|
||||
|
||||
def __deepcopy__(self, memodict={}):
|
||||
# These are very immutable.
|
||||
return self
|
||||
@@ -0,0 +1,138 @@
|
||||
# tag::scoring_imports[]
|
||||
from __future__ import absolute_import
|
||||
from collections import namedtuple
|
||||
|
||||
from game_logic.gotypes import Player, Point
|
||||
|
||||
|
||||
|
||||
# tag::scoring_territory[]
|
||||
class Territory:
|
||||
def __init__(self, territory_map): # <1>
|
||||
self.num_black_territory = 0
|
||||
self.num_white_territory = 0
|
||||
self.num_black_stones = 0
|
||||
self.num_white_stones = 0
|
||||
self.num_dame = 0
|
||||
self.dame_points = []
|
||||
for point, status in territory_map.items(): # <2>
|
||||
if status == Player.black:
|
||||
self.num_black_stones += 1
|
||||
elif status == Player.white:
|
||||
self.num_white_stones += 1
|
||||
elif status == 'territory_b':
|
||||
self.num_black_territory += 1
|
||||
elif status == 'territory_w':
|
||||
self.num_white_territory += 1
|
||||
elif status == 'dame':
|
||||
self.num_dame += 1
|
||||
self.dame_points.append(point)
|
||||
|
||||
# <1> A `territory_map` splits the board into stones, territory and neutral points (dame).
|
||||
# <2> Depending on the status of a point, we increment the respective counter.
|
||||
# end::scoring_territory[]
|
||||
|
||||
|
||||
# tag::scoring_game_result[]
|
||||
class GameResult(namedtuple('GameResult', 'b w komi')):
|
||||
@property
|
||||
def winner(self):
|
||||
if self.b > self.w + self.komi:
|
||||
return Player.black
|
||||
return Player.white
|
||||
|
||||
@property
|
||||
def winning_margin(self):
|
||||
w = self.w + self.komi
|
||||
return abs(self.b - w)
|
||||
|
||||
def __str__(self):
|
||||
w = self.w + self.komi
|
||||
if self.b > w:
|
||||
return 'B+%.1f' % (self.b - w,)
|
||||
return 'W+%.1f' % (w - self.b,)
|
||||
# end::scoring_game_result[]
|
||||
|
||||
|
||||
""" evaluate_territory:
|
||||
Map a board into territory and dame.
|
||||
|
||||
Any points that are completely surrounded by a single color are
|
||||
counted as territory; it makes no attempt to identify even
|
||||
trivially dead groups.
|
||||
"""
|
||||
|
||||
|
||||
# tag::scoring_evaluate_territory[]
|
||||
def evaluate_territory(board):
|
||||
|
||||
status = {}
|
||||
for r in range(1, board.num_rows + 1):
|
||||
for c in range(1, board.num_cols + 1):
|
||||
p = Point(row=r, col=c)
|
||||
if p in status: # <1>
|
||||
continue
|
||||
stone = board.get(p)
|
||||
if stone is not None: # <2>
|
||||
status[p] = board.get(p)
|
||||
else:
|
||||
group, neighbors = _collect_region(p, board)
|
||||
if len(neighbors) == 1: # <3>
|
||||
neighbor_stone = neighbors.pop()
|
||||
stone_str = 'b' if neighbor_stone == Player.black else 'w'
|
||||
fill_with = 'territory_' + stone_str
|
||||
else:
|
||||
fill_with = 'dame' # <4>
|
||||
for pos in group:
|
||||
status[pos] = fill_with
|
||||
return Territory(status)
|
||||
|
||||
# <1> Skip the point, if you already visited this as part of a different group.
|
||||
# <2> If the point is a stone, add it as status.
|
||||
# <3> If a point is completely surrounded by black or white stones, count it as territory.
|
||||
# <4> Otherwise the point has to be a neutral point, so we add it to dame.
|
||||
# end::scoring_evaluate_territory[]
|
||||
|
||||
|
||||
""" _collect_region:
|
||||
|
||||
Find the contiguous section of a board containing a point. Also
|
||||
identify all the boundary points.
|
||||
"""
|
||||
|
||||
|
||||
# tag::scoring_collect_region[]
|
||||
def _collect_region(start_pos, board, visited=None):
|
||||
|
||||
if visited is None:
|
||||
visited = {}
|
||||
if start_pos in visited:
|
||||
return [], set()
|
||||
all_points = [start_pos]
|
||||
all_borders = set()
|
||||
visited[start_pos] = True
|
||||
here = board.get(start_pos)
|
||||
deltas = [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
||||
for delta_r, delta_c in deltas:
|
||||
next_p = Point(row=start_pos.row + delta_r, col=start_pos.col + delta_c)
|
||||
if not board.is_on_grid(next_p):
|
||||
continue
|
||||
neighbor = board.get(next_p)
|
||||
if neighbor == here:
|
||||
points, borders = _collect_region(next_p, board, visited)
|
||||
all_points += points
|
||||
all_borders |= borders
|
||||
else:
|
||||
all_borders.add(neighbor)
|
||||
return all_points, all_borders
|
||||
# end::scoring_collect_region[]
|
||||
|
||||
|
||||
# tag::scoring_compute_game_result[]
|
||||
def compute_game_result(game_state):
|
||||
territory = evaluate_territory(game_state.board)
|
||||
return GameResult(
|
||||
territory.num_black_territory + territory.num_black_stones,
|
||||
territory.num_white_territory + territory.num_white_stones,
|
||||
komi=7.5)
|
||||
# end::scoring_compute_game_result[]
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user