refactor ai

This commit is contained in:
Sander Land committed 2020-06-04 22:03:41 +02:00
1 parent 5a67d3cc6f
commit a322980f79
9 files changed
+159 -184

No files matched your search

+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
pip3 install pytest wheel twine polib pip3 install pytest wheel twine polib
- name: Run tests - name: Run tests
run: pytest -v tests run: pytest -v -s tests
- name: Run I18N conversion - name: Run I18N conversion
run: python i18n.py run: python i18n.py
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
pip3 install pytest wheel polib pip3 install pytest wheel polib
- name: Run tests - name: Run tests
run: pytest tests run: pytest -v -s tests
- name: Run I18N conversion - name: Run I18N conversion
run: python i18n.py run: python i18n.py
+2 -2
View File
@@ -39,7 +39,7 @@ from kivy.lang import Builder
from kivy.resources import resource_add_path from kivy.resources import resource_add_path
from kivy.uix.popup import Popup from kivy.uix.popup import Popup
from kivy.uix.screenmanager import Screen from kivy.uix.screenmanager import Screen
from katrain.core.ai import ai_move from katrain.core.ai import generate_ai_move
from kivy.core.window import Window from kivy.core.window import Window
from katrain.core.lang import DEFAULT_LANGUAGE, i18n from katrain.core.lang import DEFAULT_LANGUAGE, i18n
@@ -231,7 +231,7 @@ class KaTrainGui(Screen, KaTrainBase):
mode = self.next_player_info.strategy mode = self.next_player_info.strategy
settings = self.config(f"ai/{mode}") settings = self.config(f"ai/{mode}")
if settings is not None: if settings is not None:
ai_move(self.game, mode, settings) generate_ai_move(self.game, mode, settings)
else: else:
self.log(f"AI Mode {mode} not found!", OUTPUT_ERROR) self.log(f"AI Mode {mode} not found!", OUTPUT_ERROR)
+4 -5
View File
@@ -1,7 +1,6 @@
{ {
"engine": { "engine": {
"katago": "", "katago": "",
"_hint_katago": "Path to your katago executable",
"model": "katrain/models/g170e-b15c192-s1672170752-d466197061.bin.gz", "model": "katrain/models/g170e-b15c192-s1672170752-d466197061.bin.gz",
"config": "katrain/KataGo/analysis_config.cfg", "config": "katrain/KataGo/analysis_config.cfg",
"threads": 12, "threads": 12,
@@ -9,7 +8,6 @@
"fast_visits": 50, "fast_visits": 50,
"max_time": 3.0, "max_time": 3.0,
"wide_root_noise": 0.0, "wide_root_noise": 0.0,
"_hint_wide_root_noise": "A higher value here (typically 0.05-0.1)\nmakes the analysis explore more moves\nat the cost of some strength.",
"_enable_ownership": true "_enable_ownership": true
}, },
"general": { "general": {
@@ -18,7 +16,7 @@
"anim_pv_time": 0.5, "anim_pv_time": 0.5,
"debug_level": 0, "debug_level": 0,
"lang": "en", "lang": "en",
"version": "1.1.2" "version": "1.2.0"
}, },
"timer": { "timer": {
"byo_length": 30, "byo_length": 30,
@@ -94,7 +92,8 @@
"pick_override": 0.95, "pick_override": 0.95,
"stddev": 1.5, "stddev": 1.5,
"pick_n": 15, "pick_n": 15,
"pick_frac": 0.0 "pick_frac": 0.0,
"endgame": 0.5
}, },
"ai:p:tenuki": { "ai:p:tenuki": {
"pick_override": 0.85, "pick_override": 0.85,
@@ -120,7 +119,7 @@
"endgame": 0.4 "endgame": 0.4
}, },
"ai:p:rank": { "ai:p:rank": {
"kyu": 4.0 "kyu_rank": 4.0
} }
} }
} }
+89 -141
View File
@@ -20,9 +20,8 @@ from katrain.core.constants import (
AI_TENUKI, AI_TENUKI,
AI_TERRITORY, AI_TERRITORY,
AI_PICK, AI_PICK,
AI_RANK, AI_RANK,
) )
from katrain.core.engine import EngineDiedException
from katrain.core.game import Game, GameNode, Move from katrain.core.game import Game, GameNode, Move
@@ -42,157 +41,115 @@ def fmt_moves(moves: List[Tuple[float, Move]]):
return ", ".join(f"{mv.gtp()} ({p:.2%})" for p, mv in moves) return ", ".join(f"{mv.gtp()} ({p:.2%})" for p, mv in moves)
def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]: def policy_weighted_move(policy_moves, lower_bound, weaken_fac):
lower_bound, weaken_fac = max(0, lower_bound), max(0.01, weaken_fac)
weighted_coords = [(pv, pv ** (1 / weaken_fac), move) for pv, move in policy_moves if pv > lower_bound and not move.is_pass]
if weighted_coords:
top = weighted_selection_without_replacement(weighted_coords, 1)[0]
ai_thoughts = f"Playing policy-weighted random move {top[2].gtp()} ({top[0]:.1%}) from {len(weighted_coords)} moves above lower_bound of {lower_bound:.1%}."
else:
top = policy_moves[0]
ai_thoughts = f"Playing top policy move because no non-pass move > above lower_bound of {lower_bound:.1%}."
return top[2], ai_thoughts
def generate_influence_territory_weights(ai_mode, ai_settings, policy_grid, size):
thr_line = ai_settings["threshold"] - 1 # zero-based
if ai_mode == AI_INFLUENCE:
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (max(0, thr_line - min(size[0] - 1 - x, x)) + max(0, thr_line - min(size[1] - 1 - y, y)))
else:
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (max(0, min(size[0] - 1 - x, x, size[1] - 1 - y, y) - thr_line))
weighted_coords = [(policy_grid[y][x] * weight(x, y), weight(x, y), x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0]
ai_thoughts = f"Generated weights for {ai_mode} according to weight factor {ai_settings['line_weight']} and distance from {thr_line + 1}th line. "
return weighted_coords, ai_thoughts
def generate_local_tenuki_weights(ai_mode, ai_settings, policy_grid, cn, size):
var = ai_settings["stddev"] ** 2
mx, my = cn.move.coords
weighted_coords = [(policy_grid[y][x], math.exp(-0.5 * ((x - mx) ** 2 + (y - my) ** 2) / var), x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0]
ai_thoughts = f"Generated weights based on one minus gaussian with variance {var} around coordinates {mx},{my}. "
if ai_mode == AI_TENUKI:
weighted_coords = [(p, 1 - w, x, y) for p, w, x, y in weighted_coords]
ai_thoughts = f"Generated weights based on one minus gaussian with variance {var} around coordinates {mx},{my}. "
return weighted_coords, ai_thoughts
def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]:
cn = game.current_node cn = game.current_node
while not cn.analysis_ready: while not cn.analysis_ready:
time.sleep(0.01) time.sleep(0.01)
engine = game.engines[cn.next_player] game.engines[cn.next_player].check_alive(exception_if_dead=True)
if engine.katago_process.poll() is not None: # TODO: clean up
raise EngineDiedException(f"Engine for {cn.next_player} ({engine.config}) died")
ai_thoughts = "" ai_thoughts = ""
if (ai_mode in AI_STRATEGIES_POLICY) and cn.policy: # pure policy based move if (ai_mode in AI_STRATEGIES_POLICY) and cn.policy: # pure policy based move
policy_moves = cn.policy_ranking policy_moves = cn.policy_ranking
pass_policy = cn.policy[-1] pass_policy = cn.policy[-1]
top_5_pass = any( # dont make it jump around for the last few sensible non pass moves
[polmove[1].is_pass for polmove in policy_moves[:5]] top_5_pass = any([polmove[1].is_pass for polmove in policy_moves[:5]])
) # dont make it jump around for the last few sensible non pass moves
size = game.board_size size = game.board_size
policy_grid = var_to_grid(cn.policy, size) # type: List[List[float]] policy_grid = var_to_grid(cn.policy, size) # type: List[List[float]]
top_policy_move = policy_moves[0][1] top_policy_move = policy_moves[0][1]
ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. " ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. "
len_legal_policy_moves = len([(pol, mv) for pol, mv in policy_moves if not mv.is_pass if pol > 0]) if (ai_mode == AI_POLICY and cn.depth <= ai_settings["opening_moves"]) or (ai_mode in [AI_LOCAL, AI_TENUKI] and not cn.move or cn.move.coords is None):
if ai_mode == AI_POLICY and cn.depth <= ai_settings["opening_moves"]:
ai_mode = AI_WEIGHTED ai_mode = AI_WEIGHTED
ai_thoughts += f"Switching to weighted strategy in the opening {int(ai_settings['opening_moves'])} moves. " ai_thoughts += f"Strategy override, using policy-weighted strategy instead. "
ai_settings = {"pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02} ai_settings = {"pick_override": 0.9, "weaken_fac": 1, "lower_bound": 0.02}
if ai_mode == AI_RANK:
ai_settings = {"pick_override": (0.8*(1-((size[0]*size[1])-len_legal_policy_moves)/(size[0]*size[1])*.5)), "kyu": ai_settings["kyu"] }
if top_5_pass: if top_5_pass:
aimove = top_policy_move aimove = top_policy_move
ai_thoughts += "Playing top one because one of them is pass." ai_thoughts += "Playing top one because one of them is pass."
elif ai_mode == AI_POLICY: elif ai_mode == AI_POLICY:
aimove = top_policy_move aimove = top_policy_move
ai_thoughts += f"Playing top policy move {aimove.gtp()}." ai_thoughts += f"Playing top policy move {aimove.gtp()}."
elif policy_moves[0][0] > ai_settings["pick_override"]: else: # weighted or pick-based
aimove = top_policy_move legal_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass and pol > 0]
ai_thoughts += ( board_squares = size[0] * size[1]
f"Top policy move has weight > {ai_settings['pick_override']:.1%}, so overriding other strategies." if ai_mode == AI_RANK: # calibrated, override from 0.8 at start to ~0.4 at full board
) override = 0.8 * (1 - 0.5 * (board_squares - len(legal_policy_moves)) / board_squares)
elif ai_mode == AI_WEIGHTED:
lower_bound = max(0, ai_settings["lower_bound"]) * 2 # compensate for first halving in loop
weaken_fac = max(0.01, ai_settings["weaken_fac"])
weighted_coords = []
while not weighted_coords and lower_bound > 1e-6: # fix edge case where no moves are > lb
lower_bound /= 2
weighted_coords = [
(policy_grid[y][x], policy_grid[y][x] ** (1 / weaken_fac), x, y)
for x in range(size[0])
for y in range(size[1])
if policy_grid[y][x] > lower_bound
]
top = weighted_selection_without_replacement(weighted_coords, 1)
if top:
best = top[0]
policy_value = best[0]
coords = best[2:]
else: else:
policy_value = pass_policy override = ai_settings["pick_override"]
coords = None
aimove = Move(coords, player=cn.next_player) # just take a random move by policy w/o noise
ai_thoughts += f"Playing policy-weighted random move {aimove.gtp()} ({policy_value:.1%})" + (
" because no other moves were found."
if not top
else f" because strategy is weighted (lower bound={lower_bound:.2%}, num moves > lb={len(weighted_coords)})."
)
elif ai_mode in AI_STRATEGIES_PICK:
legal_policy_moves = [(pol, mv) for pol, mv in policy_moves if not mv.is_pass if pol > 0]
if ai_mode!=AI_RANK:
n_moves = int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"])
if ai_mode in [AI_INFLUENCE, AI_TERRITORY]:
thr_line = ai_settings["threshold"] - 1 # zero-based if policy_moves[0][0] > override:
if cn.depth >= ai_settings["endgame"] * size[0] * size[1]:
weighted_coords = [
(policy_grid[y][x], 1, x, y)
for x in range(size[0])
for y in range(size[1])
if policy_grid[y][x] > 0
]
ai_thoughts += (
f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. "
)
else:
if ai_mode == AI_INFLUENCE:
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (
max(0, thr_line - min(size[0] - 1 - x, x)) + max(0, thr_line - min(size[1] - 1 - y, y))
)
else:
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (
max(0, min(size[0] - 1 - x, x, size[1] - 1 - y, y) - thr_line)
)
weighted_coords = [
(policy_grid[y][x] * weight(x, y), weight(x, y), x, y)
for x in range(size[0])
for y in range(size[1])
if policy_grid[y][x] > 0
]
ai_thoughts += f"Generated weights for {ai_mode} according to weight factor {ai_settings['line_weight']} and distance from {thr_line+1}th line. "
elif ai_mode in [AI_LOCAL, AI_TENUKI]:
var = ai_settings["stddev"] ** 2
if not cn.move or cn.move.coords is None:
weighted_coords = [(1, 1, *top_policy_move.coords)] # if "pick" in ai_mode -> even
ai_thoughts += f"No previous non-pass move, faking weights to play top policy move. "
else:
mx, my = cn.move.coords
weighted_coords = [
(policy_grid[y][x], math.exp(-0.5 * ((x - mx) ** 2 + (y - my) ** 2) / var), x, y)
for x in range(size[0])
for y in range(size[1])
if policy_grid[y][x] > 0
]
if ai_mode == AI_TENUKI:
if cn.depth < ai_settings["endgame"] * size[0] * size[1]:
weighted_coords = [(p, 1 - w, x, y) for p, w, x, y in weighted_coords]
ai_thoughts += f"Generated weights based on one minus gaussian with variance {var} around coordinates {mx},{my}. "
else:
weighted_coords = [(p, 1, x, y) for p, w, x, y in weighted_coords]
ai_thoughts += f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. "
else:
ai_thoughts += (
f"Generated weights based on gaussian with variance {var} around coordinates {mx},{my}. "
)
elif ai_mode == AI_PICK:
weighted_coords = [
(policy_grid[y][x], 1, x, y)
for x in range(size[0])
for y in range(size[1])
if policy_grid[y][x] > 0
]
elif ai_mode == AI_RANK:
n_moves = int(round((size[0]*size[1])/361*10**(-0.05737*ai_settings["kyu"] + 1.9482)))
weighted_coords = [
(policy_grid[y][x], 1, x, y)
for x in range(size[0])
for y in range(size[1])
if policy_grid[y][x] > 0
]
else:
raise ValueError(f"Unknown AI mode {ai_mode}")
pick_moves = weighted_selection_without_replacement(weighted_coords, n_moves)
ai_thoughts += f"Picked {min(n_moves,len(weighted_coords))} random moves according to weights. "
if pick_moves:
new_top = [(p, Move((x, y), player=cn.next_player)) for p, wt, x, y in heapq.nlargest(5, pick_moves)]
aimove = new_top[0][1]
ai_thoughts += f"Top 5 among these were {fmt_moves(new_top)} and picked top {aimove.gtp()}. "
if new_top[0][0] < pass_policy:
ai_thoughts += f"But found pass ({pass_policy:.2%} to be higher rated than {aimove.gtp()} ({new_top[0][0]:.2%}) so will play top policy move instead."
aimove = top_policy_move
else:
aimove = top_policy_move aimove = top_policy_move
ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}." ai_thoughts += f"Top policy move has weight > {override:.1%}, so overriding other strategies."
else: elif ai_mode == AI_WEIGHTED:
raise ValueError(f"Unknown AI mode {ai_mode}") aimove, ai_thoughts = policy_weighted_move(policy_moves, ai_settings["lower_bound"], ai_settings["weaken_fac"])
elif ai_mode in AI_STRATEGIES_PICK:
if ai_mode != AI_RANK:
n_moves = int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"])
else:
n_moves = int(round(board_squares / 361 * 10 ** (-0.05737 * ai_settings["kyu_rank"] + 1.9482)))
if ai_mode in [AI_INFLUENCE, AI_TERRITORY, AI_LOCAL, AI_TENUKI]:
if cn.depth > ai_settings["endgame"] * board_squares:
weighted_coords = [(pol, 1, *mv.coords) for pol, mv in legal_policy_moves]
x_ai_thoughts = f"Generated equal weights as move number >= {ai_settings['endgame'] * size[0] * size[1]}. "
elif ai_mode in [AI_INFLUENCE, AI_TERRITORY]:
weighted_coords, x_ai_thoughts = generate_influence_territory_weights(ai_mode, ai_settings, policy_grid, size)
else: # ai_mode in [AI_LOCAL, AI_TENUKI]
weighted_coords, x_ai_thoughts = generate_local_tenuki_weights(ai_mode, ai_settings, policy_grid, cn, size)
ai_thoughts += x_ai_thoughts
else: # ai_mode in [AI_PICK, AI_RANK]:
weighted_coords = [(policy_grid[y][x], 1, x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0]
pick_moves = weighted_selection_without_replacement(weighted_coords, n_moves)
ai_thoughts += f"Picked {min(n_moves,len(weighted_coords))} random moves according to weights. "
if pick_moves:
new_top = [(p, Move((x, y), player=cn.next_player)) for p, wt, x, y in heapq.nlargest(5, pick_moves)]
aimove = new_top[0][1]
ai_thoughts += f"Top 5 among these were {fmt_moves(new_top)} and picked top {aimove.gtp()}. "
if new_top[0][0] < pass_policy:
ai_thoughts += f"But found pass ({pass_policy:.2%} to be higher rated than {aimove.gtp()} ({new_top[0][0]:.2%}) so will play top policy move instead."
aimove = top_policy_move
else:
aimove = top_policy_move
ai_thoughts += f"Pick policy strategy {ai_mode} failed to find legal moves, so is playing top policy move {aimove.gtp()}."
else:
raise ValueError(f"Unknown Policy-based AI mode {ai_mode}")
else: # Engine based move else: # Engine based move
candidate_ai_moves = cn.candidate_moves candidate_ai_moves = cn.candidate_moves
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)
@@ -202,21 +159,12 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
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)
jigo_move = min( jigo_move = min(candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"]))
candidate_ai_moves, key=lambda move: abs(sign * move["scoreLead"] - ai_settings["target_score"])
)
aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player) aimove = Move.from_gtp(jigo_move["move"], player=cn.next_player)
ai_thoughts += f"Jigo strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} as closest to 0.5 point win" ai_thoughts += f"Jigo strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} as closest to 0.5 point win"
elif ai_mode == AI_SCORELOSS: elif ai_mode == AI_SCORELOSS:
c = ai_settings["strength"] c = ai_settings["strength"]
moves = [ moves = [(d["pointsLost"], math.exp(min(200, -c * max(0, d["pointsLost"]))), Move.from_gtp(d["move"], player=cn.next_player),) for d in candidate_ai_moves]
(
d["pointsLost"],
math.exp(min(200, -c * max(0, d["pointsLost"]))),
Move.from_gtp(d["move"], player=cn.next_player),
)
for d in candidate_ai_moves
]
topmove = weighted_selection_without_replacement(moves, 1)[0] topmove = weighted_selection_without_replacement(moves, 1)[0]
aimove = topmove[2] aimove = topmove[2]
ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights." ai_thoughts += f"ScoreLoss strategy found {len(candidate_ai_moves)} candidate moves (best {top_cand.gtp()}) and chose {aimove.gtp()} (weight {topmove[1]:.3f}, point loss {topmove[0]:.1f}) based on score weights."
+31 -28
View File
@@ -45,13 +45,13 @@ class KaTrainBase:
"""Settings, logging, and players functionality, so other classes like bots who need a katrain instance can be used without a GUI""" """Settings, logging, and players functionality, so other classes like bots who need a katrain instance can be used without a GUI"""
def __init__(self, **kwargs): def __init__(self, force_package_config=False,debug_level=0, **kwargs):
self.debug_level = 0 self.debug_level = debug_level
self.game = None self.game = None
self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level) self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level)
self.config_file = self._load_config() self.config_file = self._load_config(force_package_config=force_package_config)
self.debug_level = self.config("general/debug_level", OUTPUT_INFO) self.debug_level = debug_level or self.config("general/debug_level", OUTPUT_INFO)
Config.set("kivy", "log_level", "error") Config.set("kivy", "log_level", "error")
if self.debug_level >= OUTPUT_DEBUG: if self.debug_level >= OUTPUT_DEBUG:
@@ -68,37 +68,40 @@ class KaTrainBase:
elif self.debug_level >= level: elif self.debug_level >= level:
print(message) print(message)
def _load_config(self): def _load_config(self,force_package_config):
if len(sys.argv) > 1 and sys.argv[1].endswith(".json"): if len(sys.argv) > 1 and sys.argv[1].endswith(".json"):
config_file = os.path.abspath(sys.argv[1]) config_file = os.path.abspath(sys.argv[1])
self.log(f"Using command line config file {config_file}", OUTPUT_INFO) self.log(f"Using command line config file {config_file}", OUTPUT_INFO)
else: else:
user_config_file = find_package_resource(self.USER_CONFIG_FILE) user_config_file = find_package_resource(self.USER_CONFIG_FILE)
package_config_file = find_package_resource(self.PACKAGE_CONFIG_FILE) package_config_file = find_package_resource(self.PACKAGE_CONFIG_FILE)
try: if force_package_config:
if not os.path.exists(user_config_file):
os.makedirs(os.path.split(user_config_file)[0], exist_ok=True)
shutil.copyfile(package_config_file, user_config_file)
config_file = user_config_file
self.log(f"Copied package config to local file {config_file}", OUTPUT_INFO)
else: # user file exists
version = JsonStore(user_config_file, indent=4).get("general")["version"]
if version < CONFIG_MIN_VERSION:
backup = user_config_file + f".{version}.backup"
shutil.copyfile(user_config_file, backup)
shutil.copyfile(package_config_file, user_config_file)
self.log(
f"Copied package config file to {user_config_file} as user file is outdated (<{CONFIG_MIN_VERSION}). Old version stored as {backup}",
OUTPUT_INFO,
)
config_file = user_config_file
self.log(f"Using user config file {config_file}", OUTPUT_INFO)
except Exception as e:
config_file = package_config_file config_file = package_config_file
self.log( else:
f"Using package config file {config_file} (exception {e} occurred when finding or creating user config)", try:
OUTPUT_INFO, if not os.path.exists(user_config_file):
) os.makedirs(os.path.split(user_config_file)[0], exist_ok=True)
shutil.copyfile(package_config_file, user_config_file)
config_file = user_config_file
self.log(f"Copied package config to local file {config_file}", OUTPUT_INFO)
else: # user file exists
version = JsonStore(user_config_file, indent=4).get("general")["version"]
if version < CONFIG_MIN_VERSION:
backup = user_config_file + f".{version}.backup"
shutil.copyfile(user_config_file, backup)
shutil.copyfile(package_config_file, user_config_file)
self.log(
f"Copied package config file to {user_config_file} as user file is outdated (<{CONFIG_MIN_VERSION}). Old version stored as {backup}",
OUTPUT_INFO,
)
config_file = user_config_file
self.log(f"Using user config file {config_file}", OUTPUT_INFO)
except Exception as e:
config_file = package_config_file
self.log(
f"Using package config file {config_file} (exception {e} occurred when finding or creating user config)",
OUTPUT_INFO,
)
try: try:
self._config_store = JsonStore(config_file, indent=4) self._config_store = JsonStore(config_file, indent=4)
except Exception as e: except Exception as e:
+3 -3
View File
@@ -1,6 +1,6 @@
VERSION = "1.1.2" VERSION = "1.2.0"
HOMEPAGE = "https://github.com/sanderland/katrain" HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.1.2" CONFIG_MIN_VERSION = "1.2.0"
PLAYER_HUMAN, PLAYER_AI = "player:human", "player:ai" PLAYER_HUMAN, PLAYER_AI = "player:human", "player:ai"
PLAYER_TYPES = [PLAYER_HUMAN, PLAYER_AI] PLAYER_TYPES = [PLAYER_HUMAN, PLAYER_AI]
@@ -33,13 +33,13 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_SCORELOSS, AI_SCORELOSS,
AI_POLICY, AI_POLICY,
AI_WEIGHTED, AI_WEIGHTED,
AI_RANK,
AI_PICK, AI_PICK,
AI_LOCAL, AI_LOCAL,
AI_TENUKI, AI_TENUKI,
AI_TERRITORY, AI_TERRITORY,
AI_INFLUENCE, AI_INFLUENCE,
AI_JIGO, AI_JIGO,
AI_RANK,
] ]
+6
View File
@@ -82,6 +82,12 @@ class KataGoEngine:
self.shutdown(finish=False) self.shutdown(finish=False)
self.start() self.start()
def check_alive(self,exception_if_dead=False):
ok = self.katago_process and self.katago_process.poll() is None
if not ok and exception_if_dead:
raise EngineDiedException(f"Engine died (process {self.katago_process}, poll {self.katago_process and self.katago_process.poll()}) config {self.config}")
return ok
def shutdown(self, finish=False): def shutdown(self, finish=False):
process = self.katago_process process = self.katago_process
if finish and process: if finish and process:
+22 -3
View File
@@ -1,8 +1,27 @@
import pytest from katrain.core.ai import generate_ai_move
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, AI_STRATEGIES, OUTPUT_INFO
from katrain.core.constants import AI_STRATEGIES_RECOMMENDED_ORDER, AI_STRATEGIES from katrain.core.base_katrain import KaTrainBase
from katrain.core.engine import KataGoEngine
from katrain.core.game import Game
from katrain.core.constants import AI_STRATEGIES
class TestAI: class TestAI:
def test_order(self): def test_order(self):
assert set(AI_STRATEGIES_RECOMMENDED_ORDER) == set(AI_STRATEGIES) assert set(AI_STRATEGIES_RECOMMENDED_ORDER) == set(AI_STRATEGIES)
def test_ai_strategies(self):
katrain = KaTrainBase(force_package_config=True, debug_level=0)
engine = KataGoEngine(katrain, katrain.config("engine"))
game = Game(katrain, engine)
n_rounds = 3
for _ in range(n_rounds):
for strategy in AI_STRATEGIES:
settings = katrain.config(f"ai/{strategy}")
move, played_node = generate_ai_move(game, strategy, settings)
katrain.log(f"Testing strategy {strategy} -> {move}", OUTPUT_INFO)
assert move.coords is not None
assert played_node == game.current_node
assert game.current_node.depth == len(AI_STRATEGIES) * n_rounds