non square boards
This commit is contained in:
1 parent
d14f077e71
commit
18044dce44
7 files changed
+121
-106
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user