This commit is contained in:
2023-05-23 15:52:09 +08:00
parent e8f9c8a287
commit 663edbb5e2
88 changed files with 6250 additions and 0 deletions
+400
View File
@@ -0,0 +1,400 @@
import copy
from tugo.gotypes import Player, Point
from tugo.scoring import compute_game_result
from tugo import zobrist
from tugo.utils import MoveAge
__all__ = [
'Board',
'GameState',
'Move',
]
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.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_CODE[point, None]
# Add filled point hash code.
self._hash ^= zobrist.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_CODE[point, string.color]
# Add empty point hash code.
self._hash ^= zobrist.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