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
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"}
+16 -5
View File
@@ -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)
+13
View File
@@ -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)]
+3 -30
View File
@@ -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):
+171 -7
View File
@@ -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
+15
View File
@@ -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):
@@ -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"
+12
View File
@@ -410,6 +410,7 @@
<ConfigAIPopup>:
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
+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():
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")