From 18044dce4455eb494b45bbe1e69ca882654ca6cc Mon Sep 17 00:00:00 2001 From: Sander Land Date: Tue, 21 Apr 2020 18:09:35 +0200 Subject: [PATCH] non square boards --- game.py | 64 ++++++++++++++++++++++--------------- game_node.py | 5 +-- gui/badukpan.py | 39 +++++++++++++++-------- gui/popups.py | 6 +++- katrain.kv | 85 ++++++++++++++++++++++--------------------------- katrain.py | 8 ++--- sgf_parser.py | 20 +++++------- 7 files changed, 121 insertions(+), 106 deletions(-) diff --git a/game.py b/game.py index cf40521..f22a008 100644 --- a/game.py +++ b/game.py @@ -25,7 +25,7 @@ class Game: DEFAULT_PROPERTIES = {"GM": 1, "FF": 4, "RU": "JP", "AP": "KaTrain:https://github.com/sanderland/katrain"} - def __init__(self, katrain, engine, config, board_size=None, move_tree=None): + def __init__(self, katrain, engine, config, move_tree=None): self.katrain = katrain self.engine = engine self.config = config @@ -33,15 +33,14 @@ class Game: if move_tree: self.root = move_tree - self.board_size = self.root.board_size self.komi = self.root.komi handicap = self.root.get_first("HA") - if handicap is not None and not self.root.placements: + if handicap and not self.root.placements: self.place_handicap_stones(handicap) else: - self.board_size = board_size or config["init_size"] - self.komi = self.config.get(f"komi_{self.board_size}", 6.5) - self.root = GameNode(properties={**Game.DEFAULT_PROPERTIES, **{"SZ": self.board_size, "KM": self.komi, "DT": self.game_id}}) + board_size = config["init_size"] + self.komi = self.config.get(f"komi_{board_size}", 6.5) + self.root = GameNode(properties={**Game.DEFAULT_PROPERTIES, **{"SZ": board_size, "KM": self.komi, "DT": self.game_id}}) self.current_node = self.root self._init_chains() @@ -55,7 +54,8 @@ class Game: # -- move tree functions -- def _init_chains(self): - self.board = [[-1 for _x in range(self.board_size)] for _y in range(self.board_size)] # type: List[List[int]] # board pos -> chain id + board_size_x, board_size_y = self.board_size + self.board = [[-1 for _x in range(board_size_x)] for _y in range(board_size_y)] # type: List[List[int]] # board pos -> chain id self.chains = [] # type: List[List[Move]] # chain id -> chain self.prisoners = [] # type: List[Move] self.last_capture = [] # type: List[Move] @@ -68,12 +68,14 @@ class Game: raise Exception(f"Unexpected illegal move ({str(e)})") def _validate_move_and_update_chains(self, move: Move, ignore_ko: bool): + board_size_x, board_size_y = self.board_size + 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 + if 0 <= m.coords[0] + dx < board_size_x and 0 <= m.coords[1] + dy < board_size_y } ko_or_snapback = len(self.last_capture) == 1 and self.last_capture[0] == move @@ -114,7 +116,8 @@ class Game: # Play a Move from the current position, raise IllegalMoveException if invalid. def play(self, move: Move, ignore_ko: bool = False): - if not move.is_pass and not (0 <= move.coords[0] < self.board_size and 0 <= move.coords[1] < self.board_size): + board_size_x, board_size_y = self.board_size + if not move.is_pass and not (0 <= move.coords[0] < board_size_x and 0 <= move.coords[1] < board_size_y): raise IllegalMoveException(f"Move {move} outside of board coordinates") try: self._validate_move_and_update_chains(move, ignore_ko) @@ -150,24 +153,32 @@ class Game: self._init_chains() def place_handicap_stones(self, n_handicaps): - near = 3 if self.board_size >= 13 else 2 - far = self.board_size - 1 - near - middle = self.board_size // 2 - if n_handicaps > 9: + board_size_x, board_size_y = self.board_size + near_x = 3 if board_size_x >= 13 else 2 + near_y = 3 if board_size_y >= 13 else 2 + far_x = board_size_x - 1 - near_x + far_y = board_size_x - 1 - near_x + middle_x = board_size_x // 2 # what for even sizes? + middle_y = board_size_y // 2 + if n_handicaps > 9 and board_size_x == board_size_y: stones_per_row = math.ceil(math.sqrt(n_handicaps)) - spacing = (far - near) / (stones_per_row - 1) - if spacing < near: - far += 1 - near -= 1 - spacing = (far - near) / (stones_per_row - 1) - coords = [math.floor(0.5 + near + i * spacing) for i in range(stones_per_row)] - stones = sorted([(x, y) for x in coords for y in coords], key=lambda xy: -((xy[0] - self.board_size / 2) ** 2 + (xy[1] - self.board_size / 2) ** 2)) - else: - stones = [(far, far), (near, near), (far, near), (near, far)] + spacing = (far_x - near_x) / (stones_per_row - 1) + if spacing < near_x: + far_x += 1 + near_x -= 1 + spacing = (far_x - near_x) / (stones_per_row - 1) + coords = [math.floor(0.5 + near_x + i * spacing) for i in range(stones_per_row)] + stones = sorted([(x, y) for x in coords for y in coords], key=lambda xy: -((xy[0] - board_size_x / 2) ** 2 + (xy[1] - board_size_y / 2) ** 2)) + else: # max 9 + stones = [(far_x, far_y), (near_x, near_y), (far_x, near_y), (near_x, far_y)] if n_handicaps % 2 == 1: - stones.append((middle, middle)) - stones += [(near, middle), (far, middle), (middle, near), (middle, far)] - self.root.add_property("AB", [Move(stone).sgf(board_size=self.board_size) for stone in stones[:n_handicaps]]) + stones.append((middle_x, middle_y)) + stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)] + self.root.add_property("AB", [Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]]) + + @property + def board_size(self): + return self.root.board_size @property def next_player(self): @@ -268,7 +279,8 @@ class Game: cn.analyze(self.engine, visits=visits, priority=-1_000) return elif mode == "sweep": - analyze_moves = [Move(coords=(x, y), player=cn.next_player) for x in range(self.board_size) for y in range(self.board_size) if (x, y) not in stones] + board_size_x, board_size_y = self.board_size + analyze_moves = [Move(coords=(x, y), player=cn.next_player) for x in range(board_size_x) for y in range(board_size_y) if (x, y) not in stones] visits = self.engine.config["visits_fast"] self.katrain.controls.set_status(f"Refining analysis of entire board to {visits} visits") priority = -1_000_000_000 diff --git a/game_node.py b/game_node.py index 2fbc254..3460ace 100644 --- a/game_node.py +++ b/game_node.py @@ -118,8 +118,9 @@ class GameNode(SGFNode): if self.policy: ix = 0 moves = [] - for y in range(self.board_size - 1, -1, -1): - for x in range(self.board_size): + szx, szy = self.board_size + for y in range(szy - 1, -1, -1): + for x in range(szx): moves.append((Move((x, y), player=self.next_player), self.policy[ix])) ix += 1 moves.append((Move(None, player=self.next_player), self.policy[ix])) diff --git a/gui/badukpan.py b/gui/badukpan.py index a6bef89..0582644 100644 --- a/gui/badukpan.py +++ b/gui/badukpan.py @@ -99,7 +99,8 @@ class BadukPanWidget(Widget): if not self.ui_config: return katrain = self.katrain - board_size = katrain.game.board_size + board_size_x, board_size_y = katrain.game.board_size + max_board_size = max(board_size_x, board_size_y) self.canvas.before.clear() with self.canvas.before: # board @@ -108,29 +109,39 @@ class BadukPanWidget(Widget): Rectangle(pos=self.pos, size=(self.width, self.height)) # grid lines margin = 1.5 - self.grid_size = board_px_size / (board_size - 1 + 1.5 * margin) + margin_x = margin + (max_board_size - board_size_x) / 2 + margin_y = margin + (max_board_size - board_size_y) / 2 + + self.grid_size = board_px_size / (max_board_size - 1 + 1.5 * margin) self.stone_size = self.grid_size * self.ui_config["stone_size"] - self.gridpos_x = [self.pos[0] + math.floor((margin + i) * self.grid_size + 0.5) for i in range(board_size)] - self.gridpos_y = [self.pos[1] + math.floor((margin + i) * self.grid_size + 0.5) for i in range(board_size)] + self.gridpos_x = [self.pos[0] + math.floor((margin_x + i) * self.grid_size + 0.5) for i in range(board_size_x)] # + self.gridpos_y = [self.pos[1] + math.floor((margin_y + i) * self.grid_size + 0.5) for i in range(board_size_y)] line_color = self.ui_config["line_color"] Color(*line_color) - for i in range(board_size): + for i in range(board_size_x): Line(points=[(self.gridpos_x[i], self.gridpos_y[0]), (self.gridpos_x[i], self.gridpos_y[-1])]) + for i in range(board_size_y): Line(points=[(self.gridpos_x[0], self.gridpos_y[i]), (self.gridpos_x[-1], self.gridpos_y[i])]) # star points - star_point_pos = 3 if board_size <= 11 else 4 + def star_point_coords(size): + star_point_pos = 3 if size <= 11 else 4 + if size < 7: + return [] + return [star_point_pos - 1, size - star_point_pos] + ([int(size / 2)] if size % 2 == 1 and size > 7 else []) + starpt_size = self.grid_size * self.ui_config["starpoint_size"] - for x in [star_point_pos - 1, board_size - star_point_pos, int(board_size / 2)]: - for y in [star_point_pos - 1, board_size - star_point_pos, int(board_size / 2)]: + for x in star_point_coords(board_size_x): + for y in star_point_coords(board_size_y): draw_circle((self.gridpos_x[x], self.gridpos_y[y]), starpt_size, line_color) # coordinates Color(0.25, 0.25, 0.25) coord_offset = self.grid_size * margin / 2 - for i in range(board_size): + for i in range(board_size_x): draw_text(pos=(self.gridpos_x[i], self.gridpos_y[0] - coord_offset), text=Move.GTP_COORD[i], font_size=self.grid_size / 1.5) + for i in range(board_size_y): draw_text(pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]), text=str(i + 1), font_size=self.grid_size / 1.5) def draw_board_contents(self, *args): @@ -140,7 +151,7 @@ class BadukPanWidget(Widget): outline_color = self.ui_config["outline"] ghost_alpha = self.ui_config["ghost_alpha"] katrain = self.katrain - board_size = katrain.game.board_size + board_size_x, board_size_y = katrain.game.board_size self.canvas.clear() with self.canvas: @@ -172,8 +183,8 @@ class BadukPanWidget(Widget): if katrain.controls.ownership.active and ownership: rsz = self.grid_size * 0.2 ix = 0 - for y in range(board_size - 1, -1, -1): - for x in range(board_size): + for y in range(board_size_y - 1, -1, -1): + for x in range(board_size_x): ix_owner = "B" if ownership[ix] > 0 else "W" if ix_owner != (has_stone.get((x, y), -1)): Color(*stone_color[ix_owner], abs(ownership[ix])) @@ -188,8 +199,8 @@ class BadukPanWidget(Widget): if katrain.controls.policy.active and policy and not katrain.controls.ownership.active: ix = 0 best_move_policy = max(*policy) - for y in range(board_size - 1, -1, -1): - for x in range(board_size): + for y in range(board_size_y - 1, -1, -1): + for x in range(board_size_x): if policy[ix] > 0: polsize = math.sqrt(policy[ix]) policy_circle_color = ( diff --git a/gui/popups.py b/gui/popups.py index 57e9c9d..db4c712 100644 --- a/gui/popups.py +++ b/gui/popups.py @@ -62,7 +62,11 @@ class NewGamePopup(QuickConfigGui): properties = self.collect_properties(self) self.katrain.log(f"New game settings: {properties}", OUTPUT_DEBUG) new_root = GameNode(properties={**Game.DEFAULT_PROPERTIES, **properties}) - self.katrain("new-game", None, new_root) + x, y = new_root.board_size + if x > 52 or y > 52: + self.info.text = "Board size too big, should be at most 52" + return + self.katrain("new-game", new_root) self.popup.dismiss() diff --git a/katrain.kv b/katrain.kv index 43e8e47..09f8421 100644 --- a/katrain.kv +++ b/katrain.kv @@ -374,7 +374,7 @@ size_hint: None,1 width: 3 LargeLabel: - text: 'free real estate' + text: '' size_hint: 1,0.25 CensorableLabel: id: points_lost @@ -477,49 +477,40 @@ : orientation: 'vertical' rules_spinner: rules_spinner - BoxLayout: - size_hint: 1, 0.9 - orientation: 'horizontal' - GridLayout: - size_hint: 0.5, 1 - rows: 10 - cols: 2 - Label: - text: "Size" - size_hint: 0.5, 0.1 - LabelledIntInput: - text: '19' - size_hint: 0.5, 0.1 - input_property: 'SZ' - Label: - text: "Handicap" - size_hint: 0.5, 0.1 - LabelledIntInput: - text: '0' - size_hint: 0.5, 0.1 - input_property: 'HA' - Label: - text: "Komi" - size_hint: 0.5, 0.1 - LabelledFloatInput: - text: '6.5' - size_hint: 0.5, 0.1 - input_property: 'KM' - Label: - text: "Ruleset" - size_hint: 0.5, 0.1 - LabelledSpinner: - size_hint: 0.5, 0.1 - input_property: 'RU' - id: rules_spinner - Label: - text: " " - size_hint: 0.5, 0.6 - GridLayout: - size_hint: 0.5, 1 - rows: 10 - cols: 2 - StyledButton: - size_hint: 0.5, 0.1 - text: 'Start Game' - on_press: root.new_game() + info: info + GridLayout: + size_hint: 1, 0.8 + rows: 5 + cols: 2 + Label: + text: "Size" + LabelledTextInput: + text: '19' + input_property: 'SZ' + Label: + text: "Handicap" + LabelledIntInput: + text: '0' + input_property: 'HA' + Label: + text: "Komi" + LabelledFloatInput: + text: '6.5' + input_property: 'KM' + Label: + text: "Ruleset" + LabelledSpinner: + input_property: 'RU' + id: rules_spinner + Label: + text: "Use x:y (e.g. 19:9) to play on a non-square board, but note that handicaps above 9 are not supported." + size_hint: 1,3 + halign: 'center' + valign: 'center' + multiline: True + text_size: self.size + id: info + StyledButton: + text: 'Start\nGame' + font_size: 0.3 * self.size[1] + on_press: root.new_game() diff --git a/katrain.py b/katrain.py index 4c95f6c..e53a8db 100644 --- a/katrain.py +++ b/katrain.py @@ -122,8 +122,8 @@ class KaTrainGui(BoxLayout): if self.game: self.message_queue.put([self.game.game_id, message, *args]) - def _do_new_game(self, board_size=None, move_tree=None): - self.game = Game(self, self.engine, self.config("game"), board_size=board_size, move_tree=move_tree) + def _do_new_game(self, move_tree=None): + self.game = Game(self, self.engine, self.config("game"), move_tree=move_tree) self.controls.select_mode("analyze" if move_tree and len(move_tree.nodes_in_tree) > 1 else "play") self.update_state(redraw_board=True) # TODO: just board here/redraw is in all anyway? @@ -168,8 +168,8 @@ class KaTrainGui(BoxLayout): fileselect_popup.open() def _do_new_game_popup(self): - new_game_popup = Popup(title="New Game", size_hint=(0.9, 0.9)) - popup_contents = NewGamePopup(self, new_game_popup, {k: v[0] for k, v in self.game.root.properties.items()}) + new_game_popup = Popup(title="New Game", size_hint=(0.5, 0.6)) + popup_contents = NewGamePopup(self, new_game_popup, {k: v[0] for k, v in self.game.root.properties.items() if len(v) == 1}) new_game_popup.add_widget(popup_contents) new_game_popup.open() diff --git a/sgf_parser.py b/sgf_parser.py index f7ad6c2..4b8be4c 100644 --- a/sgf_parser.py +++ b/sgf_parser.py @@ -26,9 +26,9 @@ class Move: @classmethod def from_sgf(cls, sgf_coords, board_size, player="B"): - if sgf_coords == "" or Move.SGF_COORD.index(sgf_coords[0]) == board_size: # some servers use [tt] for pass + if sgf_coords == "" or Move.SGF_COORD.index(sgf_coords[0]) == board_size[0]: # some servers use [tt] for pass return cls(coords=None, player=player) - return cls(coords=(Move.SGF_COORD.index(sgf_coords[0]), board_size - Move.SGF_COORD.index(sgf_coords[1]) - 1), player=player) + return cls(coords=(Move.SGF_COORD.index(sgf_coords[0]), board_size[0] - Move.SGF_COORD.index(sgf_coords[1]) - 1), player=player) def __init__(self, coords: Optional[Tuple[int, int]] = None, player: str = "B"): self.player = player @@ -48,7 +48,7 @@ class Move: def sgf(self, board_size): if self.is_pass: return "" - return f"{Move.SGF_COORD[self.coords[0]]}{Move.SGF_COORD[board_size - self.coords[1] - 1]}" + return f"{Move.SGF_COORD[self.coords[0]]}{Move.SGF_COORD[board_size[0] - self.coords[1] - 1]}" @property def is_pass(self): @@ -129,18 +129,14 @@ class SGFNode: self._depth = self.parent.depth + 1 return self._depth - # root properties available on any node + # some root properties are available on any node @property - def board_size(self) -> Union[int, Tuple]: + def board_size(self) -> Tuple[int, int]: size = str(self.root.get_first("SZ", "19")) if ":" in size: - return tuple(map(size.split(":"), int)) - return int(size) - - @property - def board_size_xy(self) -> Tuple[int, int]: - x, y = self.board_size - if not y: + x, y = map(int, size.split(":")) + else: + x = int(size) y = x return x, y