rank model start and gibo file support

This commit is contained in:
Sander Land committed 2020-06-28 14:10:20 +02:00
1 parent 94fdd24260
commit 7a4a8e268b
10 files changed
+525 -52

No files matched your search

+5 -5
View File
@@ -325,7 +325,7 @@ class KaTrainGui(Screen, KaTrainBase):
self.controls.timer.paused = True self.controls.timer.paused = True
if not self.ai_settings_popup: if not self.ai_settings_popup:
self.ai_settings_popup = I18NPopup( 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__
self.ai_settings_popup.content.popup = self.ai_settings_popup self.ai_settings_popup.content.popup = self.ai_settings_popup
self.ai_settings_popup.open() self.ai_settings_popup.open()
@@ -366,7 +366,7 @@ class KaTrainGui(Screen, KaTrainBase):
def _do_output_sgf(self): def _do_output_sgf(self):
msg = self.game.write_sgf(self.config("general/sgf_save")) msg = self.game.write_sgf(self.config("general/sgf_save"))
self.log(msg, OUTPUT_INFO) self.log(msg, OUTPUT_INFO)
self.controls.set_status(msg) self.controls.set_status(msg,OUTPUT_INFO)
def load_sgf_from_clipboard(self): def load_sgf_from_clipboard(self):
clipboard = Clipboard.paste() 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) self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.",STATUS_ERROR)
return return
try: try:
move_tree = KaTrainSGF.parse(clipboard) move_tree = KaTrainSGF.parse_sgf(clipboard)
except Exception as exc: except Exception as exc:
self.controls.set_status( self.controls.set_status(
i18n._("Failed to import from clipboard").format(error=exc, contents=clipboard[:50]) i18n._("Failed to import from clipboard").format(error=exc, contents=clipboard[:50])
@@ -497,7 +497,7 @@ class KaTrainApp(MDApp):
Builder.load_file(kv_file) Builder.load_file(kv_file)
Window.bind(on_request_close=self.on_request_close) 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() self.gui = KaTrainGui()
Builder.load_file(popup_kv_file) Builder.load_file(popup_kv_file)
@@ -510,7 +510,7 @@ class KaTrainApp(MDApp):
self.gui.save_config() self.gui.save_config()
if self.gui.game: if self.gui.game:
self.gui.update_state() self.gui.update_state()
self.gui.controls.set_status("") self.gui.controls.set_status("",STATUS_INFO)
def webbrowser(self, site_key): def webbrowser(self, site_key):
websites = {"homepage": HOMEPAGE + "#manual", "support": HOMEPAGE + "#support"} websites = {"homepage": HOMEPAGE + "#manual", "support": HOMEPAGE + "#support"}
+16 -5
View File
@@ -2,7 +2,7 @@ import heapq
import math import math
import random import random
import time 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.utils import var_to_grid
from katrain.core.constants import ( from katrain.core.constants import (
@@ -22,11 +22,22 @@ from katrain.core.constants import (
AI_PICK, AI_PICK,
AI_RANK, AI_RANK,
AI_HANDICAP, AI_HANDICAP,
OUTPUT_ERROR, OUTPUT_ERROR, AI_STRENGTH,
) )
from katrain.core.game import Game, GameNode, Move 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]: 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.""" """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 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 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 error = False
analysis = None 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"] candidate_ai_moves = handicap_analysis["moveInfos"]
top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player) 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 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: else:
if ai_mode == AI_JIGO: if ai_mode == AI_JIGO:
sign = cn.player_sign(cn.next_player) sign = cn.player_sign(cn.next_player)
+13
View File
@@ -55,6 +55,19 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_JIGO, 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 = { AI_OPTION_VALUES = {
"kyu_rank": [(k, f"{k}[strength:kyu]") for k in range(15, 0, -1)] "kyu_rank": [(k, f"{k}[strength:kyu]") for k in range(15, 0, -1)]
+3 -30
View File
@@ -48,7 +48,7 @@ class Game:
self.komi = self.root.komi self.komi = self.root.komi
handicap = int(self.root.get_property("HA", 0)) handicap = int(self.root.get_property("HA", 0))
if handicap and not self.root.placements: if handicap and not self.root.placements:
self.place_handicap_stones(handicap) self.root.place_handicap_stones(handicap)
else: else:
board_size = katrain.config("game/size") board_size = katrain.config("game/size")
self.komi = katrain.config("game/komi") self.komi = katrain.config("game/komi")
@@ -61,7 +61,7 @@ class Game:
) )
handicap = katrain.config("game/handicap") handicap = katrain.config("game/handicap")
if handicap: if handicap:
self.place_handicap_stones(handicap) self.root.place_handicap_stones(handicap)
if not self.root.get_property("RU"): if not self.root.get_property("RU"):
self.root.set_property("RU", katrain.config("game/rules")) 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) ix = (ordered_children.index(cn) + len(ordered_children) + direction) % len(ordered_children)
self.set_current_node(ordered_children[ix]) 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 @property
def board_size(self): def board_size(self):
+171 -7
View File
@@ -1,4 +1,5 @@
import copy import copy
import math
import re import re
from collections import defaultdict from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
@@ -285,6 +286,37 @@ class SGFNode:
else: else:
return "W" 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: class SGF:
@@ -293,23 +325,31 @@ class SGF:
SGFPROP_PAT = re.compile(r"\s*(?:\(|\)|;|(\w+)((\s*\[([^\]\\]|\\.)*\])+))", flags=re.DOTALL) SGFPROP_PAT = re.compile(r"\s*(?:\(|\)|;|(\w+)((\s*\[([^\]\\]|\\.)*\])+))", flags=re.DOTALL)
@classmethod @classmethod
def parse(cls, input_str) -> SGFNode: def parse_sgf(cls, input_str) -> SGFNode:
"""Parse a string as SGF.""" """Parse a string as SGF."""
return cls(input_str).root return cls(input_str).root
@classmethod @classmethod
def parse_file(cls, filename, encoding=None) -> SGFNode: 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.""" """Parse a file as SGF, encoding will be detected if not given."""
with open(filename, "rb") as f: with open(filename, "rb") as f:
bin_contents = f.read() bin_contents = f.read()
if not encoding: if not encoding:
match = re.search(rb"CA\[(.*?)\]", bin_contents) if not is_gib:
if match: match = re.search(rb"CA\[(.*?)\]", bin_contents)
encoding = match[1].decode("ascii", errors="ignore") if match:
encoding = match[1].decode("ascii", errors="ignore")
else:
encoding = "ISO-8859-1" # default
else: else:
encoding = "ISO-8859-1" # default encoding = 'utf8' # ?
decoded = bin_contents.decode(encoding=encoding, errors="ignore") decoded = bin_contents.decode(encoding=encoding, errors="ignore")
return cls.parse(decoded) if is_gib:
return cls.parse_gib(decoded)
else: # sgf
return cls.parse_sgf(decoded)
def __init__(self, contents): def __init__(self, contents):
self.contents = contents self.contents = contents
@@ -341,3 +381,127 @@ class SGF:
if self.ix < len(self.contents): if self.ix < len(self.contents):
raise ParseError(f"Parse Error: unexpected character at {self.contents[self.ix:self.ix+25]}") raise ParseError(f"Parse Error: unexpected character at {self.contents[self.ix:self.ix+25]}")
raise ParseError("Parse Error: expected ')' at end of input.") 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
+15
View File
@@ -15,6 +15,7 @@ from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.selectioncontrol import MDCheckbox from kivymd.uix.selectioncontrol import MDCheckbox
from kivymd.uix.textfield import MDTextField from kivymd.uix.textfield import MDTextField
from katrain.core.ai import ai_rank_estimation
from katrain.core.constants import ( from katrain.core.constants import (
AI_CONFIG_DEFAULT, AI_CONFIG_DEFAULT,
AI_DEFAULT, AI_DEFAULT,
@@ -29,6 +30,7 @@ from katrain.core.lang import i18n
from katrain.core.utils import PATHS, find_package_resource from katrain.core.utils import PATHS, find_package_resource
from katrain.gui.kivyutils import BackgroundMixin, I18NSpinner from katrain.gui.kivyutils import BackgroundMixin, I18NSpinner
from katrain.gui.style import DEFAULT_FONT, EVAL_COLORS from katrain.gui.style import DEFAULT_FONT, EVAL_COLORS
from katrain.gui.widgets import RankGraph
from katrain.gui.widgets.progress_loader import ProgressLoader from katrain.gui.widgets.progress_loader import ProgressLoader
@@ -313,6 +315,16 @@ class ConfigAIPopup(QuickConfigGui):
self.build_ai_options() self.build_ai_options()
self.ai_select.bind(text=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): def build_ai_options(self, *_args):
strategy = self.ai_select.selected[1] strategy = self.ai_select.selected[1]
mode_settings = self.katrain.config(f"ai/{strategy}") mode_settings = self.katrain.config(f"ai/{strategy}")
@@ -325,6 +337,7 @@ class ConfigAIPopup(QuickConfigGui):
if values == "bool": if values == "bool":
widget = LabelledCheckBox(input_property=f"ai/{strategy}/{k}") widget = LabelledCheckBox(input_property=f"ai/{strategy}/{k}")
widget.active = v widget.active = v
widget.bind(active=self.estimate_rank_from_options)
else: else:
if isinstance(values[0], Tuple): # with descriptions, possibly language-specific 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] 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] fixed_values = [(v, str(v)) for v in values]
widget = LabelledSelectionSlider(values=fixed_values, input_property=f"ai/{strategy}/{k}") widget = LabelledSelectionSlider(values=fixed_values, input_property=f"ai/{strategy}/{k}")
widget.set_value(v) widget.set_value(v)
widget.textbox.bind(text=self.estimate_rank_from_options)
self.options_grid.add_widget(wrap_anchor(widget)) self.options_grid.add_widget(wrap_anchor(widget))
else: else:
self.options_grid.add_widget( self.options_grid.add_widget(
@@ -339,6 +353,7 @@ class ConfigAIPopup(QuickConfigGui):
) )
for _ in range((self.max_options - len(mode_settings)) * 2): for _ in range((self.max_options - len(mode_settings)) * 2):
self.options_grid.add_widget(Label(size_hint_x=None)) self.options_grid.add_widget(Label(size_hint_x=None))
Clock.schedule_once(self.estimate_rank_from_options)
class ConfigPopup(QuickConfigGui): class ConfigPopup(QuickConfigGui):
@@ -620,6 +620,10 @@ msgstr ""
"one. Stronger settings select the best move from a larger selection. Since " "one. Stronger settings select the best move from a larger selection. Since "
"there is no 0 kyu/dan, 3 dan = -2 kyu." "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 #. button in general settings for downloading models
msgid "download models button" msgid "download models button"
msgstr "Download Models" msgstr "Download Models"
+12
View File
@@ -410,6 +410,7 @@
<ConfigAIPopup>: <ConfigAIPopup>:
options_grid: options_grid options_grid: options_grid
help_label: help_label help_label: help_label
estimated_rank_label: estimated_rank_label
ai_select: ai_select ai_select: ai_select
BoxLayout: BoxLayout:
size_hint: 1,1 size_hint: 1,1
@@ -420,6 +421,17 @@
I18NSpinner: I18NSpinner:
size_hint: 0.8, 0.5 size_hint: 0.8, 0.5
id: ai_select 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: SmallDescriptionLabel:
id: help_label id: help_label
size_hint: 1,1.25 size_hint: 1,1.25
+271
View File
@@ -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
+15 -5
View File
@@ -5,7 +5,7 @@ from katrain.core.sgf_parser import SGF, SGFNode
def test_simple(): def test_simple():
input_sgf = "(;GM[1]FF[4]SZ[19]DT[2020-04-12]AB[dd][dj];B[dp];W[pp];B[pj])" 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 "4" == root.get_property("FF")
assert root.get_property("XYZ") is None assert root.get_property("XYZ") is None
assert "dp" == root.children[0].get_property("B") assert "dp" == root.children[0].get_property("B")
@@ -14,13 +14,13 @@ def test_simple():
def test_branch(): 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]))" 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() assert input_sgf == root.sgf()
def test_dragon_weirdness(): # dragon go server has weird line breaks 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" 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() assert input_sgf.replace("\n", "") == root.sgf()
@@ -29,7 +29,7 @@ def test_weird_escape():
[ [
or \\] or \\]
])""" ])"""
root = SGF.parse(input_sgf) root = SGF.parse_sgf(input_sgf)
assert input_sgf == root.sgf() assert input_sgf == root.sgf()
@@ -44,7 +44,7 @@ def test_backslash_escape():
node = SGFNode(properties={"C1": nasty_string}) node = SGFNode(properties={"C1": nasty_string})
node.set_property("C2", c2) node.set_property("C2", c2)
assert "(;C1[[\\]\\]\\\\]C2[\\]][\\\\])" == node.sgf() 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(): def test_alphago():
@@ -94,3 +94,13 @@ def test_pandanet():
def test_ogs(): def test_ogs():
file = os.path.join(os.path.dirname(__file__), "data/ogs.sgf") file = os.path.join(os.path.dirname(__file__), "data/ogs.sgf")
tree = SGF.parse_file(file) 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")