non square boards

This commit is contained in:
Sander Land committed 2020-04-21 18:09:35 +02:00
1 parent d14f077e71
commit 18044dce44
7 files changed
+121 -106

No files matched your search

+38 -26
View File
@@ -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
+3 -2
View File
@@ -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]))
+25 -14
View File
@@ -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 = (
+5 -1
View File
@@ -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()
+38 -47
View File
@@ -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 @@
<NewGamePopup>:
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()
+4 -4
View File
@@ -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()
+8 -12
View File
@@ -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