Optimize settings / smaller boardsize kata / fix error message / teacher improvements

Optimize settings / smaller boardsize kata / fix error message / teacher improvements
This commit is contained in:
sanderland authored and GitHub committed 2020-05-07 18:20:28 +02:00
commit 0827bd418b
30 files changed
+185 -113

No files matched your search

BIN
View File
Binary file not shown.
+3 -3
View File
@@ -55,14 +55,14 @@ maxVisits = 500
# number of visits, and thread contention will reduce efficiency, so cross-position parallelization is preferable
# to numSearchThreads, but numSearchThreads is preferable if you want to reduce latency, and have individual
# searches complete faster by doing fewer of them at a time.
numSearchThreads = 2
numSearchThreads = 6
# GPU Settings-------------------------------------------------------------------------------
# Maximum number of positions to send to GPU at once.
nnMaxBatchSize = 128
nnMaxBatchSize = 64
# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
nnCacheSizePowerOfTwo = 23
nnCacheSizePowerOfTwo = 21
# Size of mutex pool for nnCache is 2 ** this
nnMutexPoolSizePowerOfTwo = 17
# Randomize board orientation when running neural net evals?
Binary file not shown.
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+18 -10
View File
@@ -39,17 +39,25 @@ but has since grown to include a wide range of features, including:
* Execute the command `pip install kivy_deps.glew kivy_deps.sdl2 kivy_deps.gstreamer kivy`
* Start the app by running `python katrain.py` in the directory where you downloaded the scripts. Note that the program can be slow to initialize the first time, due to kata's gpu tuning.
### Installation for Linux/Mac users
### Installation for Linux users
* This assumed you have a working Python 3.6/3.7 installation as a default. If your default is python 2, use pip3/python3. Kivy currently does not have a release for Python 3.8.
* Git clone or download the repository.
* `pip install kivy`
* A binary for KataGo is included, but if you have compiled your own, point the 'engine/katago' setting to the relevant KataGo v1.3.5+ binary.
* Run the command `pip install kivy` in the terminal.
* A binary for KataGo is included, but if you have compiled your own, point the 'engine/katago' setting to the relevant KataGo v1.3.5-bs29+ binary.
* Start the app by running `python katrain.py`. Note that the program can be slow to initialize the first time, due to KataGo's GPU tuning.
### Installation for MacOS users
* Git clone or download the repository.
* Run the command `pip install kivy` in the terminal.
* Follow instructions [here](https://github.com/lightvector/KataGo) to compile KataGo yourself -- note that the version required is currently too new for the 'brew' method.
* Start the app by running `python katrain.py`, change the path of the 'katago' setting to the path where you compiled it, and click 'Apply and Save'.
## Manual
### Play
Under the 'play' tab you can select who is playing black and white.
* Human is simple play with potential feedback, but without auto-undo.
* Teach will give you instant feedback, and auto-undo bad moves to give you a second chance.
@@ -72,6 +80,7 @@ In short, if you are a weaker player you should mostly on large dots that are re
while stronger players can pay more attention to smaller mistakes.
#### AIs
Available AIs, with strength indicating an estimate for the default settings, are:
* **[9p+]** **Default** is full KataGo, above professional level.
@@ -107,7 +116,6 @@ Keyboard shortcuts are shown with **[key]**.
* **[s]**: Equalize: Re-evaluate all currently shown next moves with the same visits as the current top move. Useful to increase confidence in the suggestions with high uncertainty.
* **[d]**: Sweep: Evaluate all possible next moves. This can take a bit of time even though 'fast_visits' is used, but the result is nothing if not colourful.
## Keyboard and mouse shortcuts
In addition to shortcuts mentioned above, there are:
@@ -128,7 +136,6 @@ In addition to shortcuts mentioned above, there are:
* **[Ctrl-n]**: Load SGF from clipboard
* **[space]**: Pass
## Configuration
Configuration is stored in `config.json`. Most settings are now available to edit in the program, but some advanced options are not.
@@ -165,11 +172,12 @@ If you ever need to reset to the original settings, simply re-download the `conf
* The first startup of KataGo can be slow due to GPU tuning, after that it should be much faster.
* The program is running too slowly. How can I speed it up?
* Adjust the number of visits or maximum time allowed in the settings.
* The engine crashes with out of memory errors
* Try setting `nnMaxBatchSize` in `KataGo/analysis_config.cfg` to something lower.
* KataGo crashes with out of memory errors, how can I prevent this?
* Try using a lower number for `nnMaxBatchSize` in `KataGo/analysis_config.cfg`, and avoid using versions compiled with large board sizes.
## Contributing
* Feedback and pull requests are both very welcome.
* Feedback and pull requests are both very welcome. I would also be happy to host translations of this manual into languages where English fluency is typically lower.
* For suggestions and planned improvements, see the 'issues' tab on github.
* You can also contact me on discord (Sander#3278) or [reddit](http://reddit.com/u/sanderbaduk) to give feedback, or simply show your appreciation.
* You can also contact me on discord (Sander#3278), [KakaoTalk](https://open.kakao.com/o/gTsMJCac) or [Reddit](http://reddit.com/u/sanderbaduk) to give feedback, or simply show your appreciation.
* Some people have also asked me how to donate. Something go-related such as a book or teaching time is highly appreciated.
+2 -2
View File
@@ -75,7 +75,7 @@ def malkovich_analysis(cn):
logger.log(f"dscore {dscore} = {cn.analysis['root']['scoreLead']} {cn.parent.analysis['root']['scoreLead']} at {move}...", OUTPUT_ERROR)
if abs(dscore) > REPORT_SCORE_THRESHOLD and (cn.player == "B" and dscore < 0 or cn.player == "W" and dscore > 0): # relevant mistakes
favpl = "B" if dscore > 0 else "W"
msg = f"MALKOVICH:{cn.player} {cn.single_move.gtp()} caused a significant score change ({favpl} gained {abs(dscore):.1f} points)"
msg = f"MALKOVICH:{cn.player} {cn.move.gtp()} caused a significant score change ({favpl} gained {abs(dscore):.1f} points)"
if cn.ai_thoughts:
msg += f" -> Win Rate {cn.format_win_rate()} Score {cn.format_score()} AI Thoughts: {cn.ai_thoughts}"
else:
@@ -139,7 +139,7 @@ while True:
if not pol:
pol = ["??"]
print(f"DISCUSSION:OK, since you passed {MAX_PASS} times after the {bx+by}th move, I will pass as well [policy {pol[-1]:.3%}].", file=sys.stderr)
move = game.play(Move(None, player=game.next_player)).single_move
move = game.play(Move(None, player=game.next_player)).move
else:
move, node = ai_move(game, ai_strategy, ai_settings)
if node is None:
+1 -1
View File
@@ -18,7 +18,7 @@ ENGINE_SETTINGS = {
"max_visits": 50,
"max_time": 1.0,
"enable_ownership": False,
"threads": 8,
"threads": 32,
}
+1 -1
View File
@@ -41,7 +41,7 @@ class AI:
"max_time": 300.0,
"_enable_ownership": False,
}
NUM_THREADS = 8
NUM_THREADS = 32
IGNORE_SETTINGS_IN_TAG = {"threads", "_enable_ownership", "katago"} # katago for switching from/to bs version
ENGINES = []
LOCK = threading.Lock()
+3 -1
View File
@@ -1,5 +1,6 @@
bot_strategy_names = {
"dev": "P:Noise",
"dev-beta": "P:Weighted",
"strong": "Policy",
"influence": "P:Influence",
"territory": "P:Territory",
@@ -11,7 +12,8 @@ bot_strategy_names = {
greetings = {
"dev": "Experimental!",
"dev": "Policy+Dirichlet noise.",
"dev-beta": "Play a policy-weighted move.",
"strong": "Play top policy move.",
"influence": "Play an influential style.",
"territory": "Play a territorial style.",
+5 -2
View File
@@ -15,7 +15,10 @@ if True or bot in ["dev", "local"]:
else:
GTP2OGS = "node ../stable-gtp2ogs"
BOT_SETTINGS = f" --maxconnectedgames {MAXGAMES} --maxhandicapunranked 25 --maxhandicapranked 1 --boardsizesranked 19 --boardsizesunranked all --komisranked automatic,5.5,6.5,7.5 --komisunranked all"
if "beta" in bot:
BOT_SETTINGS += " --beta"
else:
BOT_SETTINGS += "" # --rankedonly"
username = f"katrain-{bot}"
@@ -40,6 +43,6 @@ if settings:
GREETING += f" Settings: {settings_dump}."
BYEMSG = "Thank you for playing. If you have any feedback, please message my admin! Play with these bots at any time by downloading KaTrain at github.com/sanderland/katrain"
cmd = f'{GTP2OGS} --debug --apikey {APIKEY} --username {username} --greeting "{GREETING}" --farewell "{BYEMSG}" {BOT_SETTINGS} --farewellscore --aichat --noclock --nopause --speeds blitz,live --persist --minrank 25k -- python bots/ai2gtp.py {bot} {port}'
cmd = f'{GTP2OGS} --debug --apikey {APIKEY} --rejectnewfile ~/shutdown_bots --username {username} --greeting "{GREETING}" --farewell "{BYEMSG}" {BOT_SETTINGS} --farewellscore --aichat --noclock --nopause --speeds blitz,live --persist --minrank 25k -- python bots/ai2gtp.py {bot} {port}'
print(f"starting bot {username} using server port {port} --> {cmd}")
os.system(cmd)
+5 -4
View File
@@ -1,9 +1,9 @@
{
"engine": {
"katago": "KataGo/katago-bs",
"katago": "KataGo/katago",
"model": "KataGo/models/b15-1.3.2.txt.gz",
"config": "KataGo/analysis_config.cfg",
"threads": 8,
"threads": 16,
"max_visits": 500,
"fast_visits": 50,
"max_time": 3.0,
@@ -43,7 +43,8 @@
0
],
"eval_off_show_last": 3,
"eval_show_ai": true
"eval_show_ai": true,
"lock_ai": false
},
"ai": {
"Default": {
@@ -98,7 +99,7 @@
"_help_right": "Lower `stddev` makes it prefer closer moves."
},
"P:Tenuki": {
"pick_override": 0.9,
"pick_override": 0.85,
"stddev": 7.5,
"pick_n": 5,
"pick_frac": 0.7,
+2 -2
View File
@@ -99,11 +99,11 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
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 "local" in ai_mode or "tenuki" in ai_mode:
var = ai_settings["stddev"] ** 2
if not cn.single_move or cn.single_move.coords is None:
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.single_move.coords
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
]
+2 -2
View File
@@ -43,9 +43,9 @@ class KataGoEngine:
try:
self.katrain.log(f"Starting KataGo with {self.command}", OUTPUT_DEBUG)
self.katago_process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
except FileNotFoundError:
except FileNotFoundError as e:
self.katrain.log(
f"Starting kata with command '{self.command}' failed. If you are on Mac or Linux, please change the settings or configuration file (config.json) to point to the correct KataGo executable.",
f"Starting kata with command '{self.command}' failed with error {e}. Please make sure the 'katago' value under 'engine' in settings points to the correct KataGo executable.",
OUTPUT_ERROR,
)
+11 -8
View File
@@ -265,12 +265,15 @@ class Game:
elif mode == "sweep":
board_size_x, board_size_y = self.board_size
policy_grid = var_to_grid(self.current_node.policy, size=(board_size_x, board_size_y)) if self.current_node.policy else None
analyze_moves = [
Move(coords=(x, y), player=cn.next_player)
for x in range(board_size_x)
for y in range(board_size_y)
if (policy_grid is None and (x, y) not in stones) or policy_grid[y][x] >= 0
]
analyze_moves = sorted(
[
Move(coords=(x, y), player=cn.next_player)
for x in range(board_size_x)
for y in range(board_size_y)
if (policy_grid is None and (x, y) not in stones) or policy_grid[y][x] >= 0
],
key=lambda mv: -policy_grid[mv.coords[1]][mv.coords[0]],
)
visits = engine.config["fast_visits"]
self.katrain.controls.set_status(f"Refining analysis of entire board to {visits} visits")
priority = -1_000_000_000
@@ -283,7 +286,7 @@ class Game:
cn.analyze(engine, priority, visits=visits, refine_move=move, time_limit=False) # explicitly requested so take as long as you need
def analyze_undo(self, node, train_config):
move = node.single_move
move = node.move
if node != self.current_node or node.auto_undo is not None or not node.analysis_ready or not move:
return
points_lost = node.points_lost
@@ -306,5 +309,5 @@ class Game:
node.auto_undo = undo
if undo:
self.undo(1)
self.katrain.controls.set_status(f"Undid move {move.gtp()} as it lost {points_lost:.1f} points{xmsg}")
self.katrain.controls.set_status(f"Undid move {move.gtp()} as it lost {points_lost:.1f} points{xmsg}. Hover over the move to see expected refutation.")
self.katrain.update_state()
+11 -5
View File
@@ -86,8 +86,14 @@ class GameNode(SGFNode):
if win_rate is not None:
return f"{'B' if win_rate > 0.5 else 'W'} {max(win_rate,1-win_rate):.1%}"
def comment(self, sgf=False, teach=False, hints=False):
single_move = self.single_move
def make_pv(self, player, pv, interactive):
pvtext = f"{player}{' '.join(pv)}"
# if interactive:
# pvtext = f"[u][ref={pvtext}][color=#334466]{pvtext}[/color][/ref][/u]"
return pvtext
def comment(self, sgf=False, teach=False, hints=False, interactive=False):
single_move = self.move
if not self.parent or not single_move: # root
return ""
@@ -109,7 +115,7 @@ class GameNode(SGFNode):
text += f"Move was predicted best move.\n"
if sgf:
if previous_top_move.get("pv") and (sgf or hints):
text += f"PV: {single_move.player}{' '.join(previous_top_move['pv'])}\n"
text += f"PV: {self.make_pv(single_move.player,previous_top_move['pv'],interactive)}\n"
if sgf or hints or teach:
policy_ranking = self.parent.policy_ranking
@@ -131,7 +137,7 @@ class GameNode(SGFNode):
@property
def points_lost(self) -> Optional[float]:
single_move = self.single_move
single_move = self.move
if single_move and self.parent and self.analysis_ready and self.parent.analysis_ready:
parent_score = self.parent.score
score = self.score
@@ -139,7 +145,7 @@ class GameNode(SGFNode):
@property
def parent_realized_points_lost(self) -> Optional[float]:
single_move = self.single_move
single_move = self.move
if single_move and self.parent and self.parent.parent and self.analysis_ready and self.parent.parent.analysis_ready:
parent_parent_score = self.parent.parent.score
score = self.score
+41 -16
View File
@@ -5,16 +5,19 @@ from typing import Any, Dict, List, Optional, Tuple
class ParseError(Exception):
"""Exception raised on a parse error"""
pass
class Move:
GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + [xa + c for xa in "AB" for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"] # kata board size 29 support
GTP_COORD = list("ABCDEFGHJKLMNOPQRSTUVWXYZ") + [xa + c for xa in "AB" for c in "ABCDEFGHJKLMNOPQRSTUVWXYZ"] # board size 52+ support
PLAYERS = "BW"
SGF_COORD = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ".lower()) + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
SGF_COORD = list("ABCDEFGHIJKLMNOPQRSTUVWXYZ".lower()) + list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") # sgf goes to 52
@classmethod
def from_gtp(cls, gtp_coords, player="B"):
"""Initialize a move from GTP coordinates and player"""
if "pass" in gtp_coords.lower():
return cls(coords=None, player=player)
match = re.match(r"([A-Z]+)(\d+)", gtp_coords)
@@ -22,11 +25,13 @@ class Move:
@classmethod
def from_sgf(cls, sgf_coords, board_size, player="B"):
"""Initialize a move from SGF coordinates and player"""
if sgf_coords == "" or Move.SGF_COORD.index(sgf_coords[0]) == board_size[0]: # some servers use [tt] for pass
return cls(coords=None, player=player)
return cls(coords=(Move.SGF_COORD.index(sgf_coords[0]), board_size[1] - Move.SGF_COORD.index(sgf_coords[1]) - 1), player=player)
return cls(coords=(Move.SGF_COORD.index(sgf_coords[0]), board_size[1] - Move.SGF_COORD.index(sgf_coords[1]) - 1), player=player,)
def __init__(self, coords: Optional[Tuple[int, int]] = None, player: str = "B"):
"""Initialize a move from zero-based coordinates and player"""
self.player = player
self.coords = coords
@@ -37,21 +42,25 @@ class Move:
return self.coords == other.coords and self.player == other.player
def gtp(self):
"""Returns GTP coordinates of the move"""
if self.is_pass:
return "pass"
return Move.GTP_COORD[self.coords[0]] + str(self.coords[1] + 1)
def sgf(self, board_size):
"""Returns SGF coordinates of the move"""
if self.is_pass:
return ""
return f"{Move.SGF_COORD[self.coords[0]]}{Move.SGF_COORD[board_size[1] - self.coords[1] - 1]}"
@property
def is_pass(self):
"""Returns True if the move is a pass"""
return self.coords is None
@property
def opponent(self):
"""Returns the opposing player, i.e. W <-> B"""
return "W" if self.player == "B" else "B"
@@ -114,6 +123,7 @@ class SGFNode:
@property
def parent(self) -> Optional["SGFNode"]:
"""Returns the parent node"""
return self._parent
@parent.setter
@@ -123,13 +133,15 @@ class SGFNode:
self._depth = None
@property
def root(self) -> "SGFNode": # cached root property
def root(self) -> "SGFNode":
"""Returns the root of the tree, cached for speed"""
if self._root is None:
self._root = self.parent.root if self.parent else self
return self._root
@property
def depth(self) -> int: # cached depth property
def depth(self) -> int:
"""Returns the depth of this node, where root is 0, cached for speed"""
if self._depth is None:
if self.is_root:
self._depth = 0
@@ -137,9 +149,9 @@ class SGFNode:
self._depth = self.parent.depth + 1
return self._depth
# some root properties are available on any node
@property
def board_size(self) -> Tuple[int, int]:
"""Retrieves the root's SZ property, or 19 if missing. Parses it, and returns board size as a tuple x,y"""
size = str(self.root.get_property("SZ", "19"))
if ":" in size:
x, y = map(int, size.split(":"))
@@ -150,15 +162,17 @@ class SGFNode:
@property
def komi(self) -> float:
"""Retrieves the root's KM property, or 6.5 if missing"""
return float(self.root.get_property("KM", 6.5))
@property
def ruleset(self) -> str:
return self.root.get_property("RU")
"""Retrieves the root's RU property, or 'japanese' if missing"""
return self.root.get_property("RU", "japanese")
@property
def moves(self) -> List[Move]:
"""Returns all moves in the node."""
"""Returns all moves in the node - typically 'move' will be better."""
return [Move.from_sgf(move, player=pl, board_size=self.board_size) for pl in Move.PLAYERS for move in self.get_list_property(pl, [])]
@property
@@ -172,30 +186,35 @@ class SGFNode:
return self.placements + self.moves
@property
def single_move(self) -> Optional[Move]:
def move(self) -> Optional[Move]:
"""Returns the single move for the node if one exists, or None if no moves (or multiple ones) exist."""
moves = self.moves
if len(moves) == 1: # TODO: and not placements?
if len(moves) == 1:
return moves[0]
@property
def is_root(self) -> bool:
"""Returns true if node is a root"""
return self.parent is None
@property
def is_pass(self) -> bool:
return not self.placements and self.single_move and self.single_move.is_pass
"""Returns true if associated move is pass"""
return not self.placements and self.move and self.move.is_pass
@property
def empty(self) -> bool:
"""Returns true if node has no children or properties"""
return not self.children and not self.properties
@property
def nodes_in_tree(self) -> List:
"""Returns all nodes in the tree rooted at this node"""
return [self] + sum([c.nodes_in_tree for c in self.children], [])
@property
def nodes_from_root(self) -> List:
"""Returns all nodes from the root up to this node, i.e. the moves played in the current branch of the game"""
nodes = [self]
n = self
while not n.is_root:
@@ -206,43 +225,49 @@ class SGFNode:
def play(self, move) -> "SGFNode":
"""Either find an existing child or create a new one with the given move."""
for c in self.children:
if c.single_move == move:
if c.move == move:
return c
return self.__class__(parent=self, move=move)
@property
def next_player(self):
if "B" in self.properties or "AB" in self.properties: # root or black moved
"""Returns player to move"""
if "B" in self.properties or "AB" in self.properties: # root or black moved
return "W"
else:
return "B"
@property
def player(self):
"""Returns player that moved last. nb root is considered white played if no handicap stones are placed"""
if "B" in self.properties or "AB" in self.properties:
return "B"
else:
return "W" # nb root is considered white played if no handicap stones are placed
return "W"
class SGF:
"""Class used for SGF Nodes, can change this to something that inherits from SGFNode"""
_NODE_CLASS = SGFNode
@classmethod
def parse(cls, input_str) -> SGFNode:
"""Parse a string as SGF."""
return cls(input_str).root
@classmethod
def parse_file(cls, filename, encoding=None) -> SGFNode:
"""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")
encoding = match[1].decode("ascii", errors="ignore")
else:
encoding = "ISO-8859-1" # default
decoded = bin_contents.decode(encoding=encoding)
decoded = bin_contents.decode(encoding=encoding, errors="ignore")
return cls.parse(decoded)
def __init__(self, contents):
+59 -52
View File
@@ -25,7 +25,7 @@ class BadukPanWidget(Widget):
self.grid_size = 0
self.stone_size = 0
self.last_eval = 0
self.active_hints = []
self.active_pv_moves = []
self.show_pv_for = None
self.redraw_board_contents_trigger = Clock.create_trigger(self.draw_board_contents)
Window.bind(mouse_pos=self.on_mouse_pos)
@@ -60,10 +60,10 @@ class BadukPanWidget(Widget):
pos = args[1]
rel_pos = self.to_widget(*pos) # compensate for relative layout
inside = self.collide_point(*rel_pos)
if inside and self.active_hints:
if inside and self.active_pv_moves:
near_move = [
move
for move in self.active_hints
for move in self.active_pv_moves
if abs(rel_pos[0] - self.gridpos_x[move[0]]) < self.grid_size / 2 and abs(rel_pos[1] - self.gridpos_y[move[1]]) < self.grid_size / 2
]
if near_move:
@@ -81,7 +81,7 @@ class BadukPanWidget(Widget):
xd, xp = self._find_closest(touch.x, self.gridpos_x)
yd, yp = self._find_closest(touch.y, self.gridpos_y)
nodes_here = [node for node in katrain.game.current_node.nodes_from_root if node.single_move and node.single_move.coords == (xp, yp)]
nodes_here = [node for node in katrain.game.current_node.nodes_from_root if node.move and node.move.coords == (xp, yp)]
if nodes_here and max(yd, xd) < self.grid_size / 2: # load old comment
if touch.is_double_tap: # navigate to move
katrain.game.set_current_node(nodes_here[-1])
@@ -89,8 +89,7 @@ class BadukPanWidget(Widget):
else: # load comments
katrain.log(f"\nAnalysis:\n{nodes_here[-1].analysis}", OUTPUT_DEBUG)
katrain.log(f"\nParent Analysis:\n{nodes_here[-1].parent.analysis}", OUTPUT_DEBUG)
katrain.controls.info.text = nodes_here[-1].comment(sgf=True)
katrain.controls.info.text = nodes_here[-1].comment(sgf=True)
katrain.controls.info.text = nodes_here[-1].comment(sgf=True, interactive=True)
self.ghost_stone = None
self.draw_hover_contents() # remove ghost
@@ -176,6 +175,8 @@ class BadukPanWidget(Widget):
outline_color = self.ui_config["outline"]
katrain = self.katrain
board_size_x, board_size_y = katrain.game.board_size
lock_ai = self.trainer_config.get("lock_ai", False) and katrain.controls.teaching_mode_enabled()
show_n_eval = self.trainer_config["eval_off_show_last"]
self.canvas.clear()
with self.canvas:
@@ -188,7 +189,6 @@ class BadukPanWidget(Widget):
for m in katrain.game.stones:
has_stone[m.coords] = m.player
show_n_eval = self.trainer_config["eval_off_show_last"]
show_dots_for = {p: self.trainer_config["eval_show_ai"] or "ai" not in katrain.controls.player_mode(p) for p in Move.PLAYERS}
nodes = katrain.game.current_node.nodes_from_root
realized_points_lost = None
@@ -239,7 +239,7 @@ class BadukPanWidget(Widget):
pass_btn = katrain.board_controls.pass_btn
pass_btn.canvas.after.clear()
if katrain.controls.policy.active and policy:
if katrain.controls.policy.active and policy and not lock_ai:
policy_grid = var_to_grid(policy, (board_size_x, board_size_y))
best_move_policy = max(*policy)
for y in range(board_size_y - 1, -1, -1):
@@ -255,23 +255,6 @@ class BadukPanWidget(Widget):
with pass_btn.canvas.after:
draw_circle((pass_btn.pos[0] + pass_btn.width / 2, pass_btn.pos[1] + pass_btn.height / 2), polsize * pass_btn.height / 2, self.ui_config["policy_color"])
# children of current moves in undo / review
undo_coords = set()
if katrain.controls.show_children.active:
alpha = self.ui_config["ghost_alpha"]
for child_node in current_node.children:
points_lost = child_node.points_lost
m = child_node.single_move
if m and m.coords is not None:
undo_coords.add(m.coords)
if points_lost is None:
evalcol = None
else:
evalcol = copy.copy(self.eval_color(points_lost))
evalcol[3] = alpha
scale = self.ui_config["child_scale"]
self.draw_stone(m.coords[0], m.coords[1], (*stone_color[m.player][:3], alpha), None, None, evalcol, evalscale=scale, scale=scale)
# pass circle
passed = len(nodes) > 1 and current_node.is_pass
if passed:
@@ -295,49 +278,73 @@ class BadukPanWidget(Widget):
current_node = katrain.game.current_node
player, next_player = current_node.player, current_node.next_player
stone_color = self.ui_config["stones"]
teaching = katrain.controls.teaching_mode_enabled()
lock_ai = self.trainer_config.get("lock_ai", False) and teaching
self.canvas.after.clear()
with self.canvas.after:
self.active_pv_moves = []
# children of current moves in undo / review
if katrain.controls.show_children.active:
alpha = self.ui_config["ghost_alpha"]
for child_node in current_node.children:
points_lost = child_node.points_lost
move = child_node.move
if move and move.coords is not None:
if points_lost is None:
evalcol = None
else:
evalcol = copy.copy(self.eval_color(points_lost))
evalcol[3] = alpha
self.active_pv_moves.append(move.coords)
if teaching and move.coords == self.show_pv_for and child_node.analysis_ready:
self.draw_pv(katrain, [move.gtp()] + child_node.candidate_moves[0].get("pv", []), [next_player, player])
else:
scale = self.ui_config["child_scale"]
self.draw_stone(move.coords[0], move.coords[1], (*stone_color[move.player][:3], alpha), None, None, evalcol, evalscale=scale, scale=scale)
# hints or PV
self.active_hints = []
if katrain.controls.hints.active and not game_ended:
if katrain.controls.hints.active and not game_ended and not lock_ai:
hint_moves = current_node.candidate_moves
for i, d in enumerate(hint_moves):
move = Move.from_gtp(d["move"])
if move.coords is not None: # and move.coords not in undo_coords:
for i, move_dict in enumerate(hint_moves):
move = Move.from_gtp(move_dict["move"])
if move.coords is not None:
alpha, scale = self.ui_config["ghost_alpha"], 1.0
if i == 0:
alpha += self.ui_config["top_move_x_alpha"]
elif d["visits"] < self.ui_config["visit_frac_small"] * hint_moves[0]["visits"]:
elif move_dict["visits"] < self.ui_config["visit_frac_small"] * hint_moves[0]["visits"]:
scale = 0.8
self.active_hints.append(move.coords)
if move.coords == self.show_pv_for: # TODO: overlapping moves
pv = d.get("pv", [move.gtp()]) # if empty, show current move at least
for i, gtpmove in enumerate(pv):
move_player = [next_player, player][i % 2]
opp_player = [next_player, player][1 - i % 2]
coords = Move.from_gtp(gtpmove).coords
if coords is None: # tee-hee
sizefac = katrain.board_controls.pass_btn.size[1] / 2 / self.stone_size
board_coords = [
katrain.board_controls.pass_btn.pos[0] + katrain.board_controls.pass_btn.size[0] + self.stone_size * sizefac,
katrain.board_controls.pass_btn.pos[1] + katrain.board_controls.pass_btn.size[1] / 2,
]
else:
board_coords = (self.gridpos_x[coords[0]], self.gridpos_y[coords[1]])
sizefac = 0.95
draw_circle(board_coords, self.stone_size * sizefac, stone_color[move_player])
Color(*stone_color[opp_player])
draw_text(pos=board_coords, text=str(i + 1), font_size=sizefac * self.grid_size / 1.45)
self.active_pv_moves.append(move.coords)
if move.coords == self.show_pv_for:
self.draw_pv(katrain, move_dict.get("pv", [move.gtp()]), [next_player, player]) # if empty, show current move at least
elif not self.show_pv_for:
self.draw_stone(move.coords[0], move.coords[1], [*self.eval_color(d["pointsLost"])[:3], alpha], scale=scale)
self.draw_stone(move.coords[0], move.coords[1], [*self.eval_color(move_dict["pointsLost"])[:3], alpha], scale=scale)
# hover next move ghost stone
if self.ghost_stone:
self.draw_stone(*self.ghost_stone, (*stone_color[next_player], ghost_alpha))
def draw_pv(self, katrain, pv, next_last_player):
# TODO: overlapping moves
stone_color = self.ui_config["stones"]
for i, gtpmove in enumerate(pv):
move_player = next_last_player[i % 2]
opp_player = next_last_player[1 - i % 2]
coords = Move.from_gtp(gtpmove).coords
if coords is None: # tee-hee
sizefac = katrain.board_controls.pass_btn.size[1] / 2 / self.stone_size
board_coords = [
katrain.board_controls.pass_btn.pos[0] + katrain.board_controls.pass_btn.size[0] + self.stone_size * sizefac,
katrain.board_controls.pass_btn.pos[1] + katrain.board_controls.pass_btn.size[1] / 2,
]
else:
board_coords = (self.gridpos_x[coords[0]], self.gridpos_y[coords[1]])
sizefac = 0.95
draw_circle(board_coords, self.stone_size * sizefac, stone_color[move_player])
Color(*stone_color[opp_player])
draw_text(pos=board_coords, text=str(i + 1), font_size=sizefac * self.grid_size / 1.45)
class BadukPanControls(BoxLayout):
pass
+6 -3
View File
@@ -36,6 +36,9 @@ class Controls(BoxLayout):
def ai_mode(self, player):
return self.ai_mode_groups[player].text
def teaching_mode_enabled(self):
return "undo" in self.player_mode("B") or "undo" in self.player_mode("W")
def on_size(self, *args):
self.update_evaluation()
@@ -51,7 +54,7 @@ class Controls(BoxLayout):
info = ""
if current_node:
move = current_node.single_move
move = current_node.move
both_players_are_robots = "ai" in self.player_mode(current_node.player) and "ai" in self.player_mode(current_node.next_player)
next_player_is_human_or_both_robots = current_node.player and ("ai" not in self.player_mode(current_node.player) or both_players_are_robots)
current_player_is_ai_playing_human = current_node.player and "ai" in self.player_mode(current_node.player) and "ai" not in self.player_mode(current_node.next_player)
@@ -70,10 +73,10 @@ class Controls(BoxLayout):
elif not current_player_is_ai_playing_human:
self.score_change.label = f"Points lost"
self.score_change.text = ""
elif current_player_is_ai_playing_human and current_node.parent and current_node.parent.single_move:
elif current_player_is_ai_playing_human and current_node.parent and current_node.parent.move:
points_lost = current_node.parent.points_lost
self.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained"
self.score_change.text = f"{current_node.parent.single_move.player}: {abs(points_lost):.1f}" if points_lost else "..."
self.score_change.text = f"{current_node.parent.move.player}: {abs(points_lost):.1f}" if points_lost else "..."
elif both_players_are_robots and current_node.parent and current_node.parent.analysis_ready:
self.score.text = current_node.parent.format_score()
self.win_rate.text = current_node.parent.format_win_rate()
+5
View File
@@ -80,9 +80,14 @@ class BackgroundLabel(Label, BackgroundColor):
class ScrollableLabel(ScrollView):
__events__ = ["on_ref_press"]
text = StringProperty("")
markup = BooleanProperty(False)
border_color = ListProperty([0, 0, 0, 1])
def on_ref_press(self, ref):
pass
class StyledButton(Button, ToolTipBehavior):
button_color = ListProperty([])
+5
View File
@@ -179,6 +179,7 @@ class ConfigPopup(QuickConfigGui):
if not old_proc:
self.katrain.game.analyze_all_nodes() # old engine was broken, so make sure we redo any failures
self.katrain.update_state()
Clock.schedule_once(restart_engine, 0)
self.katrain.debug_level = self.config["debug"]["level"]
@@ -275,6 +276,10 @@ class ConfigTeacherPopup(QuickConfigGui):
xsettings.add_widget(ScaledLightLabel(text="Show dots/SGF comments for AI players"))
xsettings.add_widget(LabelledCheckBox(size_hint=(0.5, 1), text=str(self.settings["eval_show_ai"]), input_property="eval_show_ai"))
self.add_widget(xsettings)
xsettings = BoxLayout(size_hint=(1, 0.15), spacing=2)
xsettings.add_widget(ScaledLightLabel(text="Disable analysis while in teach mode"))
xsettings.add_widget(LabelledCheckBox(size_hint=(0.5, 1), text=str(self.settings["lock_ai"]), input_property="lock_ai"))
self.add_widget(xsettings)
bl = BoxLayout(size_hint=(1, 0.15), spacing=2)
bl.add_widget(StyledButton(text=f"Apply", on_press=lambda _: self.update_config(False)))
+4
View File
@@ -402,6 +402,7 @@
width:1
Label:
id: label
markup: root.markup
padding: 5, 5
font_size: dp(18)
color: BLACK
@@ -409,6 +410,7 @@
height: self.texture_size[1]
text_size: self.width, None
text: root.text
on_ref_press: root.dispatch('on_ref_press',args[1])
<Controls>:
orientation: 'vertical'
@@ -626,6 +628,8 @@
height: 0.00001 if not self.text else min(0.66*self.parent.height,self.label.texture_size[1])
ScrollableLabel:
id: info
# markup: True
# on_ref_press: root.katrain.board_gui.show_pv_from_label(args[1])
size_hint: 1, None
height: self.parent.height - status_label.height - 1
BoxLayout:
+1 -1
View File
@@ -33,7 +33,7 @@ exe = EXE(pyz,
upx=True,
console=True) # , icon='..\\icon.png'
EXCLUDE_SUFFIX = ['katago-bs','b10-1.3.txt.gz','katago.exe','screenshot_analyze.png','screenshot_play.png']
EXCLUDE_SUFFIX = ['katago','b10-1.3.txt.gz','screenshot_analyze.png','screenshot_play.png']
EXCLUDE = ['KataGoData']
a.datas = [(ff,ft,tp) for ff,ft,tp in a.datas if not any(ff.endswith(suffix) for suffix in EXCLUDE_SUFFIX) and not any(kw in ff for kw in EXCLUDE)]
coll = COLLECT(exe,