From d0f49b5f729f767950d4b2087a4a232c40c717ca Mon Sep 17 00:00:00 2001 From: Sander Land Date: Tue, 14 Apr 2020 21:47:03 +0200 Subject: [PATCH] everything is still broken --- game.py | 82 ++++++++++++++++++++++++-- gui/controls.py | 151 ------------------------------------------------ gui/popups.py | 4 ++ katrain.kv | 25 ++++++++ katrain.py | 90 +++++++++++++++++++++++++---- sgf_parser.py | 4 +- 6 files changed, 187 insertions(+), 169 deletions(-) create mode 100644 gui/popups.py diff --git a/game.py b/game.py index 910b05d..7e811a8 100644 --- a/game.py +++ b/game.py @@ -1,4 +1,4 @@ -import os +import os, random from datetime import datetime from game_node import GameNode @@ -9,6 +9,8 @@ from typing import List class IllegalMoveException(Exception): pass +class KaTrainSGF(SGF): + _NODE_CLASS = GameNode class Game: GAME_COUNTER = 0 @@ -24,10 +26,10 @@ class Game: self.game_id = datetime.strftime(datetime.now(), "%Y-%m-%d %H %M %S") self.visits = [ - [analysis_settings["pass_visits"], analysis_settings["visits"], analysis_settings["analyze_all_visits"]], - [analysis_settings["pass_visits_fast"], analysis_settings["visits_fast"], analysis_settings["analyze_all_visits_fast"]], + [analysis_options["pass_visits"], analysis_options["visits"], analysis_options["analyze_all_visits"]], + [analysis_options["pass_visits_fast"], analysis_options["visits_fast"], analysis_options["analyze_all_visits_fast"]], ] - self.train_settings = Config.get("trainer") + #self.train_settings = Config.get("trainer") if move_tree: self.root = move_tree @@ -197,6 +199,74 @@ class Game: f.write(self.root.sgf()) return f"SGF with analysis written to {file_name}" + def ai_move(self): + ts = self.train_settings + while not self.parent.game.current_node.analysis_ready: + self.info.text = "Thinking..." + self.ai_thinking = True + time.sleep(0.05) + self.ai_thinking = False + # select move + current_move = self.parent.game.current_node + pos_moves = [ + (d["move"], float(d["scoreLead"]), d["evaluation"]) for i, d in enumerate(current_move.ai_moves) if i == 0 or int(d["visits"]) >= ts["balance_play_min_visits"] + ] + sel_moves = pos_moves[:1] + # don't play suicidal to balance score - pass when it's best + if self.ai_balance.active and pos_moves[0][0] != "pass": + sel_moves = [ + (move, score, move_eval) + for move, score, move_eval in pos_moves + if move_eval > ts["balance_play_randomize_eval"] + and -current_move.player_sign * score > 0 + or move_eval > ts["balance_play_min_eval"] + and -current_move.player_sign * score > ts["balance_play_target_score"] + ] or sel_moves + aimove = Move.from_gtp(random.choice(sel_moves)[0], player=self.parent.game.next_player) + if len(sel_moves) > 1: + aimove.x_comment["ai"] = "AI Balance on, moves considered: " + ", ".join(f"{move} ({aimove.format_score(score)})" for move, score, _ in sel_moves) + "\n" + self.play(aimove) -class KaTrainSGF(SGF): - _MOVE_CLASS = GameNode + + def num_undos(self, move): + if self.train_settings["num_undo_prompts"] < 1: + return int(move.undo_threshold < self.train_settings["num_undo_prompts"]) + else: + return self.train_settings["num_undo_prompts"] + + def analyze_extra(self,mode): + stones = {s.coords for s in self.parent.game.stones} + current_move = self.current_node + if not current_move.analysis: + self.info.text = "Wait for initial analysis to complete before doing a board-sweep or refinement" + return + played_moves = self.parent.game.moves + + if mode == "extra": + visits = sum([d["visits"] for d in current_move.analysis]) + self.visits[0][1] + self.info.text = f"Performing additional analysis to {visits} visits" + self._request_analysis(current_move, min_visits=visits, priority=self.game_counter - 1_000) + return + elif mode == "sweep": + analyze_moves = [Move(coords=(x, y)).gtp() for x in range(self.parent.game_size) for y in + range(self.parent.game_size) if (x, y) not in stones] + visits = self.visits[self.ai_fast.active][2] + self.info.text = f"Refining analysis of entire board to {visits} visits" + priority = self.game_counter - 1_000_000_000 + else: # mode=='refine': + analyze_moves = [a["move"] for a in current_move.analysis] + visits = current_move.analysis[0]["visits"] + self.visits[1][2] + self.info.text = f"Refining analysis of candidate moves to {visits} visits" + priority = self.game_counter - 1_000 + + for gtpcoords in analyze_moves: + self._send_analysis_query( + { + "id": f"AA:{current_move.id}:{gtpcoords}", + "moves": [[m.bw_player(), m.gtp()] for m in played_moves] + [ + [current_move.bw_player(True), gtpcoords]], + "includeOwnership": False, + "maxVisits": visits, + "priority": priority, + } + ) \ No newline at end of file diff --git a/gui/controls.py b/gui/controls.py index f7d6877..69be082 100644 --- a/gui/controls.py +++ b/gui/controls.py @@ -12,7 +12,6 @@ from kivy.uix.checkbox import CheckBox from kivy.uix.filechooser import FileChooserListView from kivy.uix.gridlayout import GridLayout from kivy.uix.label import Label -from kivy.uix.popup import Popup @@ -76,153 +75,3 @@ class Controls(GridLayout): else: self._do_aimove() self.redraw(include_board=False) - - # engine action functions - - - def _do_aimove(self): - ts = self.train_settings - while not self.parent.game.current_node.analysis_ready: - self.info.text = "Thinking..." - self.ai_thinking = True - time.sleep(0.05) - self.ai_thinking = False - # select move - current_move = self.parent.game.current_node - pos_moves = [ - (d["move"], float(d["scoreLead"]), d["evaluation"]) for i, d in enumerate(current_move.ai_moves) if i == 0 or int(d["visits"]) >= ts["balance_play_min_visits"] - ] - sel_moves = pos_moves[:1] - # don't play suicidal to balance score - pass when it's best - if self.ai_balance.active and pos_moves[0][0] != "pass": - sel_moves = [ - (move, score, move_eval) - for move, score, move_eval in pos_moves - if move_eval > ts["balance_play_randomize_eval"] - and -current_move.player_sign * score > 0 - or move_eval > ts["balance_play_min_eval"] - and -current_move.player_sign * score > ts["balance_play_target_score"] - ] or sel_moves - aimove = Move.from_gtp(random.choice(sel_moves)[0], player=self.parent.game.next_player) - if len(sel_moves) > 1: - aimove.x_comment["ai"] = "AI Balance on, moves considered: " + ", ".join(f"{move} ({aimove.format_score(score)})" for move, score, _ in sel_moves) + "\n" - self.play(aimove) - - def num_undos(self, move): - if self.train_settings["num_undo_prompts"] < 1: - return int(move.undo_threshold < self.train_settings["num_undo_prompts"]) - else: - return self.train_settings["num_undo_prompts"] - - def _do_undo(self): - if ( - self.ai_lock.active - and self.auto_undo.active(self.parent.game.current_node.player) - and len(self.parent.game.current_node.parent.children) > self.num_undos(self.parent.game.current_node) - and not self.train_settings.get("dont_lock_undos") - ): - self.info.text = f"Can't undo this move more than {self.num_undos(self.parent.game.current_node)} time(s) when locked" - return - self.parent.game.undo() - self.update_evaluation() - - def _do_redo(self): - self.parent.game.redo() - self.update_evaluation() - - def _do_redo_branch(self, direction): - self.parent.game.switch_branch(direction) - self.update_evaluation() - - def _do_init(self, board_size=None, komi=None, move_tree=None): - self.game_counter += 1 # prioritize newer games - self.parent.game_size = board_size or 19 - self.komi = float(komi or self.config.get("board").get(f"komi_{board_size}", 6.5)) - self.parent.game = Game(board_size, move_tree) - self._request_analysis(self.parent.game.root, priority=self.game_counter) - self.redraw(include_board=True) - self.ready = True - if self.ai_lock.active: - self.ai_lock.checkbox._do_press() - for el in [self.ai_lock.checkbox, self.hints.black, self.hints.white, self.ai_auto.black, self.ai_auto.white, self.auto_undo.black, self.auto_undo.white, self.ai_move]: - el.disabled = False - - def _do_analyze_extra(self, mode): - stones = {s.coords for s in self.parent.game.stones} - current_move = self.parent.game.current_node - if not current_move.analysis: - self.info.text = "Wait for initial analysis to complete before doing a board-sweep or refinement" - return - played_moves = self.parent.game.moves - - if mode == "extra": - visits = sum([d["visits"] for d in current_move.analysis]) + self.visits[0][1] - self.info.text = f"Performing additional analysis to {visits} visits" - self._request_analysis(current_move, min_visits=visits, priority=self.game_counter - 1_000) - return - elif mode == "sweep": - analyze_moves = [SGFNode(coords=(x, y)).gtp() for x in range(self.parent.game_size) for y in range(self.parent.game_size) if (x, y) not in stones] - visits = self.visits[self.ai_fast.active][2] - self.info.text = f"Refining analysis of entire board to {visits} visits" - priority = self.game_counter - 1_000_000_000 - else: # mode=='refine': - analyze_moves = [a["move"] for a in current_move.analysis] - visits = current_move.analysis[0]["visits"] + self.visits[1][2] - self.info.text = f"Refining analysis of candidate moves to {visits} visits" - priority = self.game_counter - 1_000 - - for gtpcoords in analyze_moves: - self._send_analysis_query( - { - "id": f"AA:{current_move.id}:{gtpcoords}", - "moves": [[m.bw_player(), m.gtp()] for m in played_moves] + [[current_move.bw_player(True), gtpcoords]], - "includeOwnership": False, - "maxVisits": visits, - "priority": priority, - } - ) - - def analyze_movetree(self, root, faster=False): - self._do_init(root["SZ"], root["KM"]) - self.parent.game.root = root - handicap = root["HA"] - if handicap is not None and root["AB"] is None: - self.parent.game.place_handicap_stones(handicap) - analysis_priority = self.game_counter - 1_000_000 - for move in self.parent.game.root.moves_in_tree: - self._request_analysis(move, faster=faster, priority=analysis_priority) # ensure next move analysis works - - def _do_analyze_sgf(self, sgf): - try: - root = KaTrainSGF.parse(sgf) - except: - root = GameNode() - if root.empty: - fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8)) - fc = FileChooserListView(multiselect=False, path=os.path.expanduser(self.config.get("sgf")["load"]), filters=["*.sgf"]) - blui = BoxLayout(orientation="horizontal", size_hint=(1, 0.1)) - cbfast = CheckBox(color=(0.95, 0.95, 0.95, 1)) - cbrewind = CheckBox(color=(0.95, 0.95, 0.95, 1), active=True) - for widget in [Label(text="Analyze Extra Fast"), cbfast, Label(text="Rewind to start"), cbrewind]: - blui.add_widget(widget) - bl = BoxLayout(orientation="vertical") - bl.add_widget(fc) - bl.add_widget(blui) - fileselect_popup.add_widget(bl) - - def readfile(files, _mouse): - fileselect_popup.dismiss() - self.analyze_movetree(KaTrainSGF.parse_file(files[0])) - - fc.on_submit = readfile - fileselect_popup.open() - return - else: - self.analyze_movetree(root) - - - def output_sgf(self): - for pl in Move.PLAYERS: - if self.parent.game.root[f"P{pl}"] not in ["KaTrain","Player",None,""]: - self.parent.game.root[f"P{pl}"] = "KaTrain" if self.ai_auto.active(pl) else "Player" - return self.parent.game.write_sgf(self.komi) diff --git a/gui/popups.py b/gui/popups.py new file mode 100644 index 0000000..2b2004c --- /dev/null +++ b/gui/popups.py @@ -0,0 +1,4 @@ +from kivy.uix.boxlayout import BoxLayout + +class LoadSGFPopup(BoxLayout): + pass diff --git a/katrain.kv b/katrain.kv index ebf5dfa..227cc16 100644 --- a/katrain.kv +++ b/katrain.kv @@ -147,6 +147,31 @@ id: value bold: True + +: + fast: fast + rewind: rewind + filesel: filesel + orientation: 'vertical' + FileChooserListView: + id: filesel + multiselect: False + filters: ["*.sgf"] + path: os.path.expanduser("~") + BoxLayout: + orientation: 'horizontal' + Label: + text: "Analyze Extra Fast" + Checkbox: + id: fast + color: (0.95, 0.95, 0.95) + Label: + text: "Rewind to Start" + Checkbox: + id: rewind + active: True + color: (0.95, 0.95, 0.95) + : size: self.parent.height, self.parent.height engine: self.parent.controls diff --git a/katrain.py b/katrain.py index fb82fce..c5a95e0 100644 --- a/katrain.py +++ b/katrain.py @@ -9,6 +9,8 @@ from kivy.clock import Clock from queue import Queue from game import Game, IllegalMoveException, KaTrainSGF, Move from game_node import GameNode +from kivy.uix.popup import Popup +from gui.popups import LoadSGFPopup OUTPUT_ERROR = -1 OUTPUT_INFO = 0 @@ -39,9 +41,9 @@ class KaTrainGui(BoxLayout): print(message) def _load_config(self): + base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) # for pyinstaller + config_file = sys.argv[1] if len(sys.argv) > 1 else os.path.join(base_path, "config.json") try: - base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) # for pyinstaller - config_file = sys.argv[1] if len(sys.argv) > 1 else os.path.join(base_path, "config.json") self.log(f"Using config file {config_file}",OUTPUT_INFO) self._config_store = JsonStore(config_file) except FileNotFoundError: @@ -77,13 +79,48 @@ class KaTrainGui(BoxLayout): self.log(f"Exception in Engine thread: {e}",OUTPUT_ERROR) raise - def action(self, message, *args): + def __call__(self, message, *args): if self.game: self.message_queue.put([self.game.game_id, message, *args]) def _do_new_game(self,board_size=None): self.game = Game(self,self.engine,self.config("analysis"),self.config("board"),board_size=board_size) + def _do_aimove(self): + self.game.ai_move() + + def _do_undo(self): + if ( + self.controls.ai_lock.active + and self.contols.auto_undo.active(self.game.current_node.player) + and len(self.game.current_node.parent.children) > self.num_undos(self.game.current_node) + and not self.train_settings.get("dont_lock_undos") + ): + self.info.text = f"Can't undo this move more than {self.num_undos(self.game.current_node)} time(s) when locked" + return + self.game.undo() + self.controls.update_evaluation() + + def _do_redo(self): + self.game.redo() + self.update_evaluation() + + def _do_switch_branch(self, direction): + self.game.switch_branch(direction) + self.update_evaluation() + + def _do_init(self, board_size=None, komi=None, move_tree=None): + self.game_counter += 1 # prioritize newer games + self.game_size = board_size or 19 + self.komi = float(komi or self.config.get("board").get(f"komi_{board_size}", 6.5)) + self.game = Game(board_size, move_tree) + self._request_analysis(self.game.root, priority=self.game_counter) + self.redraw(include_board=True) + self.ready = True + if self.ai_lock.active: + self.ai_lock.checkbox._do_press() + for el in [self.ai_lock.checkbox, self.hints.black, self.hints.white, self.ai_auto.black, self.ai_auto.white, self.auto_undo.black, self.auto_undo.white, self.ai_move]: + el.disabled = False def play(self, move: Move, faster=False, analysis_priority=None): try: @@ -99,6 +136,39 @@ class KaTrainGui(BoxLayout): def _do_play(self, *args): self.play(Move(args[0], player=self.board.next_player)) + def _do_analyze_extra(self, mode): + self.game.analyze_extra(mode) + + + def analyze_movetree(self, root, faster=False): + self._do_init(root["SZ"], root["KM"]) + self.parent.game.root = root + handicap = root["HA"] + if handicap is not None and root["AB"] is None: + self.parent.game.place_handicap_stones(handicap) + analysis_priority = self.game_counter - 1_000_000 + for move in self.parent.game.root.moves_in_tree: + self._request_analysis(move, faster=faster, priority=analysis_priority) # ensure next move analysis works + + def _do_analyze_sgf(self, sgf): + fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8)) + popup_contents = LoadSGFPopup() + fileselect_popup.add_widget(popup_contents) + + def readfile(files, _mouse): + fileselect_popup.dismiss() + self.analyze_movetree(KaTrainSGF.parse_file(files[0])) + + popup_contents.filesel.on_submit = readfile + fileselect_popup.open() + + def output_sgf(self): + for pl in Move.PLAYERS: + if self.parent.game.root[f"P{pl}"] not in ["KaTrain","Player",None,""]: + self.parent.game.root[f"P{pl}"] = "KaTrain" if self.ai_auto.active(pl) else "Player" + return self.parent.game.write_sgf(self.komi) + + def redraw(self, include_board=False): if include_board: Clock.schedule_once(self.board.draw_board, -1) # main thread needs to do this @@ -107,19 +177,19 @@ class KaTrainGui(BoxLayout): def _on_keyboard_down(self, keyboard, keycode, text, modifiers): if keycode[1] == "up": - self.controls.action("undo") + self("undo") elif keycode[1] == "down": - self.controls.action("redo") + self("redo") elif keycode[1] == "right": - self.controls.action("redo-branch", 1) + self("switch-branch", 1) elif keycode[1] == "left": - self.controls.action("redo-branch", -1) + self("switch-branch", -1) elif keycode[1] == "s": - self.controls.action("analyze-extra", "sweep") + self("analyze-extra", "sweep") elif keycode[1] == "x": - self.controls.action("analyze-extra", "extra") + self("analyze-extra", "extra") elif keycode[1] == "r": - self.controls.action("analyze-extra", "refine") + self("analyze-extra", "refine") elif keycode[1] == "a": if not self.controls.ai_thinking: self.controls.ai_move.trigger_action(duration=0) diff --git a/sgf_parser.py b/sgf_parser.py index e989eac..20a788f 100644 --- a/sgf_parser.py +++ b/sgf_parser.py @@ -178,7 +178,7 @@ class SGFNode: class SGF: - _MOVE_CLASS = SGFNode + _NODE_CLASS = SGFNode @staticmethod def parse(input_str) -> SGFNode: @@ -218,7 +218,7 @@ class SGF: self._parse_branch(SGFNode(parent=current_move)) elif match[0] == ";": if not current_move.empty: # ignore ; that generate empty nodes - current_move = self._MOVE_CLASS(parent=current_move) + current_move = self._NODE_CLASS(parent=current_move) else: prop, value = match[1], match[2].strip()[1:-1] current_move[prop] = value