141 lines
4.8 KiB
Python
141 lines
4.8 KiB
Python
from move import Move
|
|
|
|
|
|
class IllegalMoveException(Exception):
|
|
pass
|
|
|
|
|
|
class Board:
|
|
_move_id_counter = 0 # used to make a map to all moves across all games
|
|
|
|
def __init__(self, board_size=19):
|
|
self.board_size = board_size
|
|
self.root = Move(1, (None, None)) # root is 1=white so black is first
|
|
self.root.id = -1
|
|
self.current_move = self.root
|
|
self.all_moves = {}
|
|
self._init_chains()
|
|
|
|
# -- move tree functions --
|
|
def _init_chains(self):
|
|
self.board = [[-1 for x in range(self.board_size)] for y in range(self.board_size)] # board pos -> chain id
|
|
self.chains = [] # chain id -> chain
|
|
self.prisoners = []
|
|
self.last_capture = []
|
|
try:
|
|
for m in self.moves:
|
|
self._validate_move_and_update_chains(m, True) # ignore ko since we didn't know if it was forced
|
|
except IllegalMoveException as e:
|
|
raise Exception(f"Unexpected illegal move ({str(e)})")
|
|
|
|
def _validate_move_and_update_chains(self, move, ignore_ko):
|
|
def neighbours(moves):
|
|
return {
|
|
self.board[m.coords[1] + dy][m.coords[0] + dx]
|
|
for m in moves
|
|
for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
|
if 0 <= m.coords[0] + dx < self.board_size and 0 <= m.coords[1] + dy < self.board_size
|
|
}
|
|
|
|
ko_or_snapback = len(self.last_capture) == 1 and self.last_capture[0] == move
|
|
self.last_capture = []
|
|
|
|
if move.is_pass:
|
|
return
|
|
|
|
if self.board[move.coords[1]][move.coords[0]] != -1:
|
|
raise IllegalMoveException("Space occupied")
|
|
|
|
nb_chains = list({c for c in neighbours([move]) if c >= 0 and self.chains[c][0].player == move.player})
|
|
if nb_chains:
|
|
this_chain = nb_chains[0]
|
|
self.board = [
|
|
[nb_chains[0] if sq in nb_chains else sq for sq in line] for line in self.board
|
|
] # merge chains connected by this move
|
|
for oc in nb_chains[1:]:
|
|
self.chains[nb_chains[0]] += self.chains[oc]
|
|
self.chains[oc] = []
|
|
self.chains[nb_chains[0]].append(move)
|
|
else:
|
|
this_chain = len(self.chains)
|
|
self.chains.append([move])
|
|
self.board[move.coords[1]][move.coords[0]] = this_chain
|
|
|
|
opp_nb_chains = {c for c in neighbours([move]) if c >= 0 and self.chains[c][0].player != move.player}
|
|
for c in opp_nb_chains:
|
|
if -1 not in neighbours(self.chains[c]):
|
|
self.last_capture += self.chains[c]
|
|
for om in self.chains[c]:
|
|
self.board[om.coords[1]][om.coords[0]] = -1
|
|
self.chains[c] = []
|
|
if ko_or_snapback and len(self.last_capture) == 1 and not ignore_ko:
|
|
raise IllegalMoveException("Ko")
|
|
self.prisoners += self.last_capture
|
|
|
|
if -1 not in neighbours(self.chains[this_chain]):
|
|
raise IllegalMoveException("Suicide")
|
|
|
|
# Play a Move from the current position, returns false if invalid.
|
|
def play(self, move, ignore_ko=False):
|
|
try:
|
|
self._validate_move_and_update_chains(move, ignore_ko)
|
|
except IllegalMoveException as e:
|
|
self._init_chains() # restore
|
|
raise
|
|
|
|
move = self.current_move.play(move) # traverse or append
|
|
if not move.id:
|
|
move.id = Board._move_id_counter
|
|
Board._move_id_counter += 1
|
|
self.all_moves[move.id] = move
|
|
self.current_move = move
|
|
return move
|
|
|
|
def undo(self):
|
|
if self.current_move != self.root:
|
|
self.current_move = self.current_move.parent
|
|
self._init_chains()
|
|
|
|
@property
|
|
def moves(self) -> list: # flat list of moves to current
|
|
moves = []
|
|
p = self.current_move
|
|
while p != self.root:
|
|
moves.append(p)
|
|
p = p.parent
|
|
return moves[::-1]
|
|
|
|
@property
|
|
def current_player(self):
|
|
return self.current_move.player
|
|
|
|
# --analysis
|
|
|
|
def store_analysis(self, json):
|
|
if json["id"].starts_with("PASS_"):
|
|
id = int(json["id"].lstrip("PASS_"))
|
|
else:
|
|
id = int(json["id"])
|
|
move = self.all_moves.get(id)
|
|
if move: # else this should be old
|
|
move.set_analysis(json)
|
|
else:
|
|
print("WARNING: ORPHANED ANALYSIS FOUND - RECENT NEW GAME?")
|
|
|
|
# -- board visualization etc
|
|
# ko: single capture and
|
|
# other not allowed: suicide
|
|
|
|
@property
|
|
def stones(self):
|
|
return sum(self.chains, [])
|
|
|
|
def sgf(self):
|
|
return "SGF[]"
|
|
|
|
def __str__(self):
|
|
return (
|
|
"\n".join("".join("BW"[self.chains[c][0].player] if c >= 0 else "-" for c in l) for l in self.board)
|
|
+ f"\ncaptures: {self.prisoners}"
|
|
)
|