* edit katrain/__main__.py

* delay / 0

* edit katrain/core/constants.py

* edit ENGINE.md

* fix bug

* fix bug

* fix scroll variations

* edit katrain/core/sgf_parser.py, edit tests/test_parser.py and 4 other changes

* fix touchscreen

* antimirror

* openssl dir try

* openssl dir try

* fix log

* openssl

* auto version in osx

* edit .github/workflows/osxbuild.yaml

* edit .github/workflows/osxbuild.yaml

* edit katrain/KataGo/katago

* better pondering

* new downloads

* middle click add variation

* change middle click pv

* more tweaks

* black

Co-authored-by: Sander Land <sander.land@futurefacts.nl>
Co-authored-by: Sander Land <sander.land@cognite.com>
Co-authored-by: Sander Land <sander@chatdesk.com>
This commit is contained in:
authored and GitHub committed 2021-10-31 19:54:19 +01:00
1 parent f368f1dcfa
commit 6005579c53
38 files changed
+263 -100

No files matched your search

+11 -1
View File
@@ -10,6 +10,7 @@ from katrain.core.constants import (
AI_INFLUENCE,
AI_INFLUENCE_ELO_GRID,
AI_JIGO,
AI_ANTIMIRROR,
AI_LOCAL,
AI_LOCAL_ELO_GRID,
AI_PICK,
@@ -264,6 +265,11 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
if not handicap_analysis:
game.katrain.log("Error getting handicap-based move", OUTPUT_ERROR)
ai_mode = AI_DEFAULT
elif ai_mode == AI_ANTIMIRROR:
antimirror_analysis = request_ai_analysis(game, cn, {"antiMirror": True})
if not antimirror_analysis:
game.katrain.log("Error getting antimirror move", OUTPUT_ERROR)
ai_mode = AI_DEFAULT
while not cn.analysis_complete:
time.sleep(0.01)
@@ -388,6 +394,8 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
candidate_ai_moves = cn.candidate_moves
if ai_mode == AI_HANDICAP:
candidate_ai_moves = handicap_analysis["moveInfos"]
elif ai_mode == AI_ANTIMIRROR:
candidate_ai_moves = antimirror_analysis["moveInfos"]
top_cand = Move.from_gtp(candidate_ai_moves[0]["move"], player=cn.next_player)
if top_cand.is_pass and ai_mode not in [
@@ -492,12 +500,14 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
else:
raise (Exception("No moves found - are you using an older KataGo with no per-move ownership info?"))
else:
if ai_mode not in [AI_DEFAULT, AI_HANDICAP]:
if ai_mode not in [AI_DEFAULT, AI_HANDICAP, AI_ANTIMIRROR]:
game.katrain.log(f"Unknown AI mode {ai_mode} or policy missing, using default.", OUTPUT_INFO)
ai_thoughts += f"Strategy {ai_mode} not found or unexpected fallback."
aimove = top_cand
if ai_mode == AI_HANDICAP:
ai_thoughts += f"Handicap strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. PDA based score {cn.format_score(handicap_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(handicap_analysis['rootInfo']['winrate'])}"
if ai_mode == AI_ANTIMIRROR:
ai_thoughts += f"AntiMirror strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move. antiMirror based score {cn.format_score(antimirror_analysis['rootInfo']['scoreLead'])} and win rate {cn.format_winrate(antimirror_analysis['rootInfo']['winrate'])}"
else:
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
game.katrain.log(f"AI thoughts: {ai_thoughts}", OUTPUT_DEBUG)
+13 -4
View File
@@ -54,6 +54,13 @@ class Player:
return f"{self.player_type} ({self.player_subtype})"
def parse_version(s):
parts = [int(p) for p in s.split(".")]
while len(parts) < 3:
parts.append(0)
return parts
class KaTrainBase:
USER_CONFIG_FILE = os.path.expanduser(os.path.join(DATA_FOLDER, "config.json"))
PACKAGE_CONFIG_FILE = "katrain/config.json"
@@ -101,11 +108,13 @@ class KaTrainBase:
self.log(f"Copied package config to local file {config_file}", OUTPUT_INFO)
else: # user file exists
try:
version = JsonStore(user_config_file).get("general")["version"]
version_str = JsonStore(user_config_file).get("general")["version"]
version = parse_version(version_str)
except Exception: # noqa E722 broken file etc
version = "0.0.0"
if version < CONFIG_MIN_VERSION:
backup = user_config_file + f".{version}.backup"
version_str = "0.0.0"
version = [0, 0, 0]
if version < parse_version(CONFIG_MIN_VERSION):
backup = f"{user_config_file}.{version_str}.backup"
shutil.copyfile(user_config_file, backup)
shutil.copyfile(package_config_file, user_config_file)
self.log(
+9 -6
View File
@@ -1,7 +1,7 @@
PROGRAM_NAME = "KaTrain"
VERSION = "1.9.3"
VERSION = "1.10.0"
HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.9.0" # keep config files from this version
CONFIG_MIN_VERSION = "1.10.0" # keep config files from this version
ANALYSIS_FORMAT_VERSION = "1.0"
DATA_FOLDER = "~/.katrain"
@@ -42,6 +42,7 @@ AI_HANDICAP = "ai:handicap"
AI_SCORELOSS = "ai:scoreloss"
AI_WEIGHTED = "ai:p:weighted"
AI_JIGO = "ai:jigo"
AI_ANTIMIRROR = "ai:antimirror"
AI_POLICY = "ai:policy"
AI_PICK = "ai:p:pick"
AI_LOCAL = "ai:p:local"
@@ -54,7 +55,7 @@ AI_SETTLE_STONES = "ai:settle"
AI_CONFIG_DEFAULT = AI_RANK
AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_JIGO] # AI_SETTLE_STONES
AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_SIMPLE_OWNERSHIP, AI_JIGO, AI_ANTIMIRROR]
AI_STRATEGIES_PICK = [AI_PICK, AI_LOCAL, AI_TENUKI, AI_INFLUENCE, AI_TERRITORY, AI_RANK]
AI_STRATEGIES_POLICY = [AI_WEIGHTED, AI_POLICY] + AI_STRATEGIES_PICK
AI_STRATEGIES = AI_STRATEGIES_ENGINE + AI_STRATEGIES_POLICY
@@ -63,20 +64,21 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_RANK,
AI_HANDICAP,
AI_SIMPLE_OWNERSHIP,
# AI_SETTLE_STONES,
AI_SCORELOSS,
AI_POLICY,
AI_WEIGHTED,
AI_JIGO,
AI_ANTIMIRROR,
AI_PICK,
AI_LOCAL,
AI_TENUKI,
AI_TERRITORY,
AI_INFLUENCE,
AI_JIGO,
]
AI_STRENGTH = { # dan ranks, backup if model is missing. TODO: remove some?
AI_DEFAULT: 9,
AI_ANTIMIRROR: 9,
AI_POLICY: 5,
AI_JIGO: float("nan"),
AI_SCORELOSS: -4,
@@ -260,7 +262,8 @@ TOP_MOVE_OPTIONS = [
# TOP_MOVE_UTILITY,
# TOP_MOVE_UTILITYLCB
]
REPORT_DT = 0.25
REPORT_DT = 1
PONDERING_REPORT_DT = 0.25
SGF_INTERNAL_COMMENTS_MARKER = "\u3164\u200b"
SGF_SEPARATOR_MARKER = "\u3164\u3164"
+3 -1
View File
@@ -243,7 +243,9 @@ class KataGoContributeEngine(BaseEngine):
game_properties["RU"] = json.dumps(analysis["rules"])
game_properties["PB"] = analysis["blackPlayer"]
game_properties["PW"] = analysis["whitePlayer"]
current_game = BaseGame(self.katrain, game_properties=game_properties)
current_game = BaseGame(
self.katrain, game_properties=game_properties, bypass_config=True
)
self.active_games[game_id] = current_game
last_node = current_game.sync_branch(
[Move.from_gtp(coord, pl) for pl, coord in analysis["moves"]]
+51 -7
View File
@@ -19,6 +19,7 @@ from katrain.core.constants import (
OUTPUT_KATAGO_STDERR,
DATA_FOLDER,
KATAGO_EXCEPTION,
PONDERING_REPORT_DT,
)
from katrain.core.game_node import GameNode
from katrain.core.lang import i18n
@@ -93,11 +94,14 @@ class BaseEngine: # some common elements between analysis and contribute engine
class KataGoEngine(BaseEngine):
"""Starts and communicates with the KataGO analysis engine"""
PONDER_KEY = "_kt_continuous"
def __init__(self, katrain, config):
super().__init__(katrain, config)
self.allow_recovery = self.config.get("allow_recovery", True) # if false, don't give popups
self.queries = {} # outstanding query id -> start time and callback
self.ponder_query = None
self.query_counter = 0
self.katago_process = None
self.base_priority = 0
@@ -166,6 +170,7 @@ class KataGoEngine(BaseEngine):
with self.thread_lock:
self.write_queue = queue.Queue()
self.terminate_queries(only_for_node=None, lock=False)
self.ponder_query = None
self.queries = {}
def terminate_queries(self, only_for_node=None, lock=True):
@@ -176,10 +181,19 @@ class KataGoEngine(BaseEngine):
if only_for_node is None or only_for_node is node:
self.terminate_query(query_id)
def terminate_query(self, query_id):
def stop_pondering(self):
pq = self.ponder_query
if pq:
self.terminate_query(pq["id"], ignore_further_results=False)
self.ponder_query = None
def terminate_query(self, query_id, ignore_further_results=True):
self.katrain.log(f"Terminating query {query_id}", OUTPUT_DEBUG)
if query_id is not None:
self.send_query({"action": "terminate", "terminateId": query_id}, None, None)
self.queries.pop(query_id, None)
if ignore_further_results:
self.queries.pop(query_id, None)
def restart(self):
self.queries = {}
@@ -269,9 +283,10 @@ class KataGoEngine(BaseEngine):
continue
query_id = analysis["id"]
if query_id not in self.queries:
self.katrain.log(
f"Query result {query_id} discarded -- recent new game or node reset?", OUTPUT_DEBUG
)
if analysis.get("action") != "terminate":
self.katrain.log(
f"Query result {query_id} discarded -- recent new game or node reset?", OUTPUT_DEBUG
)
continue
callback, error_callback, start_time, next_move, _ = self.queries[query_id]
if "error" in analysis:
@@ -314,12 +329,38 @@ class KataGoEngine(BaseEngine):
except queue.Empty:
continue
with self.thread_lock:
self.katrain.log(f"GOT QUERY: {query}", OUTPUT_DEBUG)
if "id" not in query:
self.query_counter += 1
query["id"] = f"QUERY:{str(self.query_counter)}"
if query.get("action") != "terminate":
ponder = query.pop(self.PONDER_KEY, False)
if ponder: # handle pondering in here to be in lock and such
pq = self.ponder_query or {}
# basically we handle pondering by just asking for these queries a lot and ignoring duplicates
# when a different ponder query comes in, e.g. due to selecting a roi or different node, switch
differences = {
k: (pq.get(k), query.get(k))
for k in (query.keys() | pq.keys()) - {"id", "maxVisits", "reportDuringSearchEvery"}
if pq.get(k) != query.get(k)
}
if differences:
# TODO:remove
self.katrain.log(f"Found differences in ponder check: {differences}", OUTPUT_EXTRA_DEBUG)
self.stop_pondering()
query["maxVisits"] = 1_000_000
query["reportDuringSearchEvery"] = PONDERING_REPORT_DT
self.ponder_query = query
else:
# TODO:remove
self.katrain.log("Found no differences in ponder check, skipping", OUTPUT_EXTRA_DEBUG)
continue
terminate = query.get("action") == "terminate"
if not terminate:
self.queries[query["id"]] = (callback, error_callback, time.time(), next_move, node)
self.katrain.log(f"Sending query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
tag = "ponder " if ponder else ("terminate " if terminate else "")
self.katrain.log(f"Sending {tag}query {query['id']}: {json.dumps(query)}", OUTPUT_DEBUG)
try:
self.katago_process.stdin.write((json.dumps(query) + "\n").encode())
self.katago_process.stdin.flush()
@@ -341,6 +382,7 @@ class KataGoEngine(BaseEngine):
find_alternatives: bool = False,
region_of_interest: Optional[List] = None,
priority: int = 0,
ponder=False, # infinite visits, cancellable
ownership: Optional[bool] = None,
next_move: Optional[GameNode] = None,
extra_settings: Optional[Dict] = None,
@@ -358,6 +400,7 @@ class KataGoEngine(BaseEngine):
moves.append(next_move)
if ownership is None:
ownership = self.config["_enable_ownership"] and not next_move
if visits is None:
visits = self.config["max_visits"]
if analyze_fast and self.config.get("fast_visits"):
@@ -411,6 +454,7 @@ class KataGoEngine(BaseEngine):
"initialPlayer": analysis_node.initial_player,
"moves": [[m.player, m.gtp()] for m in moves],
"overrideSettings": {**settings, **(extra_settings or {})},
self.PONDER_KEY: ponder,
}
if report_every is not None:
query["reportDuringSearchEvery"] = report_every
+27 -14
View File
@@ -53,6 +53,7 @@ class BaseGame:
move_tree: GameNode = None,
game_properties: Optional[Dict] = None,
sgf_filename=None,
bypass_config=False, # TODO: refactor?
):
self.katrain = katrain
self._lock = threading.Lock()
@@ -83,18 +84,24 @@ class BaseGame:
): # not really according to sgf, and not sure if still needed, last clause for fox
self.root.place_handicap_stones(handicap)
else:
board_size = katrain.config("game/size")
rules = katrain.config("game/rules")
self.komi = katrain.config("game/komi")
default_properties = {**Game.DEFAULT_PROPERTIES, "DT": self.game_id}
if not bypass_config:
default_properties.update(
{
"SZ": katrain.config("game/size"),
"KM": katrain.config("game/komi"),
"RU": katrain.config("game/rules"),
}
)
self.root = GameNode(
properties={
**Game.DEFAULT_PROPERTIES,
**{"SZ": board_size, "KM": self.komi, "DT": self.game_id, "RU": rules},
**default_properties,
**(game_properties or {}),
}
)
self.komi = self.root.komi
handicap = katrain.config("game/handicap")
if handicap:
if not bypass_config and handicap:
self.root.place_handicap_stones(handicap)
if not self.root.get_property("RU"): # if rules missing in sgf, inherit current
@@ -558,22 +565,27 @@ class Game(BaseGame):
cn = self.current_node
if mode == "stop":
self.katrain.pondering = False
for e in set(self.engines.values()):
e.stop_pondering()
e.terminate_queries()
self.katrain.idle_analysis = False
return
engine = self.engines[cn.next_player]
Clock.schedule_once(self.katrain.analysis_controls.hints.activate, 0)
if mode == "ponder":
cn.analyze(
engine,
ponder=True,
priority=PRIORITY_EXTRA_ANALYSIS,
region_of_interest=self.region_of_interest,
time_limit=False,
)
return
if mode == "extra":
if kwargs.get("continuous", False):
visits = min(
1_000_000_000, max(engine.config["max_visits"], math.ceil(cn.analysis_visits_requested * 1.25))
)
else:
visits = cn.analysis_visits_requested + engine.config["max_visits"]
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
visits = cn.analysis_visits_requested + engine.config["max_visits"]
self.katrain.controls.set_status(i18n._("extra analysis").format(visits=visits), STATUS_ANALYSIS)
cn.analyze(
engine,
@@ -583,6 +595,7 @@ class Game(BaseGame):
time_limit=False,
)
return
if mode == "game":
nodes = self.root.nodes_in_tree
only_mistakes = kwargs.get("mistakes_only", False)
+6
View File
@@ -187,6 +187,7 @@ class GameNode(SGFNode):
engine,
priority=PRIORITY_DEFAULT,
visits=None,
ponder=False,
time_limit=True,
refine_move=None,
analyze_fast=False,
@@ -201,6 +202,7 @@ class GameNode(SGFNode):
),
priority=priority,
visits=visits,
ponder=ponder,
analyze_fast=analyze_fast,
time_limit=time_limit,
next_move=refine_move,
@@ -278,6 +280,10 @@ class GameNode(SGFNode):
def analysis_complete(self):
return self.analysis["completed"] and self.analysis["root"] is not None
@property
def root_visits(self):
return ((self.analysis or {}).get("root") or {}).get("visits", 0)
@property
def score(self) -> Optional[float]:
if self.analysis_exists:
+26 -10
View File
@@ -45,7 +45,7 @@ class Move:
self.coords = coords
def __repr__(self):
return f"Move({self.player}{self.gtp()})"
return f"Move({self.player or ''}{self.gtp()})"
def __eq__(self, other):
return self.coords == other.coords and self.player == other.player
@@ -247,22 +247,38 @@ class SGFNode:
]
return self.moves_cache
def _expanded_placements(self, player):
sgf_pl = player if player is not None else "E" # AE
placements = self.get_list_property("A" + sgf_pl, [])
if not placements:
return []
to_be_expanded = [p for p in placements if ":" in p]
board_size = self.board_size
if to_be_expanded:
coords = {
Move.from_sgf(sgf_coord, player=player, board_size=board_size)
for sgf_coord in placements
if ":" not in sgf_coord
}
for p in to_be_expanded:
from_coord, to_coord = [Move.from_sgf(c, board_size=board_size) for c in p.split(":")[:2]]
for x in range(from_coord.coords[0], to_coord.coords[0] + 1):
for y in range(to_coord.coords[1], from_coord.coords[1] + 1): # sgf upside dn
if 0 <= x < board_size[0] and 0 <= y < board_size[1]:
coords.add(Move((x, y), player=player))
return list(coords)
else:
return [Move.from_sgf(sgf_coord, player=player, board_size=board_size) for sgf_coord in placements]
@property
def placements(self) -> List[Move]:
"""Returns all placements (AB/AW) in the node."""
return [
Move.from_sgf(sgf_coords, player=pl, board_size=self.board_size)
for pl in Move.PLAYERS
for sgf_coords in self.get_list_property("A" + pl, [])
]
return [coord for pl in Move.PLAYERS for coord in self._expanded_placements(pl)]
@property
def clear_placements(self) -> List[Move]:
"""Returns all AE clear square commends in the node."""
return [
Move.from_sgf(sgf_coords, player=None, board_size=self.board_size)
for sgf_coords in self.get_list_property("AE", [])
]
return self._expanded_placements(None)
@property
def move_with_placements(self) -> List[Move]: