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:
30 files changed
+185
-113
No files matched your search
Binary file not shown.
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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:
|
||||
|
||||
@@ -18,7 +18,7 @@ ENGINE_SETTINGS = {
|
||||
"max_visits": 50,
|
||||
"max_time": 1.0,
|
||||
"enable_ownership": False,
|
||||
"threads": 8,
|
||||
"threads": 32,
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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([])
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user