This commit is contained in:
Sander Land committed 2020-06-27 12:23:31 +02:00
commit 48c4c81b8a
24 files changed
+245 -59

No files matched your search

+4 -2
View File
@@ -48,15 +48,17 @@ The following packages may help resolve missing OS packages for Kivy or KataGo.
```
sudo apt-get install python3-pip build-essential git python3 python3-dev ffmpeg libsdl2-dev libsdl2-image-dev\
libsdl2-mixer-dev libsdl2-ttf-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev zlib1g-dev\
libgstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good\
libgstreamer1.0 gstreamer1.0-plugins-base gstreamer1.0-plugins-good libpulse\
pkg-config libgl-dev opencl-headers ocl-icd-opencl-dev python3-pygame
```
Then, try installing python package dependencies using:
```
pip3 install -U cython wheel setuptools
pip3 install kivy==2.0.0rc2 kivymd==1.104.1
pip3 install kivy==2.0.0rc2 kivymd==0.104.1
```
You can also install kivy from source using `pip install git+https://github.com/kivy/kivy.git@2.0.0rc3` which may help issues with audio on linux.
In case KataGo does not start, an alternative is to go [here](https://github.com/lightvector/KataGo) and compile KataGo yourself.
# Configuring the GPU(s) KataGo uses
+8 -3
View File
@@ -3,7 +3,8 @@
import os
os.environ["KCFG_KIVY_LOG_LEVEL"] = os.environ.get("KCFG_KIVY_LOG_LEVEL", "warning")
os.environ["KIVY_AUDIO"] = "sdl2" # force working audio
if "KIVY_AUDIO" not in os.environ:
os.environ["KIVY_AUDIO"] = "sdl2" # seems to be most stable / some players hard crash
# next, icon
from katrain.core.utils import find_package_resource, PATHS
@@ -161,7 +162,10 @@ class KaTrainGui(Screen, KaTrainBase):
# update move tree
self.controls.move_tree.current_node = self.game.current_node
def update_state(
def update_state(self, redraw_board=False): # redirect to message queue thread
self("update_state", redraw_board=redraw_board)
def _do_update_state(
self, redraw_board=False
): # is called after every message and on receiving analyses and config changes
# AI and Trainer/auto-undo handlers
@@ -214,7 +218,8 @@ class KaTrainGui(Screen, KaTrainBase):
continue
fn = getattr(self, f"_do_{msg.replace('-','_')}")
fn(*args, **kwargs)
self.update_state()
if msg != "update_state":
self._do_update_state()
except Exception as exc:
self.log(f"Exception in processing message {msg} {args}: {exc}", OUTPUT_ERROR)
traceback.print_exc()
+4
View File
@@ -69,6 +69,10 @@
},
"ai": {
"ai:default": {},
"ai:handicap": {
"automatic": true,
"pda": 0
},
"ai:jigo": {
"target_score": 0.5
},
+90 -3
View File
@@ -21,6 +21,8 @@ from katrain.core.constants import (
AI_TERRITORY,
AI_PICK,
AI_RANK,
AI_HANDICAP,
OUTPUT_ERROR,
)
from katrain.core.game import Game, GameNode, Move
@@ -93,8 +95,51 @@ 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:
error = False
analysis = None
def set_analysis(a):
nonlocal analysis
analysis = a
def set_error(a):
nonlocal error
game.katrain.log("Error in PDA-based analysis", a)
error = True
engine = game.engines[cn.player]
engine.request_analysis(
cn,
callback=set_analysis,
error_callback=set_error,
priority=1_000,
ownership=False,
extra_settings=extra_settings,
)
while not (error or analysis):
time.sleep(0.01)
engine.check_alive(exception_if_dead=True)
return analysis
def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode]:
cn = game.current_node
if ai_mode == AI_HANDICAP:
pda = ai_settings["pda"]
if ai_settings["automatic"]:
n_handicaps = len(game.root.get_list_property("AB", []))
MOVE_VALUE = 14 # could be rules dependent
b_stones_advantage = max(n_handicaps - 1, 0) - (cn.komi - MOVE_VALUE / 2) / MOVE_VALUE
pda = min(3, max(-3, -b_stones_advantage * (3 / 8))) # max PDA at 8 stone adv, normal 9 stone game is 8.46
handicap_analysis = request_ai_analysis(
game, cn, {"playoutDoublingAdvantage": pda, "playoutDoublingAdvantagePla": "BLACK"}
)
if not handicap_analysis:
game.katrain.log(f"Error getting handicap-based move", OUTPUT_ERROR)
ai_mode = AI_DEFAULT
while not cn.analysis_ready:
time.sleep(0.01)
game.engines[cn.next_player].check_alive(exception_if_dead=True)
@@ -143,7 +188,43 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
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)))
n_moves = int(
round(
board_squares
/ 361
* len(legal_policy_moves)
/ (
1.311648546930214
* (
(
0.31164467
+ 0.55726218
* (len(legal_policy_moves) / board_squares)
* math.exp(
-1
* (
3.0308747
* (len(legal_policy_moves) / board_squares)
* (len(legal_policy_moves) / board_squares)
- (len(legal_policy_moves) / board_squares)
- 0.045792218 * ai_settings["kyu_rank"]
- 0.31164467
)
** 2
)
- 0.0064860256 * ai_settings["kyu_rank"]
)
* (
0.0630149
+ 0.762399
* board_squares
/ (10 ** (-0.05737 * ai_settings["kyu_rank"] + 1.9482))
)
)
- 0.08265346672884874
)
)
)
if ai_mode in [AI_INFLUENCE, AI_TERRITORY, AI_LOCAL, AI_TENUKI]:
if cn.depth > ai_settings["endgame"] * board_squares:
@@ -188,6 +269,9 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
raise ValueError(f"Unknown Policy-based AI mode {ai_mode}")
else: # Engine based move
candidate_ai_moves = cn.candidate_moves
if ai_mode == AI_HANDICAP:
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
aimove = top_cand
@@ -214,11 +298,14 @@ def generate_ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move,
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."
else:
if ai_mode != AI_DEFAULT:
if ai_mode not in [AI_DEFAULT, AI_HANDICAP]:
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
ai_thoughts += f"Default strategy found {len(candidate_ai_moves)} moves returned from the engine and chose {aimove.gtp()} as top move"
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'])}"
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)
played_node = game.play(aimove)
played_node.ai_thoughts = ai_thoughts
+6 -16
View File
@@ -1,6 +1,6 @@
VERSION = "1.3.0"
HOMEPAGE = "https://github.com/sanderland/katrain"
CONFIG_MIN_VERSION = "1.2.0" # keep config files from this version
CONFIG_MIN_VERSION = "1.3.0" # keep config files from this version
OUTPUT_ERROR = -1
OUTPUT_KATAGO_STDERR = -0.5
@@ -17,6 +17,7 @@ GAME_TYPES = [PLAYING_NORMAL, PLAYING_TEACHING]
MODE_PLAY, MODE_ANALYZE = "play", "analyze"
AI_DEFAULT = "ai:default"
AI_HANDICAP = "ai:handicap"
AI_SCORELOSS = "ai:scoreloss"
AI_WEIGHTED = "ai:p:weighted"
AI_JIGO = "ai:jigo"
@@ -30,13 +31,14 @@ AI_RANK = "ai:p:rank"
AI_CONFIG_DEFAULT = AI_SCORELOSS
AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_SCORELOSS, AI_JIGO]
AI_STRATEGIES_ENGINE = [AI_DEFAULT, AI_HANDICAP, AI_SCORELOSS, AI_JIGO]
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
AI_STRATEGIES_RECOMMENDED_ORDER = [
AI_DEFAULT,
AI_RANK,
AI_HANDICAP,
AI_SCORELOSS,
AI_POLICY,
AI_WEIGHTED,
@@ -49,20 +51,6 @@ AI_STRATEGIES_RECOMMENDED_ORDER = [
]
AI_STRENGTH = { # not used
AI_DEFAULT: "9d",
AI_POLICY: "4d",
AI_JIGO: "?d",
AI_SCORELOSS: "5k",
AI_WEIGHTED: "5k",
AI_PICK: "8k",
AI_LOCAL: "5k",
AI_TENUKI: "8k",
AI_INFLUENCE: "8k",
AI_TERRITORY: "5k",
AI_RANK: "15k - 3d",
}
AI_OPTION_VALUES = {
"kyu_rank": [(k, f"{k}[strength:kyu]") for k in range(15, 0, -1)]
+ [(k, f"{1-k}[strength:dan]") for k in range(0, -3, -1)],
@@ -77,4 +65,6 @@ AI_OPTION_VALUES = {
"stddev": [x / 2 for x in range(21)],
"line_weight": range(0, 11),
"threshold": [2, 2.5, 3, 3.5, 4, 4.5],
"automatic": "bool",
"pda": [(x / 10, f"{'W' if x<0 else 'B'}+{abs(x/10):.1f}") for x in range(-30, 31)],
}
+9 -6
View File
@@ -5,7 +5,7 @@ import subprocess
import threading
import time
import traceback
from typing import Callable, Optional
from typing import Callable, Optional, Dict
from katrain.core.constants import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_KATAGO_STDERR
from katrain.core.game_node import GameNode
@@ -206,9 +206,11 @@ class KataGoEngine:
time_limit=True,
priority: int = 0,
ownership: Optional[bool] = None,
next_move=None,
next_move: Optional[GameNode] = None,
extra_settings: Optional[Dict] = None,
):
moves = [m for node in analysis_node.nodes_from_root for m in node.move_with_placements]
moves = [m for node in analysis_node.nodes_from_root for m in node.moves]
initial_stones = analysis_node.root.placements
if next_move:
moves.append(next_move)
if ownership is None:
@@ -233,10 +235,11 @@ class KataGoEngine:
"komi": analysis_node.komi,
"boardXSize": size_x,
"boardYSize": size_y,
"includeOwnership": ownership,
"includeOwnership": ownership and not next_move,
"includePolicy": not next_move,
"initialStones": [[m.player, m.gtp()] for m in initial_stones],
"moves": [[m.player, m.gtp()] for m in moves],
"overrideSettings": settings,
"overrideSettings": {**settings, **(extra_settings or {})},
}
self.send_query(query, callback, error_callback, next_move)
analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested,visits)
analysis_node.analysis_visits_requested = max(analysis_node.analysis_visits_requested, visits)
+10 -6
View File
@@ -322,12 +322,16 @@ class ConfigAIPopup(QuickConfigGui):
self.options_grid.add_widget(DescriptionLabel(text=k, size_hint_x=0.25))
if k in AI_OPTION_VALUES:
values = AI_OPTION_VALUES[k]
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]
else: # just numbers
fixed_values = [(v, str(v)) for v in values]
widget = LabelledSelectionSlider(values=fixed_values, input_property=f"ai/{strategy}/{k}")
widget.set_value(v)
if values == "bool":
widget = LabelledCheckBox(input_property=f"ai/{strategy}/{k}")
widget.active = v
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]
else: # just numbers
fixed_values = [(v, str(v)) for v in values]
widget = LabelledSelectionSlider(values=fixed_values, input_property=f"ai/{strategy}/{k}")
widget.set_value(v)
self.options_grid.add_widget(wrap_anchor(widget))
else:
self.options_grid.add_widget(
+14 -7
View File
@@ -163,11 +163,13 @@ def averagemod(data):
(int(lendata * 0.8) + 1) - int(lendata * 0.2)
) # average without the best and worst 20% of ranks
def gauss(data):
return math.exp(-1*(data)**2)
class RankGraph(Graph):
black_rank_points = ListProperty([])
white_rank_points = ListProperty([])
segment_length = NumericProperty(60)
segment_length = NumericProperty(80)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -191,12 +193,16 @@ class RankGraph(Graph):
if not non_obvious_moves:
return None
num_legal, rank, value = zip(*non_obvious_moves)
averagemod_rank = averagemod(rank)
rank = list(rank)
for (i, item) in enumerate(rank):
if item > num_legal[i]*0.09:
rank[i] = num_legal[i]*0.09
rank = tuple(rank)
averagemod_rank = averagemod(rank)+1
averagemod_len_legal = averagemod(num_legal)
# the averagemod_rank is the outlier free average of the best move from a selection of n_moves with averagemod_len_legal of total legal moves
n_moves = math.floor(0.40220696 + averagemod_len_legal / (1.313341 * (averagemod_rank + 1) - 0.088646986))
# using the calibration curve of p:pick:rank
rank_kyu = (math.log10(n_moves * 361 / num_intersec) - 1.9482) / -0.05737
norm_avemod_len_legal = (averagemod_len_legal/num_intersec)
rank_kyu = -0.6284*math.log(averagemod_rank)/(0.1705+averagemod_rank*gauss(3.374*(norm_avemod_len_legal)))+13.59*(norm_avemod_len_legal)+10.41*math.log(averagemod_rank)+12.42*gauss(2.519*(norm_avemod_len_legal))-14.58
return 1 - rank_kyu # dan rank
@staticmethod
@@ -226,7 +232,8 @@ class RankGraph(Graph):
for segment_mid in range(0, len(nodes), dx):
bounds = (max(0, segment_mid - half_seg), min(segment_mid + half_seg, len(nodes)))
for pl, rank in self.calculate_ranks(policy_stats[bounds[0] : bounds[1] + 1], num_intersec).items():
ranks[pl].append((segment_mid, rank))
if bounds[1] - bounds[0] > self.segment_length * 0.75:
ranks[pl].append((segment_mid, rank))
self.rank_by_player = ranks
self.redraw_trigger()
Binary file not shown.
@@ -584,3 +584,15 @@ msgstr "Re-analyze entire game"
#. TODO
msgid "analysis:continuous"
msgstr "Continuously analyze"
#. TODO
msgid "ai:handicap"
msgstr "KataGo Handicap"
#. TODO
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective.When `automatic` is set, KaTrain will find a suitable value "
"automatically."
Binary file not shown.
@@ -627,3 +627,15 @@ msgstr "Re-analyze entire game"
#. TODO
msgid "analysis:continuous"
msgstr "Continuously analyze"
#. TODO
msgid "ai:handicap"
msgstr "KataGo Handicap"
#. TODO
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective.When `automatic` is set, KaTrain will find a suitable value "
"automatically."
Binary file not shown.
@@ -518,6 +518,16 @@ msgstr ""
"in the general settings `engine` section and engine configuration file. No "
"options are available here."
msgid "ai:handicap"
msgstr "KataGo Handicap"
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective. When `automatic` is set, KaTrain will find a suitable value "
"automatically."
msgid "ai:jigo"
msgstr "KataJigo"
Binary file not shown.
@@ -615,3 +615,15 @@ msgstr "Re-analizar juego"
msgid "analysis:continuous"
msgstr "Analizar continuamente "
#. TODO
msgid "ai:handicap"
msgstr "KataGo Handicap"
#. TODO
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective.When `automatic` is set, KaTrain will find a suitable value "
"automatically."
Binary file not shown.
@@ -661,3 +661,15 @@ msgstr "Re-analyze entire game"
#. TODO
msgid "analysis:continuous"
msgstr "Continuously analyze"
#. TODO
msgid "ai:handicap"
msgstr "KataGo Handicap"
#. TODO
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective.When `automatic` is set, KaTrain will find a suitable value "
"automatically."
Binary file not shown.
@@ -599,3 +599,15 @@ msgstr "Re-analyze entire game"
#. TODO
msgid "analysis:continuous"
msgstr "Continuously analyze"
#. TODO
msgid "ai:handicap"
msgstr "KataGo Handicap"
#. TODO
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective.When `automatic` is set, KaTrain will find a suitable value "
"automatically."
Binary file not shown.
@@ -624,3 +624,15 @@ msgstr "Re-analyze entire game"
#. TODO
msgid "analysis:continuous"
msgstr "Continuously analyze"
#. TODO
msgid "ai:handicap"
msgstr "KataGo Handicap"
#. TODO
msgid "aihelp:handicap"
msgstr ""
"KataGo optimized for playing higher handicap games.The `pda` setting "
"corresponds to `playoutDoublingAdvantage` in KataGo, from black's "
"perspective.When `automatic` is set, KaTrain will find a suitable value "
"automatically."
+2 -2
View File
@@ -41,12 +41,12 @@ setup(
"wheel",
"setuptools",
"importlib_resources ;python_version<'3.7'",
"pygame", # some versions need this for kivy
"pygame", # some mac versions need this for kivy
"cython>=0.24,<=0.29.14,!=0.27,!=0.27.2", # kivy wants this
"kivy_deps.glew;platform_system=='Windows'",
"kivy_deps.sdl2;platform_system=='Windows'",
"kivy_deps.gstreamer;platform_system=='Windows'",
"kivy>=2.0.0rc2",
"kivy==2.0.0rc2", # rc3 failing on mac
"kivymd>=0.104.1",
"screeninfo;platform_system!='Darwin'", # for screen resolution, has problems on macos
],
+16 -14
View File
@@ -2,27 +2,29 @@ import pytest
from katrain.core.game import Game, IllegalMoveException, Move
from katrain.core.base_katrain import KaTrainBase, OUTPUT_INFO
from katrain.core.game_node import GameNode
class MockKaTrain(KaTrainBase):
pass
# def log(self, message, level=OUTPUT_INFO):
# pass
class MockEngine:
def request_analysis(self, *args, **kwargs):
pass
@pytest.fixture
def new_game():
return GameNode(properties={"SZ": 19})
class TestBoard:
def nonempty_chains(self, b):
return [c for c in b.chains if c]
def test_merge(self):
b = Game(MockKaTrain(), MockEngine())
def test_merge(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
b.play(Move.from_gtp("B9", player="B"))
b.play(Move.from_gtp("A3", player="B"))
b.play(Move.from_gtp("A9", player="B"))
@@ -30,8 +32,8 @@ class TestBoard:
assert 3 == len(b.stones)
assert 0 == len(b.prisoners)
def test_collide(self):
b = Game(MockKaTrain(), MockEngine())
def test_collide(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
b.play(Move.from_gtp("B9", player="B"))
with pytest.raises(IllegalMoveException):
b.play(Move.from_gtp("B9", player="W"))
@@ -39,8 +41,8 @@ class TestBoard:
assert 1 == len(b.stones)
assert 0 == len(b.prisoners)
def test_capture(self):
b = Game(MockKaTrain(), MockEngine())
def test_capture(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
b.play(Move.from_gtp("A2", player="B"))
b.play(Move.from_gtp("B1", player="W"))
b.play(Move.from_gtp("A1", player="W"))
@@ -60,8 +62,8 @@ class TestBoard:
assert 4 == len(b.stones)
assert 2 == len(b.prisoners)
def test_snapback(self):
b = Game(MockKaTrain(), MockEngine())
def test_snapback(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
for move in ["C1", "D1", "E1", "C2", "D3", "E4", "F2", "F3", "F4"]:
b.play(Move.from_gtp(move, player="B"))
for move in ["D2", "E2", "C3", "D4", "C4"]:
@@ -78,8 +80,8 @@ class TestBoard:
assert 12 == len(b.stones)
assert 4 == len(b.prisoners)
def test_ko(self):
b = Game(MockKaTrain(), MockEngine())
def test_ko(self, new_game):
b = Game(MockKaTrain(force_package_config=True), MockEngine(), move_tree=new_game)
for move in ["A2", "B1"]:
b.play(Move.from_gtp(move, player="B"))