From 7a4a8e268b4817459ba4ea672afda9d3cc84f8ff Mon Sep 17 00:00:00 2001 From: Sander Land Date: Sun, 28 Jun 2020 14:10:20 +0200 Subject: [PATCH] rank model start and gibo file support --- katrain/__main__.py | 10 +- katrain/core/ai.py | 21 +- katrain/core/constants.py | 13 + katrain/core/game.py | 33 +-- katrain/core/sgf_parser.py | 178 +++++++++++- katrain/gui/popups.py | 15 + .../i18n/locales/en/LC_MESSAGES/katrain.po | 4 + katrain/popups.kv | 12 + tests/data/test.gib | 271 ++++++++++++++++++ tests/test_parser.py | 20 +- 10 files changed, 525 insertions(+), 52 deletions(-) create mode 100644 tests/data/test.gib diff --git a/katrain/__main__.py b/katrain/__main__.py index 04bc2c2..0311fb1 100644 --- a/katrain/__main__.py +++ b/katrain/__main__.py @@ -325,7 +325,7 @@ class KaTrainGui(Screen, KaTrainBase): self.controls.timer.paused = True if not self.ai_settings_popup: self.ai_settings_popup = I18NPopup( - title_key="ai settings", size=[dp(600), dp(600)], content=ConfigAIPopup(self) + title_key="ai settings", size=[dp(600), dp(650)], content=ConfigAIPopup(self) ).__self__ self.ai_settings_popup.content.popup = self.ai_settings_popup self.ai_settings_popup.open() @@ -366,7 +366,7 @@ class KaTrainGui(Screen, KaTrainBase): def _do_output_sgf(self): msg = self.game.write_sgf(self.config("general/sgf_save")) self.log(msg, OUTPUT_INFO) - self.controls.set_status(msg) + self.controls.set_status(msg,OUTPUT_INFO) def load_sgf_from_clipboard(self): clipboard = Clipboard.paste() @@ -374,7 +374,7 @@ class KaTrainGui(Screen, KaTrainBase): self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.",STATUS_ERROR) return try: - move_tree = KaTrainSGF.parse(clipboard) + move_tree = KaTrainSGF.parse_sgf(clipboard) except Exception as exc: self.controls.set_status( i18n._("Failed to import from clipboard").format(error=exc, contents=clipboard[:50]) @@ -497,7 +497,7 @@ class KaTrainApp(MDApp): Builder.load_file(kv_file) Window.bind(on_request_close=self.on_request_close) - Window.bind(on_dropfile=lambda win, file: self.gui.load_sgf_file(file)) + Window.bind(on_dropfile=lambda win, file: self.gui.load_sgf_file(file.decode('utf8'))) self.gui = KaTrainGui() Builder.load_file(popup_kv_file) @@ -510,7 +510,7 @@ class KaTrainApp(MDApp): self.gui.save_config() if self.gui.game: self.gui.update_state() - self.gui.controls.set_status("") + self.gui.controls.set_status("",STATUS_INFO) def webbrowser(self, site_key): websites = {"homepage": HOMEPAGE + "#manual", "support": HOMEPAGE + "#support"} diff --git a/katrain/core/ai.py b/katrain/core/ai.py index e9704d2..31f3baa 100644 --- a/katrain/core/ai.py +++ b/katrain/core/ai.py @@ -2,7 +2,7 @@ import heapq import math import random import time -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Optional from katrain.core.utils import var_to_grid from katrain.core.constants import ( @@ -22,11 +22,22 @@ from katrain.core.constants import ( AI_PICK, AI_RANK, AI_HANDICAP, - OUTPUT_ERROR, + OUTPUT_ERROR, AI_STRENGTH, ) from katrain.core.game import Game, GameNode, Move +def ai_rank_estimation(strategy,settings) -> Tuple[int,bool]: + if strategy in [AI_DEFAULT,AI_HANDICAP,AI_JIGO]: + return 9, True + if strategy == AI_RANK: + return 1-settings['kyu_rank'], True + if strategy == AI_WEIGHTED: + dan_rank = -4 + return dan_rank, True + else: + return AI_STRENGTH[strategy], False + def weighted_selection_without_replacement(items: List[Tuple], pick_n: int) -> List[Tuple]: """For a list of tuples where the second element is a weight, returns random items with those weights, without replacement.""" elt = [(math.log(random.random()) / item[1], item) for item in items] # magic @@ -95,7 +106,7 @@ def generate_local_tenuki_weights(ai_mode, ai_settings, policy_grid, cn, size): return weighted_coords, ai_thoughts -def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Dict: +def request_ai_analysis(game: Game, cn: GameNode, extra_settings: Dict) -> Optional[Dict]: error = False analysis = None @@ -259,9 +270,9 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, candidate_ai_moves = handicap_analysis["moveInfos"] top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) - if top_cand.is_pass: # don't play suicidal to balance score - pass when it's best + if top_cand.is_pass and not ai_mode not in [AI_DEFAULT, AI_HANDICAP]: # don't play suicidal to balance score aimove = top_cand - ai_thoughts += f"Top move is pass, so passing regardless of strategy." + ai_thoughts += f"Top move is pass, so passing regardless of strategy. " else: if ai_mode == AI_JIGO: sign = cn.player_sign(cn.next_player) diff --git a/katrain/core/constants.py b/katrain/core/constants.py index bb72447..86bcbd5 100644 --- a/katrain/core/constants.py +++ b/katrain/core/constants.py @@ -55,6 +55,19 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [ AI_JIGO, ] +AI_STRENGTH = { # dan ranks + AI_DEFAULT: 9, + AI_POLICY: 4, + AI_JIGO: float('nan'), + AI_SCORELOSS: -4, + AI_WEIGHTED: -4, + AI_PICK: -7, + AI_LOCAL: -4, + AI_TENUKI: -7, + AI_INFLUENCE: -7, + AI_TERRITORY: -7, + AI_RANK: float('nan') +} AI_OPTION_VALUES = { "kyu_rank": [(k, f"{k}[strength:kyu]") for k in range(15, 0, -1)] diff --git a/katrain/core/game.py b/katrain/core/game.py index 9a944c5..cb31682 100644 --- a/katrain/core/game.py +++ b/katrain/core/game.py @@ -48,7 +48,7 @@ class Game: self.komi = self.root.komi handicap = int(self.root.get_property("HA", 0)) if handicap and not self.root.placements: - self.place_handicap_stones(handicap) + self.root.place_handicap_stones(handicap) else: board_size = katrain.config("game/size") self.komi = katrain.config("game/komi") @@ -61,7 +61,7 @@ class Game: ) handicap = katrain.config("game/handicap") if handicap: - self.place_handicap_stones(handicap) + self.root.place_handicap_stones(handicap) if not self.root.get_property("RU"): self.root.set_property("RU", katrain.config("game/rules")) @@ -184,34 +184,7 @@ class Game: ix = (ordered_children.index(cn) + len(ordered_children) + direction) % len(ordered_children) self.set_current_node(ordered_children[ix]) - def place_handicap_stones(self, n_handicaps): - board_size_x, board_size_y = self.board_size - near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1) - near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1) - far_x = board_size_x - 1 - near_x - far_y = board_size_y - 1 - near_y - 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_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 = list({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 - 1) / 2) ** 2 + (xy[1] - (board_size_y - 1) / 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_x, middle_y)) - stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)] - self.root.set_property( - "AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]}) - ) + @property def board_size(self): diff --git a/katrain/core/sgf_parser.py b/katrain/core/sgf_parser.py index 8574139..f475504 100644 --- a/katrain/core/sgf_parser.py +++ b/katrain/core/sgf_parser.py @@ -1,4 +1,5 @@ import copy +import math import re from collections import defaultdict from typing import Any, Dict, List, Optional, Tuple @@ -285,6 +286,37 @@ class SGFNode: else: return "W" + def place_handicap_stones(self, n_handicaps, tygem=False): + board_size_x, board_size_y = self.board_size + near_x = 3 if board_size_x >= 13 else min(2, board_size_x - 1) + near_y = 3 if board_size_y >= 13 else min(2, board_size_y - 1) + far_x = board_size_x - 1 - near_x + far_y = board_size_y - 1 - near_y + 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_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 = list({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 - 1) / 2) ** 2 + (xy[1] - (board_size_y - 1) / 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_x, middle_y)) + stones += [(near_x, middle_y), (far_x, middle_y), (middle_x, near_y), (middle_x, far_y)] + if tygem: + stones[2],stones[3] = stones[3],stones[2] + self.set_property( + "AB", list({Move(stone).sgf(board_size=(board_size_x, board_size_y)) for stone in stones[:n_handicaps]}) + ) + class SGF: @@ -293,23 +325,31 @@ class SGF: SGFPROP_PAT = re.compile(r"\s*(?:\(|\)|;|(\w+)((\s*\[([^\]\\]|\\.)*\])+))", flags=re.DOTALL) @classmethod - def parse(cls, input_str) -> SGFNode: + def parse_sgf(cls, input_str) -> SGFNode: """Parse a string as SGF.""" return cls(input_str).root @classmethod def parse_file(cls, filename, encoding=None) -> SGFNode: + is_gib = filename.lower().endswith('.gib') + """Parse a file as SGF, encoding will be detected if not given.""" with open(filename, "rb") as f: bin_contents = f.read() if not encoding: - match = re.search(rb"CA\[(.*?)\]", bin_contents) - if match: - encoding = match[1].decode("ascii", errors="ignore") + if not is_gib: + match = re.search(rb"CA\[(.*?)\]", bin_contents) + if match: + encoding = match[1].decode("ascii", errors="ignore") + else: + encoding = "ISO-8859-1" # default else: - encoding = "ISO-8859-1" # default - decoded = bin_contents.decode(encoding=encoding, errors="ignore") - return cls.parse(decoded) + encoding = 'utf8' # ? + decoded = bin_contents.decode(encoding=encoding, errors="ignore") + if is_gib: + return cls.parse_gib(decoded) + else: # sgf + return cls.parse_sgf(decoded) def __init__(self, contents): self.contents = contents @@ -341,3 +381,127 @@ class SGF: if self.ix < len(self.contents): raise ParseError(f"Parse Error: unexpected character at {self.contents[self.ix:self.ix+25]}") raise ParseError("Parse Error: expected ')' at end of input.") + + + # GIB parser adapted from https://github.com/fohristiwhirl/gofish/ + @classmethod + def parse_gib(cls,gib): + def parse_player_name(raw): + name = raw + rank = "" + foo = raw.split("(") + if len(foo) == 2: + if foo[1][-1] == ")": + name = foo[0].strip() + rank = foo[1][0:-1] + return name, rank + + def gib_make_result(grlt, zipsu): + easycases = {3: "B+R", 4: "W+R", 7: "B+T", 8: "W+T"} + + if grlt in easycases: + return easycases[grlt] + + if grlt in [0, 1]: + return "{}+{}".format("B" if grlt == 0 else "W", zipsu / 10) + + return "" + + def gib_get_result(line, grlt_regex, zipsu_regex): + try: + grlt = int(re.search(grlt_regex, line).group(1)) + zipsu = int(re.search(zipsu_regex, line).group(1)) + except: + return "" + return gib_make_result(grlt, zipsu) + + root = cls._NODE_CLASS() + node = root + + lines = gib.split("\n") + for line in lines: + line = line.strip() + if line.startswith("\\[GAMEBLACKNAME=") and line.endswith("\\]"): + s = line[16:-2] + name, rank = parse_player_name(s) + if name: + root.set_property("PB", name) + if rank: + root.set_property("BR", rank) + + if line.startswith("\\[GAMEWHITENAME=") and line.endswith("\\]"): + s = line[16:-2] + name, rank = parse_player_name(s) + if name: + root.set_property("PW", name) + if rank: + root.set_property("WR", rank) + + if line.startswith("\\[GAMEINFOMAIN="): + result = gib_get_result(line, r"GRLT:(\d+),", r"ZIPSU:(\d+),") + if result: + root.set_property("RE", result) + try: + komi = int(re.search(r"GONGJE:(\d+),", line).group(1)) / 10 + if komi: + root.set_property("KM", komi) + except: + pass + + if line.startswith("\\[GAMETAG="): + if "DT" not in root.properties: + try: + match = re.search(r"C(\d\d\d\d):(\d\d):(\d\d)", line) + date = "{}-{}-{}".format(match.group(1), match.group(2), match.group(3)) + root.set_property("DT", date) + except: + pass + + if "RE" not in root.properties: + result = gib_get_result(line, r",W(\d+),", r",Z(\d+),") + if result: + root.set_property("RE", result) + + if "KM" not in root.properties: + try: + komi = int(re.search(r",G(\d+),", line).group(1)) / 10 + if komi: + root.set_property("KM", komi) + except: + pass + + if line[0:3] == "INI": + if node is not root: + raise ParseError("Node is not root") + setup = line.split() + try: + handicap = int(setup[3]) + except IndexError: + continue + + if handicap < 0 or handicap > 9: + raise ParseError(f"Handicap {handicap} out of range") + + if handicap >= 2: + root.set_property("HA", handicap) + root.place_handicap_stones(handicap,tygem=True) + + if line[0:3] == "STO": + move = line.split() + key = "B" if move[3] == "1" else "W" + try: + x = int(move[4]) + y = 18 - int(move[5]) + if not (0 <= x < 19 and 0 <= y < 19): + raise ParseError(f"Coordinates for move ({x},{y}) out of range on line {line}") + value = Move(coords=(x,y)).sgf(board_size=(19,19)) + except IndexError: + continue + + node = cls._NODE_CLASS(parent=node) + node.set_property(key, value ) + + if len(root.children) == 0: # We'll assume we failed in this case + raise ParseError("No valid nodes found") + + return root \ No newline at end of file diff --git a/katrain/gui/popups.py b/katrain/gui/popups.py index 76de150..c3bac16 100644 --- a/katrain/gui/popups.py +++ b/katrain/gui/popups.py @@ -15,6 +15,7 @@ from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.selectioncontrol import MDCheckbox from kivymd.uix.textfield import MDTextField +from katrain.core.ai import ai_rank_estimation from katrain.core.constants import ( AI_CONFIG_DEFAULT, AI_DEFAULT, @@ -29,6 +30,7 @@ from katrain.core.lang import i18n from katrain.core.utils import PATHS, find_package_resource from katrain.gui.kivyutils import BackgroundMixin, I18NSpinner from katrain.gui.style import DEFAULT_FONT, EVAL_COLORS +from katrain.gui.widgets import RankGraph from katrain.gui.widgets.progress_loader import ProgressLoader @@ -313,6 +315,16 @@ class ConfigAIPopup(QuickConfigGui): self.build_ai_options() self.ai_select.bind(text=self.build_ai_options) + def estimate_rank_from_options(self,*_args): + strategy = self.ai_select.selected[1] + options = self.collect_properties(self) # [strategy] + print(strategy,options) + prefix = f"ai/{strategy}/" + options = {k[len(prefix):]:v for k,v in options.items() if k.startswith(prefix)} + print(options) + dan_rank, model_based = ai_rank_estimation(strategy, options) + self.estimated_rank_label.text = ('' if model_based else '~') + RankGraph.rank_label(dan_rank) + def build_ai_options(self, *_args): strategy = self.ai_select.selected[1] mode_settings = self.katrain.config(f"ai/{strategy}") @@ -325,6 +337,7 @@ class ConfigAIPopup(QuickConfigGui): if values == "bool": widget = LabelledCheckBox(input_property=f"ai/{strategy}/{k}") widget.active = v + widget.bind(active=self.estimate_rank_from_options) else: if isinstance(values[0], Tuple): # with descriptions, possibly language-specific fixed_values = [(v, re.sub(r"\[(.*?)\]", lambda m: i18n._(m[1]), l)) for v, l in values] @@ -332,6 +345,7 @@ class ConfigAIPopup(QuickConfigGui): fixed_values = [(v, str(v)) for v in values] widget = LabelledSelectionSlider(values=fixed_values, input_property=f"ai/{strategy}/{k}") widget.set_value(v) + widget.textbox.bind(text=self.estimate_rank_from_options) self.options_grid.add_widget(wrap_anchor(widget)) else: self.options_grid.add_widget( @@ -339,6 +353,7 @@ class ConfigAIPopup(QuickConfigGui): ) for _ in range((self.max_options - len(mode_settings)) * 2): self.options_grid.add_widget(Label(size_hint_x=None)) + Clock.schedule_once(self.estimate_rank_from_options) class ConfigPopup(QuickConfigGui): diff --git a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po index 48ca4f8..36f86a4 100644 --- a/katrain/i18n/locales/en/LC_MESSAGES/katrain.po +++ b/katrain/i18n/locales/en/LC_MESSAGES/katrain.po @@ -620,6 +620,10 @@ msgstr "" "one. Stronger settings select the best move from a larger selection. Since " "there is no 0 kyu/dan, 3 dan = -2 kyu." +#. in AI settings +msgid "estimated strength" +msgstr "Estimated Strength" + #. button in general settings for downloading models msgid "download models button" msgstr "Download Models" diff --git a/katrain/popups.kv b/katrain/popups.kv index 8cd339c..97e73b7 100644 --- a/katrain/popups.kv +++ b/katrain/popups.kv @@ -410,6 +410,7 @@ : options_grid: options_grid help_label: help_label + estimated_rank_label: estimated_rank_label ai_select: ai_select BoxLayout: size_hint: 1,1 @@ -420,6 +421,17 @@ I18NSpinner: size_hint: 0.8, 0.5 id: ai_select + BoxLayout: + size_hint: 1,1 + DescriptionLabel: + font_size: DESC_FONT_SIZE + size_hint: 1, 1 + text: i18n._('estimated strength') + DescriptionLabel: + size_hint: 0.8, 1 + font_size: DESC_FONT_SIZE + id: estimated_rank_label + text: '?' + i18n._('strength:kyu') SmallDescriptionLabel: id: help_label size_hint: 1,1.25 diff --git a/tests/data/test.gib b/tests/data/test.gib new file mode 100644 index 0000000..8405b20 --- /dev/null +++ b/tests/data/test.gib @@ -0,0 +1,271 @@ +\HS +\[GIBOKIND=Global\] +\[TYPE=0\] +\[SZAUDIO=0\] +\[GAMECONDITION=Even : Black 6.5 Dum\] +\[GAMETIME=Time limit 10minute : 30 second countdown 3 time\] +\[GAMERESULT=white wins by time\] +\[GAMEZIPSU=0\] +\[GAMEDUM=0\] +\[GAMEGONGJE=65\] +\[GAMETOTALNUM=232\] +\[GAMENAME=�°��� �뱹\] +\[GAMEDATE=2020- 6-14- 1-26-14\] +\[GAMEPLACE=Ÿ���� �ٵ�\] +\[GAMELECNAME=\] +\[GAMEWHITENAME=wildsim1 (2D)\] +\[GAMEWHITELEVEL=19\] +\[GAMEWHITENICK=wildsim1\] +\[GAMEWHITECOUNTRY=0\] +\[GAMEWAVATA=60011\] +\[GAMEWIMAGE=\] +\[GAMEBLACKNAME=kim (2D)\] +\[GAMEBLACKLEVEL=19\] +\[GAMEBLACKNICK=kim\] +\[GAMEBLACKCOUNTRY=3\] +\[GAMEBAVATA=114\] +\[GAMEBIMAGE=\] +\[GAMECOMMENT=\] +\[GAMEINFOMAIN=GBKIND:3,GTYPE:0,GCDT:0,GTIME:600-30-3,GRLT:8,ZIPSU:0,DUM:0,GONGJE:65,TCNT:232,AUSZ:0\] +\[GAMEINFOSUB=GNAMEF:0,GPLCF:0,GNAME:�°��� �뱹,GDATE:2020- 6-14- 1-26-14,GPLC:Ÿ���� �ٵ�,GCMT:\] +\[WUSERINFO=WID:楷摬楳ㅭ,WLV:19,WNICK:楷摬楳ㅭ,WNCD:0,WAID:60011,WIMG:\] +\[BUSERINFO=BID:楫獭慳浥,BLV:19,BNICK:楫獭慳浥,BNCD:3,BAID:114,BIMG:\] +\[GAMETAG=S0,R0,D0,G65,W8,Z0,T30-3-600, C2020:06:14:01:26, I:楷摬楳ㅭ,L:19,M:楫獭慳浥,N:19,A:楷摬楳ㅭ,B:楫獭慳浥,J:0,K:3\] +\HE +\GS +2 5 0 +233 0 &4 +INI 0 1 0 &4 +STO 0 2 1 15 3 +STO 0 3 2 3 15 +STO 0 4 1 16 15 +STO 0 5 2 13 2 +STO 0 6 1 3 3 +STO 0 7 2 2 5 +STO 0 8 1 5 2 +STO 0 9 2 1 3 +STO 0 10 1 2 2 +STO 0 11 2 15 1 +STO 0 12 1 14 2 +STO 0 13 2 14 1 +STO 0 14 1 13 3 +STO 0 15 2 12 2 +STO 0 16 1 12 3 +STO 0 17 2 11 2 +STO 0 18 1 13 16 +STO 0 19 2 16 9 +STO 0 20 1 16 7 +STO 0 21 2 16 12 +STO 0 22 1 2 13 +STO 0 23 2 2 11 +STO 0 24 1 2 16 +STO 0 25 2 3 16 +STO 0 26 1 2 15 +STO 0 27 2 3 14 +STO 0 28 1 2 14 +STO 0 29 2 4 12 +STO 0 30 1 9 15 +STO 0 31 2 8 16 +STO 0 32 1 8 15 +STO 0 33 2 9 16 +STO 0 34 1 10 16 +STO 0 35 2 10 17 +STO 0 36 1 11 16 +STO 0 37 2 11 17 +STO 0 38 1 7 16 +STO 0 39 2 7 17 +STO 0 40 1 6 17 +STO 0 41 2 8 17 +STO 0 42 1 6 15 +STO 0 43 2 5 16 +STO 0 44 1 6 16 +STO 0 45 2 12 16 +STO 0 46 1 12 15 +STO 0 47 2 12 17 +STO 0 48 1 14 15 +STO 0 49 2 13 17 +STO 0 50 1 14 17 +STO 0 51 2 13 14 +STO 0 52 1 13 15 +STO 0 53 2 2 17 +STO 0 54 1 1 17 +STO 0 55 2 3 17 +STO 0 56 1 7 12 +STO 0 57 2 1 18 +STO 0 58 1 1 12 +STO 0 59 2 1 11 +STO 0 60 1 0 17 +STO 0 61 2 2 12 +STO 0 62 1 0 13 +STO 0 63 2 6 13 +STO 0 64 1 14 12 +STO 0 65 2 17 14 +STO 0 66 1 7 13 +STO 0 67 2 17 15 +STO 0 68 1 16 16 +STO 0 69 2 15 8 +STO 0 70 1 15 11 +STO 0 71 2 16 11 +STO 0 72 1 6 12 +STO 0 73 2 6 14 +STO 0 74 1 7 14 +STO 0 75 2 5 15 +STO 0 76 1 4 11 +STO 0 77 2 5 12 +STO 0 78 1 5 11 +STO 0 79 2 3 11 +STO 0 80 1 3 13 +STO 0 81 2 4 13 +STO 0 82 1 3 12 +STO 0 83 2 5 14 +STO 0 84 1 15 7 +STO 0 85 2 2 8 +STO 0 86 1 14 8 +STO 0 87 2 15 10 +STO 0 88 1 14 10 +STO 0 89 2 14 9 +STO 0 90 1 17 10 +STO 0 91 2 13 10 +STO 0 92 1 14 11 +STO 0 93 2 13 8 +STO 0 94 1 16 10 +STO 0 95 2 15 9 +STO 0 96 1 17 9 +STO 0 97 2 17 16 +STO 0 98 1 16 13 +STO 0 99 2 17 13 +STO 0 100 1 16 14 +STO 0 101 2 17 11 +STO 0 102 1 14 7 +STO 0 103 2 13 7 +STO 0 104 1 13 6 +STO 0 105 2 13 9 +STO 0 106 1 16 8 +STO 0 107 2 11 7 +STO 0 108 1 12 6 +STO 0 109 2 10 10 +STO 0 110 1 12 7 +STO 0 111 2 11 8 +STO 0 112 1 12 8 +STO 0 113 2 11 9 +STO 0 114 1 12 11 +STO 0 115 2 12 10 +STO 0 116 1 11 11 +STO 0 117 2 16 2 +STO 0 118 1 16 3 +STO 0 119 2 17 2 +STO 0 120 1 17 3 +STO 0 121 2 8 10 +STO 0 122 1 11 10 +STO 0 123 2 12 9 +STO 0 124 1 10 9 +STO 0 125 2 9 9 +STO 0 126 1 10 8 +STO 0 127 2 9 7 +STO 0 128 1 10 11 +STO 0 129 2 9 10 +STO 0 130 1 3 10 +STO 0 131 2 2 10 +STO 0 132 1 3 9 +STO 0 133 2 2 9 +STO 0 134 1 8 8 +STO 0 135 2 9 8 +STO 0 136 1 8 2 +STO 0 137 2 1 2 +STO 0 138 1 1 1 +STO 0 139 2 2 3 +STO 0 140 1 3 2 +STO 0 141 2 3 4 +STO 0 142 1 4 4 +STO 0 143 2 4 5 +STO 0 144 1 5 4 +STO 0 145 2 5 5 +STO 0 146 1 6 4 +STO 0 147 2 6 5 +STO 0 148 1 7 4 +STO 0 149 2 6 11 +STO 0 150 1 7 11 +STO 0 151 2 6 10 +STO 0 152 1 7 10 +STO 0 153 2 7 9 +STO 0 154 1 6 9 +STO 0 155 2 5 10 +STO 0 156 1 8 9 +STO 0 157 2 8 7 +STO 0 158 1 7 8 +STO 0 159 2 4 10 +STO 0 160 1 7 7 +STO 0 161 2 10 5 +STO 0 162 1 10 2 +STO 0 163 2 11 3 +STO 0 164 1 11 6 +STO 0 165 2 10 6 +STO 0 166 1 11 4 +STO 0 167 2 10 3 +STO 0 168 1 10 4 +STO 0 169 2 9 4 +STO 0 170 1 9 3 +STO 0 171 2 11 5 +STO 0 172 1 12 4 +STO 0 173 2 8 5 +STO 0 174 1 8 4 +STO 0 175 2 9 5 +STO 0 176 1 7 5 +STO 0 177 2 8 6 +STO 0 178 1 10 1 +STO 0 179 2 15 2 +STO 0 180 1 14 3 +STO 0 181 2 11 1 +STO 0 182 1 17 17 +STO 0 183 2 18 11 +STO 0 184 1 18 16 +STO 0 185 2 18 15 +STO 0 186 1 18 13 +STO 0 187 2 18 17 +STO 0 188 1 17 18 +STO 0 189 2 18 12 +STO 0 190 1 18 2 +STO 0 191 2 18 1 +STO 0 192 1 18 3 +STO 0 193 2 10 0 +STO 0 194 1 9 1 +STO 0 195 2 17 0 +STO 0 196 1 13 18 +STO 0 197 2 12 18 +STO 0 198 1 14 18 +STO 0 199 2 5 17 +STO 0 200 1 7 18 +STO 0 201 2 8 18 +STO 0 202 1 6 18 +STO 0 203 2 5 18 +STO 0 204 1 7 15 +STO 0 205 2 9 18 +STO 0 206 1 12 0 +STO 0 207 2 13 0 +STO 0 208 1 9 0 +STO 0 209 2 0 1 +STO 0 210 1 11 0 +STO 0 211 2 13 1 +STO 0 212 1 1 0 +STO 0 213 2 5 7 +STO 0 214 1 5 8 +STO 0 215 2 4 8 +STO 0 216 1 5 9 +STO 0 217 2 4 9 +STO 0 218 1 9 11 +STO 0 219 2 8 11 +STO 0 220 1 8 12 +STO 0 221 2 12 5 +STO 0 222 1 13 5 +STO 0 223 2 18 9 +STO 0 224 1 18 8 +STO 0 225 2 18 10 +STO 0 226 1 17 8 +STO 0 227 2 13 11 +STO 0 228 1 13 12 +STO 0 229 2 10 7 +STO 0 230 1 15 12 +STO 0 231 2 18 14 +STO 0 232 1 6 7 +STO 0 233 2 6 6 +\GE diff --git a/tests/test_parser.py b/tests/test_parser.py index 707f53a..d84f77d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -5,7 +5,7 @@ from katrain.core.sgf_parser import SGF, SGFNode def test_simple(): input_sgf = "(;GM[1]FF[4]SZ[19]DT[2020-04-12]AB[dd][dj];B[dp];W[pp];B[pj])" - root = SGF.parse(input_sgf) + root = SGF.parse_sgf(input_sgf) assert "4" == root.get_property("FF") assert root.get_property("XYZ") is None assert "dp" == root.children[0].get_property("B") @@ -14,13 +14,13 @@ def test_simple(): def test_branch(): input_sgf = "(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.43.3]KM[6.5]SZ[19]DT[2020-04-12]AB[dd][dj](;B[dp];W[pp](;B[pj])(;PL[B]AW[jp]C[sdfdsfdsf]))(;B[pd]))" - root = SGF.parse(input_sgf) + root = SGF.parse_sgf(input_sgf) assert input_sgf == root.sgf() def test_dragon_weirdness(): # dragon go server has weird line breaks input_sgf = "\n(\n\n;\nGM[1]\nFF[4]\nCA[UTF-8]AP[Sabaki:0.43.3]KM[6.5]SZ[19]DT[2020-04-12]AB[dd]\n[dj]\n(\n;\nB[dp]\n;\nW[pp]\n(\n;\nB[pj]\n)\n(\n;\nPL[B]\nAW[jp]\nC[sdfdsfdsf]\n)\n)\n(\n;\nB[pd]\n)\n)\n" - root = SGF.parse(input_sgf) + root = SGF.parse_sgf(input_sgf) assert input_sgf.replace("\n", "") == root.sgf() @@ -29,7 +29,7 @@ def test_weird_escape(): [ or \\] ])""" - root = SGF.parse(input_sgf) + root = SGF.parse_sgf(input_sgf) assert input_sgf == root.sgf() @@ -44,7 +44,7 @@ def test_backslash_escape(): node = SGFNode(properties={"C1": nasty_string}) node.set_property("C2", c2) assert "(;C1[[\\]\\]\\\\]C2[\\]][\\\\])" == node.sgf() - assert {"C1": [nasty_string], "C2": c2} == SGF.parse(node.sgf()).properties + assert {"C1": [nasty_string], "C2": c2} == SGF.parse_sgf(node.sgf()).properties def test_alphago(): @@ -94,3 +94,13 @@ def test_pandanet(): def test_ogs(): file = os.path.join(os.path.dirname(__file__), "data/ogs.sgf") tree = SGF.parse_file(file) + + +def test_gibo(): + file = os.path.join(os.path.dirname(__file__), "data/test.gib") + root = SGF.parse_file(file) + assert {'PW': ['wildsim1'], 'WR': ['2D'], 'PB': ['kim'], 'BR': ['2D'], 'RE': ['W+T'], 'KM': [6.5], 'DT': ['2020-06-14']} == root.properties + assert "pp" == root.children[0].get_property("B") + + +