diff --git a/TODO b/TODO index daa407c..9af0a97 100644 --- a/TODO +++ b/TODO @@ -14,25 +14,24 @@ [x] config player to sep. row/popups? [x] dots: teacher settings fixes [x] ogs bot connector +[x] clarify score change vs score +[x] List edit settings/object edit settings? -> Mostly superseded by popups. [/] README [] engine status [] sgf review improvements -- Likewise, in the 0.9 version, better alternatives to the played move were shown with squares, which was also pretty useful when using the sgf outside of Katrain. I mean, having the top move mentioned is all and good, but when you see multiple squares shown on the board as better alternatives to the move played in the game, it makes obvious how far from perfect that move actually was :D +[] selective save dots / config [] Release notes - -[] clarify score change vs score +[] box to label ? split in status and comment? [] pv with overlap? - Low priority [] more teaching / groups in danger? hard -[] box to label ? split in status and comment? -[] dual engine support -- easily possible but has weird effects on win rate etc [] When creating a new game, the 9 buttons on the right side aren't all that useful. Maybe the 9, 13 and 19 ones make sense since these three board sizes are the traditionally used ones, but why 2, 4 and 9 stones buttons? Why 0.5, 6.5 and -40pts komi buttons? [] Score instead of game end Wont do for now -[] List edit settings/object edit settings? +[] dual engine support -- easily possible but has weird effects on win rate etc [] Currently, when saving an SGF after analyzing a game, Katrain stores data for every move by default. Like for dots, it would be great if it was possible to only store data for moves that we were interested in (e.g. only from this or that player, and/or only the most inefficient moves/most costly mistakes, etc.) [] Typing something in the comments window freezes Katrain -> probably keyboard shortcuts / solved by label [] Plus, you wouldn't need "B" and "W" beside these numbers if the top part of the window was black with the graph being white on top of it (and the bottom White with the graph being black as they are currently). diff --git a/ai.py b/ai.py index 88a8853..1df3b37 100644 --- a/ai.py +++ b/ai.py @@ -2,13 +2,13 @@ import heapq import math import random import time -from typing import Dict, List, Tuple, Any +from typing import Any, Dict, List, Tuple import numpy as np -from common import OUTPUT_INFO, var_to_grid, OUTPUT_DEBUG, OUTPUT_ERROR +from common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, var_to_grid from engine import EngineDiedException -from game import Move, Game, IllegalMoveException, GameNode +from game import Game, GameNode, IllegalMoveException, Move def weighted_selection_without_replacement(items: List[Tuple[float, float, int, int]], pick_n: int) -> List[Tuple[float, float, int, int]]: @@ -71,10 +71,11 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode elif "p:" in ai_mode: n_moves = int(ai_settings["pick_frac"] * len(legal_policy_moves) + ai_settings["pick_n"]) if "influence" in ai_mode or "territory" in ai_mode: + thr_line = ai_settings["threshold"] - 1 # zero-based if "influence" in ai_mode: - weight = lambda x, y: (1 / ai_settings["line_weight"]) ** max(0, 3 - min(size[0] - 1 - x, x, y, size[1] - 1 - y)) + weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (max(0, thr_line - min(size[0] - 1 - x, x)) + max(0, thr_line - min(size[1] - 1 - y, y))) else: - weight = lambda x, y: (1 / ai_settings["line_weight"]) ** max(0, min(size[0] - 1 - x, x, y, size[1] - 1 - y) - 2) + weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (max(0, min(size[0] - 1 - x, x, size[1] - 1 - y, y) - thr_line)) weighted_coords = [(policy_grid[y][x] * weight(x, y), weight(x, y), x, y) for x in range(size[0]) for y in range(size[1]) if policy_grid[y][x] > 0] ai_thoughts += f"Generated weights for {ai_mode} according to weight factor {ai_settings['line_weight']} and distance from 4th line. " elif "local" in ai_mode or "tenuki" in ai_mode: diff --git a/ai2gtp.py b/ai2gtp.py index 8c9a184..be006e2 100644 --- a/ai2gtp.py +++ b/ai2gtp.py @@ -1,11 +1,13 @@ # This is a script that turns a KaTrain AI into a sort-of GTP compatible bot import json -import time, sys +import sys +import time import traceback -from game import Game, Move + from ai import ai_move -from engine import KataGoEngine, EngineDiedException -from common import OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_DEBUG, bot_strategy_names +from common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, bot_strategy_names +from engine import EngineDiedException, KataGoEngine +from game import Game, Move from sgf_parser import Move DB_FILENAME = "ai_performance.pickle" @@ -14,6 +16,7 @@ if len(sys.argv) < 2: bot = "dev" else: bot = sys.argv[1].strip() +port = int(sys.argv[2]) if len(sys.argv) > 2 else 8587 class Logger: @@ -24,9 +27,10 @@ class Logger: logger = Logger() + ENGINE_SETTINGS = { # "katago": "../KataGo/cpp/katago", - "katago": "python engine_connector.py 2222", # actual engine settings in engine_server.py + "katago": f"python engine_connector.py {port}", # actual engine settings in engine_server.py "model": "models/b15-1.3.2.txt.gz", "config": "KataGo/analysis_config.cfg", "max_visits": 5, @@ -34,7 +38,7 @@ ENGINE_SETTINGS = { "enable_ownership": False, "threads": 1, } -ai_settings = {"noise_strength": 0.8, "pick_n": 10, "pick_frac": 0.2, "stddev": 10, "line_weight": 10, "pick_override": 0.95} + engine = KataGoEngine(logger, ENGINE_SETTINGS) @@ -47,6 +51,9 @@ all_ai_settings["dev"] = all_ai_settings["P:Noise"] ai_strategy = bot_strategy_names[bot] ai_settings = all_ai_settings[ai_strategy] +print(f"starting bot {bot} using server port {port}", file=sys.stderr) +print(ENGINE_SETTINGS, file=sys.stderr) +print(ai_strategy, ai_settings, file=sys.stderr) logger.log(f"STARTED ENGINE", OUTPUT_ERROR) diff --git a/common.py b/common.py index 0ee093c..efe7617 100644 --- a/common.py +++ b/common.py @@ -1,4 +1,4 @@ -from typing import List, Any, Tuple +from typing import Any, List, Tuple OUTPUT_ERROR = -1 OUTPUT_INFO = 0 @@ -26,3 +26,10 @@ def var_to_grid(array_var: List[Any], size: Tuple[int, int]) -> List[List[Any]]: grid[y] = array_var[ix : ix + size[0]] ix += size[0] return grid + + +def evaluation_class(points_lost, eval_thresholds): + i = 0 + while i < len(eval_thresholds) - 1 and points_lost < eval_thresholds[i]: + i += 1 + return i diff --git a/config.json b/config.json index 51cdb84..bc34f99 100644 --- a/config.json +++ b/config.json @@ -99,16 +99,18 @@ "pick_override": 0.95, "pick_n": 5, "pick_frac": 0.4, + "threshold": 3.5, "line_weight": 10, - "_help_right": "Samples `pick_n + pick_frac * ` and plays the best one, biased towards the center.", + "_help_right": "Samples `pick_n + pick_frac * ` and plays the best one, biased to above the `threshold` line.", "_help_left": "Increase `line_weight` to penalize moves near the edge more." }, "P:Territory": { "pick_override": 0.95, "pick_n": 5, "pick_frac": 0.4, + "threshold": 3.5, "line_weight": 5, - "_help_right": "Samples `pick_n + pick_frac * ` and plays the best one, biased towards the edge.", + "_help_right": "Samples `pick_n + pick_frac * ` and plays the best one, biased to below the `threshold` line.", "_help_left": "Increase `line_weight` to penalize moves closer to the center more." } }, @@ -116,6 +118,8 @@ "starpoint_size": 0.1, "stone_size": 0.475, "eval_dot_max_size": 0.5, + "eval_dot_min_size": 0.25, + "visit_frac_small": 0.1, "stones": { "B": [ 0.05, diff --git a/engine.py b/engine.py index 314039c..dc88f50 100644 --- a/engine.py +++ b/engine.py @@ -75,8 +75,8 @@ class KataGoEngine: continue analysis = json.loads(line) if analysis["id"] in self.queries: - id = analysis["id"] - callback, error_callback, start_time, next_move = self.queries[id] + query_id = analysis["id"] + callback, error_callback, start_time, next_move = self.queries[query_id] else: self.katrain.log(f"Query result {analysis['id']} discarded -- recent new game?", OUTPUT_DEBUG) continue @@ -87,12 +87,15 @@ class KataGoEngine: self.katrain.log(f"{analysis} received from KataGo", OUTPUT_ERROR) continue else: - callback, error_callback, start_time, next_move = self.queries[id] + callback, error_callback, start_time, next_move = self.queries[query_id] time_taken = time.time() - start_time self.katrain.log(f"[{time_taken:.1f}][{analysis['id']}] KataGo Analysis Received: {analysis.keys()}", OUTPUT_DEBUG) self.katrain.log(line, OUTPUT_EXTRA_DEBUG) - callback(analysis) - del self.queries[id] + del self.queries[query_id] + try: + callback(analysis) + except Exception as e: + self.katrain.log(f"Error in engine callback for query {query_id}: {e}", OUTPUT_ERROR) if getattr(self.katrain, "update_state", None): # easier mocking etc self.katrain.update_state() diff --git a/engine_server.py b/engine_server.py index f4784ad..707304a 100644 --- a/engine_server.py +++ b/engine_server.py @@ -1,10 +1,13 @@ # used to scale bots -import socket, threading -import traceback, sys, json +import json import random +import socket +import sys +import threading +import traceback +from common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO from engine import KataGoEngine -from common import OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_DEBUG PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8587 diff --git a/game.py b/game.py index 9fedf18..f0845c0 100644 --- a/game.py +++ b/game.py @@ -1,9 +1,9 @@ import math import os import re -from datetime import datetime -from typing import List, Union, Dict import threading +from datetime import datetime +from typing import Dict, List, Union from common import var_to_grid from engine import KataGoEngine @@ -206,8 +206,12 @@ class Game: game_name = f"katrain_{black} vs {white} {self.game_id}" file_name = os.path.join(path, f"{game_name}.sgf") os.makedirs(os.path.dirname(file_name), exist_ok=True) + + show_dots_for = {p: self.katrain.config("trainer/eval_show_ai") or "ai" not in self.katrain.controls.player_mode(p) for p in Move.PLAYERS} + thresholds = self.katrain.config("trainer/eval_thresholds") + sgf = self.root.sgf(save_comments_player=show_dots_for, save_comments_class=self.katrain.config("sgf/save_feedback"), eval_thresholds=thresholds) with open(file_name, "w") as f: - f.write(self.root.sgf()) + f.write(sgf) return f"SGF with analysis written to {file_name}" def analyze_extra(self, mode): diff --git a/game_node.py b/game_node.py index 1cd2dbd..8c081f7 100644 --- a/game_node.py +++ b/game_node.py @@ -2,7 +2,7 @@ import copy import random from typing import Dict, List, Optional, Tuple -from common import var_to_grid +from common import evaluation_class, var_to_grid from sgf_parser import Move, SGFNode @@ -19,15 +19,19 @@ class GameNode(SGFNode): self.move_number = 0 self.undo_threshold = random.random() # for fractional undos, store the random threshold in the move itself for consistency - @property - def sgf_properties(self): - best_sq = [] - properties = copy.copy(super().sgf_properties) - if best_sq and "SQ" not in properties: - properties["SQ"] = best_sq - comment = self.comment(sgf=True) - if comment: - properties["C"] = [properties.get("C", "") + comment] + def sgf_properties(self, save_comments_player, save_comments_player_class, eval_thresholds): + properties = copy.copy(super().sgf_properties()) + if self.points_lost: + show_class = save_comments_player_class[evaluation_class(self.points_lost, eval_thresholds)] + else: + show_class = False + if save_comments_player.get(self.player, False) and show_class: + best_sq = [] + if best_sq and "SQ" not in properties: + properties["SQ"] = best_sq + comment = self.comment(sgf=True) + if comment: + properties["C"] = [properties.get("C", "") + comment] return properties # various analysis functions diff --git a/gui/badukpan.py b/gui/badukpan.py index 480a168..7dee3e0 100644 --- a/gui/badukpan.py +++ b/gui/badukpan.py @@ -6,7 +6,7 @@ from kivy.properties import ListProperty from kivy.uix.boxlayout import BoxLayout from kivy.uix.widget import Widget -from common import OUTPUT_DEBUG +from common import OUTPUT_DEBUG, evaluation_class from game import Move from gui.kivyutils import draw_circle, draw_text from common import var_to_grid @@ -92,7 +92,7 @@ class BadukPanWidget(Widget): self.draw_hover_contents() # remove ghost # drawing functions - def on_size(self, *args): + def on_size(self, *_args): self.draw_board() self.draw_board_contents() @@ -112,13 +112,11 @@ class BadukPanWidget(Widget): def eval_color(self, points_lost): colors = self.ui_config["eval_colors"] - thresholds = self.trainer_config["eval_thresholds"] - i = 0 - while i < len(thresholds) and points_lost < thresholds[i]: - i += 1 - return colors[min(i, len(colors) - 1)] + i = evaluation_class(points_lost, self.trainer_config["eval_thresholds"]) + print(i, colors) + return colors[i] - def draw_board(self, *args): + def draw_board(self, *_args): if not self.ui_config: return katrain = self.katrain @@ -167,7 +165,7 @@ class BadukPanWidget(Widget): for i in range(board_size_y): draw_text(pos=(self.gridpos_x[0] - coord_offset, self.gridpos_y[i]), text=str(i + 1), font_size=self.grid_size / 1.5) - def draw_board_contents(self, *args): + def draw_board_contents(self, *_args): if not self.ui_config: return stone_color = self.ui_config["stones"] @@ -194,7 +192,7 @@ class BadukPanWidget(Widget): evalsize = 1 for m in node.move_with_placements: if has_stone.get(m.coords) and not drawn_stone.get(m.coords): # skip captures, last only for - move_eval_on = full_eval_on or (i < show_n_eval and show_dots_for.get(m.player)) + move_eval_on = show_dots_for.get(m.player) and (i < show_n_eval or full_eval_on) if move_eval_on and points_lost is not None: evalcol = self.eval_color(points_lost) else: @@ -212,7 +210,7 @@ class BadukPanWidget(Widget): self.draw_stone(6, c, stone_color["B"], outline_color["B"], stone_color["W"], evalcol, evalsize) self.draw_stone(7, c, stone_color["W"], outline_color["W"], None, evalcol, evalsize) self.draw_stone(8, c, stone_color["W"], outline_color["W"], stone_color["B"], evalcol, evalsize) - self.draw_stone(9, c, [*evalcol, 0.5], scale=0.8) + self.draw_stone(9, c, [*evalcol[:3], 0.5], scale=0.8) # ownership - allow one move out of date for smooth animation ownership = current_node.ownership or (current_node.parent and current_node.parent.ownership) @@ -234,7 +232,7 @@ class BadukPanWidget(Widget): pass_btn = katrain.board_controls.pass_btn pass_btn.canvas.after.clear() if katrain.controls.policy.active and policy: - policy_grid = var_to_grid(policy, [board_size_x, board_size_y]) + 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): for x in range(board_size_x): @@ -278,7 +276,7 @@ class BadukPanWidget(Widget): self.draw_hover_contents() - def draw_hover_contents(self, *args): + def draw_hover_contents(self, *_args): ghost_alpha = self.ui_config["ghost_alpha"] katrain = self.katrain game_ended = katrain.game.ended @@ -299,7 +297,7 @@ class BadukPanWidget(Widget): alpha, scale = self.ui_config["ghost_alpha"], 1.0 if i == 0: alpha += self.ui_config["top_move_x_alpha"] - elif d["visits"] < 0.1 * hint_moves[0]["visits"]: # TODO: config? + elif d["visits"] < self.ui_config["visit_frac_small"] * hint_moves[0]["visits"]: scale = 0.8 self.active_hints.append(move.coords) @@ -322,7 +320,7 @@ class BadukPanWidget(Widget): Color(*stone_color[opp_player]) draw_text(pos=board_coords, text=str(i + 1), font_size=sizefac * self.grid_size / 1.45) elif not self.show_pv_for: - self.draw_stone(move.coords[0], move.coords[1], [*self.eval_color(d["pointsLost"]), alpha], scale=scale) + self.draw_stone(move.coords[0], move.coords[1], [*self.eval_color(d["pointsLost"])[:3], alpha], scale=scale) # hover next move ghost stone if self.ghost_stone: diff --git a/gui/controls.py b/gui/controls.py index 31a8d6c..72727d8 100644 --- a/gui/controls.py +++ b/gui/controls.py @@ -1,5 +1,3 @@ -from kivy.graphics.context_instructions import Color -from kivy.graphics.vertex_instructions import Line, SmoothLine from kivy.uix.boxlayout import BoxLayout from kivy.uix.popup import Popup @@ -15,7 +13,7 @@ class Controls(BoxLayout): def set_status(self, msg, at_node=None): self.status = msg self.status_node = at_node or self.katrain.game and self.katrain.game.current_node - self.info.text = msg + self.status_label.text = msg self.update_evaluation() def select_mode(self, mode): @@ -44,12 +42,12 @@ class Controls(BoxLayout): katrain = self.katrain current_node = katrain.game and katrain.game.current_node - info = "" - if current_node is self.status_node or (self.status is not None and self.status_node is None and current_node.is_root): # startup errors on root - info += self.status + "\n" - else: + if current_node is not self.status_node and not (self.status is not None and self.status_node is None and current_node.is_root): # startup errors on root + self.status_label.text = "" self.status_node = None + info = "" + if current_node: move = current_node.single_move next_player_is_human_or_both_robots = current_node.player and ("ai" not in self.player_mode(current_node.player) or "ai" in self.player_mode(current_node.next_player)) diff --git a/gui/kivyutils.py b/gui/kivyutils.py index 4e96a13..4d7a66d 100644 --- a/gui/kivyutils.py +++ b/gui/kivyutils.py @@ -13,6 +13,7 @@ from kivy.uix.button import Button from kivy.uix.checkbox import CheckBox from kivy.uix.gridlayout import GridLayout from kivy.uix.label import Label +from kivy.uix.scrollview import ScrollView from kivy.uix.spinner import Spinner from kivy.uix.textinput import TextInput from kivy.uix.widget import Widget @@ -38,6 +39,11 @@ class BackgroundLabel(Label, BackgroundColor): pass +class ScrollableLabel(ScrollView): + text = StringProperty("") + border_color = ListProperty([0, 0, 0, 1]) + + class StyledButton(Button): button_color = ListProperty([]) button_color_down = ListProperty([]) diff --git a/gui/popups.py b/gui/popups.py index 6f70762..3dbcc79 100644 --- a/gui/popups.py +++ b/gui/popups.py @@ -1,9 +1,11 @@ from collections import defaultdict +from typing import Dict, List, DefaultDict from kivy.clock import Clock from kivy.uix.boxlayout import BoxLayout from kivy.uix.gridlayout import GridLayout from kivy.uix.label import Label +from kivy.uix.popup import Popup from common import OUTPUT_DEBUG, OUTPUT_ERROR from engine import KataGoEngine @@ -27,7 +29,7 @@ class InputParseError(Exception): class QuickConfigGui(BoxLayout): - def __init__(self, katrain, popup, initial_values=None, **kwargs): + def __init__(self, katrain: "KaTrainGui", popup: Popup, initial_values: Dict = None, **kwargs): super().__init__(**kwargs) self.katrain = katrain self.popup = popup @@ -74,7 +76,7 @@ class LoadSGFPopup(BoxLayout): class NewGamePopup(QuickConfigGui): - def __init__(self, katrain, popup, properties, **kwargs): + def __init__(self, katrain: "KaTrainGui", popup: Popup, properties: Dict, **kwargs): properties["RU"] = KataGoEngine.get_rules(katrain.game.root) super().__init__(katrain, popup, properties, **kwargs) self.rules_spinner.values = list(set(self.katrain.engine.RULESETS.values())) @@ -93,7 +95,7 @@ class NewGamePopup(QuickConfigGui): class ConfigPopup(QuickConfigGui): - def __init__(self, katrain, popup, config, ignore_cats, **kwargs): + def __init__(self, katrain: "KaTrainGui", popup: Popup, config: Dict, ignore_cats: List, **kwargs): self.config = config self.ignore_cats = ignore_cats self.orientation = "vertical" @@ -134,7 +136,7 @@ class ConfigPopup(QuickConfigGui): self.add_widget(btn_container) def update_config(self, save_to_file=False): - updated_cat = defaultdict(list) + updated_cat = defaultdict(list) # type: DefaultDict[List[str]] try: for k, v in self.collect_properties(self).items(): k1, k2 = k.split("/") @@ -157,14 +159,17 @@ class ConfigPopup(QuickConfigGui): if {key for key in engine_updates if key not in {"max_visits", "max_time", "enable_ownership"}}: self.katrain.log(f"Restarting Engine after {engine_updates} settings change") self.katrain.controls.set_status(f"Restarting Engine after {engine_updates} settings change") - old_engine = self.katrain.engine - new_engine = KataGoEngine(self.katrain, self.config["engine"]) - self.katrain.engine = {"B": new_engine, "W": new_engine} - self.katrain.game.engine = new_engine - if getattr(old_engine, "katago_process"): - old_engine.shutdown(finish=True) - else: - self.katrain.game.analyze_all_nodes() # old engine was broken, so make sure we redo any failures + + def restart_engine(_dt): + old_engine = self.katrain.engine # type: KataGoEngine + new_engine = KataGoEngine(self.katrain, self.config["engine"]) + self.katrain.engine = {"B": new_engine, "W": new_engine} + self.katrain.game.engine = new_engine + if getattr(old_engine, "katago_process"): + old_engine.shutdown(finish=True) + else: + self.katrain.game.analyze_all_nodes() # old engine was broken, so make sure we redo any failures + Clock.schedule_once(restart_engine, 0) self.katrain.debug_level = self.config["debug"]["level"] self.katrain.update_state(redraw_board=True) diff --git a/katrain.kv b/katrain.kv index 1306706..6f8d462 100644 --- a/katrain.kv +++ b/katrain.kv @@ -360,11 +360,37 @@ Label: size_hint: 0.025, 1 +: + do_scroll_x: False + scroll_type: ['bars'] + bar_width: 5 + bar_color: BUTTON_COLOR + canvas.before: + Color: + rgba: root.border_color + Line: + rectangle: [self.pos[0],self.pos[1]+1,self.width-1,self.height-2] + width:1 + Color: + rgba: [*[(c+1)/2 for c in root.border_color[:3]], root.border_color[3]] + Line: + rectangle: [self.pos[0]+1,self.pos[1]+2,self.width-3,self.height-4] + width:1 + Label: + padding: 5, 5 + font_size: dp(20) + color: BLACK + size_hint_y: None + height: self.texture_size[1] + text_size: self.width, None + text: root.text + : orientation: 'vertical' play_tab_button: play_tab_button analyze_tab_button: analyze_tab_button info: info + status_label: status_label win_rate: win_rate score_change: score_change score: score @@ -509,7 +535,7 @@ StyledSpinner: id: B_AI_mode values: ['Default'] - sync_height_frac: 0.66 + sync_height_frac: 0.7 size_hint: 0.3, 1 on_text: if B_player_mode.children: B_player_mode.children[0].trigger_action(duration=0) Label: @@ -527,7 +553,7 @@ StyledSpinner: id: W_AI_mode size_hint: 0.3, 1 - sync_height_frac: 0.66 + sync_height_frac: 0.7 values: ['Default'] on_text: if W_player_mode.children: W_player_mode.children[0].trigger_action(duration=0) BoxLayout: @@ -549,9 +575,18 @@ size_hint: 1, 0.0225 label: 'Score change' text: '' - TextInput: - id: info - size_hint: 1, 0.2 + BoxLayout: + size_hint: 1,0.2 + orientation: 'vertical' + ScrollableLabel: + id: status_label + border_color: 0.5,0.1,0.1,1 + opacity: 1 if self.text else 0 + text: '' + size_hint_y: 0.00001 if not self.text else 0.66 + ScrollableLabel: + id: info + size_hint: 1, 1 - status_label.size_hint_y BoxLayout: orientation: 'horizontal' size_hint: 1, None diff --git a/katrain.py b/katrain.py index a38f339..177ca12 100644 --- a/katrain.py +++ b/katrain.py @@ -1,5 +1,3 @@ -import inspect - from kivy.config import Config # isort:skip Config.set("input", "mouse", "mouse,multitouch_on_demand") # isort:skip # no red dots on right click @@ -10,13 +8,13 @@ import sys import threading import traceback from queue import Queue +from typing import Optional from kivy.app import App from kivy.core.clipboard import Clipboard from kivy.core.window import Window from kivy.storage.jsonstore import JsonStore from kivy.uix.popup import Popup -from kivy.uix.widget import Widget from ai import ai_move from common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_EXTRA_DEBUG, OUTPUT_INFO @@ -33,14 +31,14 @@ class KaTrainGui(BoxLayout): def __init__(self, **kwargs): super(KaTrainGui, self).__init__(**kwargs) self.debug_level = 0 - self.engine = None + self.engine = None # type: Optional[KataGoEngine] self.game = None self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level) self._load_config() self.debug_level = self.config("debug/level", OUTPUT_INFO) - self.controls.ai_mode_groups["W"].values = self.controls.ai_mode_groups["B"].values = list(self.config("ai").keys()) + [""] + self.controls.ai_mode_groups["W"].values = self.controls.ai_mode_groups["B"].values = list(self.config("ai").keys()) self.message_queue = Queue() self._keyboard = Window.request_keyboard(None, self, "") diff --git a/selfplay.py b/selfplay.py index 5eacb72..a9a6587 100644 --- a/selfplay.py +++ b/selfplay.py @@ -1,16 +1,17 @@ # This is a script I use to test the performance of AIs +import pickle +import sys import threading -import time, sys +import time import traceback from collections import defaultdict -import pickle from concurrent.futures.thread import ThreadPoolExecutor -from game import Game from ai import ai_move -from engine import KataGoEngine -from common import OUTPUT_ERROR, OUTPUT_INFO, OUTPUT_DEBUG +from common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO from elote import EloCompetitor +from engine import KataGoEngine +from game import Game DB_FILENAME = "ai_performance.pickle" diff --git a/sgf_parser.py b/sgf_parser.py index c978091..4a6d66e 100644 --- a/sgf_parser.py +++ b/sgf_parser.py @@ -71,16 +71,16 @@ class SGFNode: if parent and move: self.set_property(move.player, move.sgf(self.board_size)) - @property - def sgf_properties(self) -> Dict: + def sgf_properties(self, **xargs) -> Dict: """For hooking into in a subclass and overriding/formatting any additional properties to be output""" return copy.deepcopy(self.properties) - def sgf(self) -> str: + def sgf(self, **xargs) -> str: + """Generates an SGF, calling sgf_properties on each node with the given xargs, so it can filter relevant properties if needed.""" import sys sys.setrecursionlimit(max(sys.getrecursionlimit(), 3 * 29 * 29)) # thanks to lightvector for causing stack overflows - sgf_str = "".join([prop + "".join(f"[{v}]" for v in values) for prop, values in self.sgf_properties.items() if values]) + sgf_str = "".join([prop + "".join(f"[{v}]" for v in values) for prop, values in self.sgf_properties(**xargs).items() if values]) if self.children: children = [c.sgf() for c in self.children] if len(children) == 1: diff --git a/start_bot.py b/start_bot.py index b5951e9..c3812f9 100755 --- a/start_bot.py +++ b/start_bot.py @@ -8,7 +8,10 @@ if len(sys.argv) < 2: exit(0) bot = sys.argv[1].strip() +port = int(sys.argv[2]) if len(sys.argv) > 2 else 8587 + username = f"katrain-{bot}" + greetings = { "dev": "Experimental!", "strong": "Play top policy move.", @@ -40,6 +43,8 @@ GREETING = ( ) BYEMSG = "Thank you for playing. If you have any feedback, please message my admin!" MAXGAMES = 10 -os.system( - f'gtp2ogs --apikey {APIKEY} --username {username} --greeting "{GREETING}" --rankedonly --farewell "{BYEMSG}" --ogspv katago --noclock --speeds blitz,live --maxconnectedgames {MAXGAMES} --persist --minrank 20k --noautohandicap --maxhandicap 0 --boardsizes 9,13,19 --komis automatic,6.5 -- python ai2gtp.py {bot}' -) +# --rankedonly + +cmd = f'gtp2ogs --apikey {APIKEY} --username {username} --greeting "{GREETING}" --farewell "{BYEMSG}" --ogspv katago --noclock --nopause --speeds blitz,live --maxconnectedgames {MAXGAMES} --persist --minrank 20k --noautohandicap --maxhandicap 0 --boardsizes 19 --komis automatic,6.5 -- python ai2gtp.py {bot} {port}' +print(f"starting bot {username} using server port {port} --> {cmd}") +os.system(cmd) diff --git a/start_bots b/start_bots deleted file mode 100644 index 79260e8..0000000 --- a/start_bots +++ /dev/null @@ -1,11 +0,0 @@ -screen echo 1 -split -v -focus -screen echo 2 -split -focus -screen echo 3 -focus -split -focus -screen echo 4 \ No newline at end of file