fix bots and ai

This commit is contained in:
Sander Land committed 2020-04-30 21:17:36 +02:00
1 parent 05e55a24a0
commit e13fbee9af
22 files changed
+233 -441

No files matched your search

+1 -1
View File
@@ -2,6 +2,6 @@ Primary author and project maintainer (https://github.com/sanderland/katrain):
Sander Land Sander Land
Thanks to: Thanks to:
'Dontbtme' for feedback and testing of v1.0. 'Dontbtme' for an incredible amount of detailed feedback and early testing of v1.0.
-45
View File
@@ -1,45 +0,0 @@
KataGo v1.3.5
https://github.com/lightvector/KataGo
-----------------------------------------------------
USAGE:
-----------------------------------------------------
FIRST:
Run this to make sure KataGo is working, with a neural net file.
katago.exe benchmark -model <NEURALNET>.bin.gz
(download neural nets here if you don't have one: https://d3dndmfyhecmj0.cloudfront.net/g170/neuralnets/index.html)
On OpenCL, it should also cause KataGo to tune for your GPU. Then, the benchmark will report stats about speed and threads. You can configure gtp_example.cfg to use that many numSearchThreads to get good performance.
ALSO:
You can also run this command to have KataGo generate a gtp config for you, and automatically tune the number of threads and other parameters
and other settings based on your answers to various questions.
katago.exe genconfig -model <NEURALNET>.bin.gz -output gtp_custom.cfg
NEXT:
This command will run the KataGo engine proper. Feed this command to any program GUI program to launch KataGo's engine:
katago.exe gtp -model <NEURALNET>.bin.gz
Or if you generated a config yourself:
katago.exe gtp -model <NEURALNET>.bin.gz -config gtp_custom.cfg
KataGo should be able to work with any GUI program that supports GTP, as well as any analysis program that supports Leela Zero's `lz-analyze` command, such as Lizzie (https://github.com/featurecat/lizzie) or Sabaki (https://sabaki.yichuanshen.de/).
NOTE:
If you encounter errors due to a missing "msvcp140.dll" or "msvcp140_1.dll" or "msvcp140_2.dll" or "vcruntime140.dll" or similar, you need to download and install the Microsoft Visual C++ Redistributable, here:
https://www.microsoft.com/en-us/download/details.aspx?id=48145
If this is for a 64-bit Windows version of KataGo, these dll files have already been included for you, otherwise you will need to install them yourself. On a 64-bit Windows version, there is a rare chance that you may need to delete them if you already have it installed yourself separately and the pre-included files are actually causing problems running KataGo.
-----------------------------------------------------
OPENCL VS CUDA:
-----------------------------------------------------
Depending on hardware and settings, in practice the OpenCL version seems to range from anywhere to several times slower to a little faster than the CUDA version. More optimization work may happen in the future though - the OpenCL version has definitely not reached the limit of how well it can be optimized. It also has not been tested for self-play training with extremely large batch sizes to run hundreds of games in parallel, all of KataGo's main training runs so far have been performed with the CUDA implementation.
Extensive testing across different OSs and versions and compilers has not been done, so if you encounter issues, feel free to open an issue.
-----------------------------------------------------
TUNING FOR PERFORMANCE:
You will very likely want to tune some of the parameters in `default_gtp.cfg` for your system for good performance, including the number of threads, fp16 usage (CUDA only), NN cache size, pondering settings, and so on. You can also adjust things like KataGo's resign threshold or utility function. Most of the relevant parameters should be be reasonably well documented directly inline in that config.
There are other a few notes about usage and performance at : https://github.com/lightvector/KataGo
File renamed without changes.
File renamed without changes.
+3 -3
View File
@@ -57,7 +57,7 @@ Under the 'play' tab you can select who is playing black and white.
* Teach will give you instant feedback, and auto-undo bad moves to give you a second chance. * Teach will give you instant feedback, and auto-undo bad moves to give you a second chance.
* Settings for this mode can be found under 'Configure Teacher' * Settings for this mode can be found under 'Configure Teacher'
* AI will activate the AI in the dropdown next to the buttons. * AI will activate the AI in the dropdown next to the buttons.
* Settings for the selected AI(s) can be found under 'Configure AIs' * Settings for all AIs can be found under 'Configure AIs'
If you do not want to see 'Points lost' or other feedback for your moves, If you do not want to see 'Points lost' or other feedback for your moves,
set 'show last n dots' to 0 under 'Configure Teacher', and click on the words 'Points lost' to hide its value. set 'show last n dots' to 0 under 'Configure Teacher', and click on the words 'Points lost' to hide its value.
@@ -80,7 +80,7 @@ Available AIs, with strength indicating an estimate for the default settings, ar
* **[9p+]** **Default** is full KataGo, above professional level. * **[9p+]** **Default** is full KataGo, above professional level.
* **Balance** is KataGo occasionally making weaker moves, attempting to win by ~2 points. * **Balance** is KataGo occasionally making weaker moves, attempting to win by ~2 points.
* **Jigo** is KataGo aggressively making weaker moves, attempting to win by 0.5 points. * **Jigo** is KataGo aggressively making weaker moves, attempting to win by 0.5 points.
* **[~4d]** **Policy** uses the top move from the policy network (it's 'shape sense' without reading), should be around high dan level depending on the model used. * **[~4d]** **Policy** uses the top move from the policy network (it's 'shape sense' without reading), should be around high dan level depending on the model used. There is a setting to increase variety in the opening, but otherwise it plays deterministically.
* **[~3k]**: **P:Weighted** picks a random move weighted by the policy, as long as it's above `lower_bound`. `weaken_fac` uses `policy^(1/weaken_fac)`, increasing the chance for weaker moves. * **[~3k]**: **P:Weighted** picks a random move weighted by the policy, as long as it's above `lower_bound`. `weaken_fac` uses `policy^(1/weaken_fac)`, increasing the chance for weaker moves.
* **[~5k]**: **P:Pick** picks `pick_n + pick_frac * <number of legal moves>` moves at random, and play the best move among them. * **[~5k]**: **P:Pick** picks `pick_n + pick_frac * <number of legal moves>` moves at random, and play the best move among them.
The setting `pick_override` determines the minimum value at which this process is bypassed to play the best move instead, preventing obvious blunders. The setting `pick_override` determines the minimum value at which this process is bypassed to play the best move instead, preventing obvious blunders.
@@ -123,7 +123,7 @@ In addition to shortcuts mentioned above, there are:
* **[scroll down]**: Redo move. Only works when hovering the cursor over the board. * **[scroll down]**: Redo move. Only works when hovering the cursor over the board.
* **[click on a move]**: See detailed statistics for a previous move. * **[click on a move]**: See detailed statistics for a previous move.
* **[double-click on a move]**: Navigate directly to that point in the game. * **[double-click on a move]**: Navigate directly to that point in the game.
* **[Ctrl-v]**: Load SGF from clipboard and do a 'fast' analysis. * **[Ctrl-v]**: Load SGF from clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move).
* **[Ctrl-c]**: Save SGF to clipboard. * **[Ctrl-c]**: Save SGF to clipboard.
* **[Ctrl-l]**: Load SGF from file and do a normal analysis. * **[Ctrl-l]**: Load SGF from file and do a normal analysis.
* **[Ctrl-s]**: Save SGF with automated review to file. * **[Ctrl-s]**: Save SGF with automated review to file.
+5 -2
View File
@@ -44,12 +44,15 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
policy_grid = var_to_grid(cn.policy, size) # type: List[List[float]] policy_grid = var_to_grid(cn.policy, size) # type: List[List[float]]
top_policy_move = policy_moves[0][1] top_policy_move = policy_moves[0][1]
ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. " ai_thoughts += f"Using policy based strategy, base top 5 moves are {fmt_moves(policy_moves[:5])}. "
if "policy" in ai_mode and cn.depth <= int(ai_settings["opening_moves"] * (game.board_size[0] * game.board_size[1])):
ai_mode = "p:weighted"
ai_thoughts += f"Switching to weighted strategy in the opening {int(ai_settings['opening_moves'] * (game.board_size[0]*game.board_size[1]))} moves."
if top_5_pass: if top_5_pass:
aimove = top_policy_move aimove = top_policy_move
ai_thoughts += "Playing top one because one of them is pass." ai_thoughts += "Playing top one because one of them is pass."
elif "policy" in ai_mode: elif "policy" in ai_mode:
aimove = top_policy_move aimove = top_policy_move
ai_thoughts += f"Playing top policy move {aimove.gtp()} due to mode chosen." ai_thoughts += f"Playing top policy move {aimove.gtp()}."
elif policy_moves[0][0] > ai_settings["pick_override"]: elif policy_moves[0][0] > ai_settings["pick_override"]:
aimove = top_policy_move aimove = top_policy_move
ai_thoughts += f"Top policy move has weight > {ai_settings['pick_override']:.1%}, so overriding other strategies." ai_thoughts += f"Top policy move has weight > {ai_settings['pick_override']:.1%}, so overriding other strategies."
@@ -88,7 +91,7 @@ def ai_move(game: Game, ai_mode: str, ai_settings: Dict) -> Tuple[Move, GameNode
else: else:
weight = lambda x, y: (1 / ai_settings["line_weight"]) ** (max(0, min(size[0] - 1 - x, x, size[1] - 1 - y, y) - thr_line)) 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] 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. " 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: elif "local" in ai_mode or "tenuki" in ai_mode:
var = ai_settings["stddev"] ** 2 var = ai_settings["stddev"] ** 2
if not cn.single_move or cn.single_move.coords is None: if not cn.single_move or cn.single_move.coords is None:
+2 -175
View File
@@ -1,175 +1,2 @@
# KaTrain v1.0 # Bots
This directory contains the source code used to run the katrain-* bots on OGS.
This repository contains a tool for analyzing and playing go with AI feedback from KataGo.
The original idea was to give immediate feedback on the many large mistakes we make in terms of inefficient moves,
but has since grown to include a wide range of features, including:
* Review your games to find the moves that were most costly in terms of points lost.
* Play against AI and get immediate feedback on mistakes with option to retry.
* Play against a wide range of weakened versions of AI with various styles.
* Play against a stronger player and use the retry option instead of handicap stones.
## Screenshots
| Analyze games | Play against an AI Teacher |
| ------------- | ------------- |
| ![screenshot](img/screenshot_analyze.png) | ![screenshot](img/screenshot_play.png) |
## Quickstart
* Right-click any button you don't understand for help.
* To analyze a game, load it using the button in the bottom right, or press `ctrl-L`
* To play against AI, pick an AI from the drop down a color and either 'human' or 'teach' for yourself and start playing.
* For different board sizes, use the button with the little goban in the bottom right for a new game.
## Installation
### Quick Installation for Windows users
* See the releases tab for pre-built installers
### Installation from source for Windows users
* Download the repository by clicking the green *Clone or download* on this page and *Download zip*. Extract the contents.
* Make sure you have a python installation, I will assume Anaconda (Python 3.7), available [here](https://www.anaconda.com/distribution/#download-section).
* Open 'Anaconda prompt' from the start menu and navigate to where you extracted the zip file using the `cd <folder>` command.
* Execute the command `pip install numpy 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
* 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 numpy`
* Put your KataGo binary in the `KataGo/` directory or change the `engine.command` field in `config.json` to your KataGo v1.3.5+ binary.
* Compiled binaries and source code can be found [here](https://github.com/lightvector/KataGo/releases).
* You will need to `chmod +x katago` your binary if you downloaded it.
* Executables for Mac are not available, so compiling from source code is required there.
* 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.
## 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.
* Settings for this mode can be found under 'Configure Teacher'
* AI will activate the AI in the dropdown next to the buttons.
* Settings for all AIs can be found under 'Configure AIs'
If you do not want to see 'Points lost' or other feedback for your moves,
set 'show last n dots' to 0 under 'Configure Teacher', and click on the words 'Points lost' to hide its value.
#### What are all these coloured dots?
The dots indicate how many points were lost by that move.
* The colour indicates the size of the mistake according to kata
* The size indicates if the mistake was actually punished. Going from fully punished at maximal size,
to no actual effect on the score at minimal size.
In short, if you are a weaker player you should mostly on large dots that are red or purple,
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.
* **Balance** is KataGo occasionally making weaker moves, attempting to win by ~2 points.
* **Jigo** is KataGo aggressively making weaker moves, attempting to win by 0.5 points.
* **[~4d]** **Policy** uses the top move from the policy network (it's 'shape sense' without reading), should be around high dan level depending on the model used.
* **[~3k]**: **P:Weighted** picks a random move weighted by the policy, as long as it's above `lower_bound`. `weaken_fac` uses `policy^(1/weaken_fac)`, increasing the chance for weaker moves.
* **[~5k]**: **P:Pick** picks `pick_n + pick_frac * <number of legal moves>` moves at random, and play the best move among them.
The setting `pick_override` determines the minimum value at which this process is bypassed to play the best move instead, preventing obvious blunders.
This, along with 'Weighted' are probably the best choice for kyu players who want a chance of winning without playing the sillier bots below. Variants of this strategy include:
* **[~3k]**: **P:Local** will pick such moves biased towards the last move with probability related to `local_stddev`.
* **[~7k]**: **~P:Tenuki** is biased in the opposite way as P:Local, using the same setting.
* **[~7k]**: **P:Influence** is biased towards 4th+ line moves, with every line below that dividing both the chance of considering the move and the policy value by `influence_weight`. Consider setting `pick_frac=1.0` to only affect the policy weight.
* **[~7k]**: **P:Territory** is biased in the opposite way, towards 1-3rd line moves, using the same setting.
* * **[~7k]**: **P:Noise** mixes the policy with `noise_strength` Dirichlet noise. At `noise_strength=0.9` play is near-random, while `noise_strength=0.7` is still quite strong. A threshold setting is included to avoid senseless first-line moves.
Selecting the AI as either white or black opens up the option to configure it under 'Configure AI'.
### Analysis
Keyboard shortcuts are shown with **[key]**.
* The checkboxes configure:
* **[q]**: Child moves are shown. On by default, can turn it off to avoid obscuring other information or when wanting to guess the next move.
* **[w]**: All dots: Show all evaluation dots instead of the last few.
* You can configure how many are shown with this setting off, and whether they are shown for AIs under 'Play/Configure Teacher'.
* **[e]**: Top moves: Show the next moves KataGo considered, colored by their expected point loss. Small dots indicate high uncertainty. Hover over any of them to see the principal variation.
* **[r]**: Show owner: Show expected ownership of each intersection.
* **[t]**: NN Policy: Show KataGo's policy network evaluation, i.e. where it thinks the best next move is purely from the position, and in the absence of any 'reading'.
* The analysis buttons are used for:
* **[a]**: Extra: Re-evaluate the position using more visits, usually resulting in a more accurate evaluation.
* **[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:
* **[Tab]**: to switch between analysis and play modes. (NB. keyboard shortcuts function regardless)
* **[~]** or **[`]** or **[p]**: Hide side panel UI and only show the board.
* **[enter]**: AI Move
* **[arrow up]** or **[z]**: Undo move. Hold shift for 10 moves at a time, or ctrl to skip to the start.
* **[arrow down]** or **[x]**: Redo move. Hold shift for 10 moves at a time, or ctrl to skip to the start.
* **[scroll up]**: Undo move. Only works when hovering the cursor over the board.
* **[scroll down]**: Redo move. Only works when hovering the cursor over the board.
* **[click on a move]**: See detailed statistics for a previous move.
* **[double-click on a move]**: Navigate directly to that point in the game.
* **[Ctrl-v]**: Load SGF from clipboard and do a 'fast' analysis of the game (with a high priority normal analysis for the last move).
* **[Ctrl-c]**: Save SGF to clipboard.
* **[Ctrl-l]**: Load SGF from file and do a normal analysis.
* **[Ctrl-s]**: Save SGF with automated review to file.
* **[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.
You can use `python katrain.py your_config_file.json` to use another config file instead.
If you ever need to reset to the original settings, simply re-download the `config.json` file in this repository.
### Settings Panel
* engine settings
* max_visits: the number of visits used in analyses and AI moves, higher is more accurate but slower.
* max_time: maximal time in seconds for analyses, even when the target number of visits has not been reached.
* fast_visits: the number of visits used for certain operations with fewer visits.
* katago: path to your KataGo executable.
* model: path to your KataGo model file.
* config: path to your KataGo config file.
* threads: number of threads to use in the KataGo analysis engine.
* game settings
* init_size: the initial size of the board, on start-up.
* init_komi: likewise, for komi.
* sgf settings
* sgf_load: default path where the load SGF dialog opens.
* sgf_save: path where SGF files are saved.
* board_ui settings
* eval_dot_max_size: size of coloured dots when point size is maximal, relative to stone size.
* eval_dot_min_size: size of coloured dots when point size is minimal
* ... various other minor cosmetic options.
* debug settings
* level: determines the level of output in the console, where 0 shows no debug output, 1 shows some and 2 shows a lot. This is mainly used for reporting bugs.
## FAQ
* The program is slow to start!
* The first startup of KataGo can be slow, after that it should be much faster.
* The program is running too slowly!
* Adjust the number of visits or maximum time allowed in the settings.
## Contributing
* Feedback and pull requests are both very welcome.
* For suggestions and planned improvements, see the 'issues' tab on github.
View File
Whitespace-only changes.
+29 -25
View File
@@ -2,21 +2,21 @@
import json import json
import sys import sys
import time import time
import traceback
from ai import ai_move from ai import ai_move
from common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO, bot_strategy_names from common import OUTPUT_DEBUG, OUTPUT_ERROR, OUTPUT_INFO
from bots.settings import bot_strategy_names
from engine import EngineDiedException, KataGoEngine from engine import EngineDiedException, KataGoEngine
from game import Game, Move from game import Game, Move
from sgf_parser import Move from sgf_parser import Move
DB_FILENAME = "ai_performance.pickle"
if len(sys.argv) < 2: if len(sys.argv) < 2:
bot = "dev" bot = "dev"
else: else:
bot = sys.argv[1].strip() bot = sys.argv[1].strip()
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8587 port = int(sys.argv[2]) if len(sys.argv) > 2 else 8587
REPORT_SCORE_THRESHOLD = 1.5
MAX_WAIT_ANALYSIS = 10
class Logger: class Logger:
@@ -29,8 +29,7 @@ logger = Logger()
ENGINE_SETTINGS = { ENGINE_SETTINGS = {
# "katago": "../KataGo/cpp/katago", "katago": f"python bots/engine_connector.py {port}", # 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", "model": "models/b15-1.3.2.txt.gz",
"config": "KataGo/analysis_config.cfg", "config": "KataGo/analysis_config.cfg",
"max_visits": 5, "max_visits": 5,
@@ -59,6 +58,28 @@ logger.log(f"STARTED ENGINE", OUTPUT_ERROR)
game = Game(Logger(), engine, {}) game = Game(Logger(), engine, {})
def malkovich_analysis(cn):
start = time.time()
while not cn.analysis_ready:
time.sleep(0.001)
if engine.katago_process.poll() is not None: # TODO: clean up
raise EngineDiedException(f"Engine for {cn.next_player} ({engine.config}) died")
if time.time() - start > MAX_WAIT_ANALYSIS:
logger.log(f"Waiting for analysis timed out!", OUTPUT_ERROR)
return
if cn.analysis_ready and cn.parent and cn.parent.analysis_ready:
dscore = cn.analysis["root"]["scoreLead"] - cn.parent.analysis["root"]["scoreLead"]
logger.log(f"dscore {dscore} = {cn.analysis['root']['scoreLead']} {cn.parent.analysis['root']['scoreLead']} at {move}...", OUTPUT_ERROR)
if abs(dscore) > REPORT_SCORE_THRESHOLD:
favpl = "B" if dscore > 0 else "W"
msg = f"MALKOVICH:{cn.player} {cn.single_move.gtp()} caused a significant score change {favpl}{abs(dscore):+.1f} -> Winrate {cn.format_win_rate()} ScoreLead {cn.format_score()}"
if cn.ai_thoughts:
msg += f" AI Thoughts: {cn.ai_thoughts}"
print(msg, file=sys.stderr)
sys.stderr.flush()
while not game.ended: while not game.ended:
p = game.current_node.next_player p = game.current_node.next_player
line = input() line = input()
@@ -74,29 +95,12 @@ while not game.ended:
elif "genmove" in line: elif "genmove" in line:
logger.log(f"{ai_strategy} generating move", OUTPUT_ERROR) logger.log(f"{ai_strategy} generating move", OUTPUT_ERROR)
game.current_node.analyze(engine) game.current_node.analyze(engine)
malkovich_analysis(game.current_node)
game.root.properties[f"P{game.current_node.next_player}"] = [f"KaTrain {ai_strategy}"] game.root.properties[f"P{game.current_node.next_player}"] = [f"KaTrain {ai_strategy}"]
move, node = ai_move(game, ai_strategy, ai_settings) move, node = ai_move(game, ai_strategy, ai_settings)
print(f"= {move.gtp()}\n") print(f"= {move.gtp()}\n")
sys.stdout.flush() sys.stdout.flush()
cn = game.current_node malkovich_analysis(game.current_node)
logger.log(f"Waiting for analysis...", OUTPUT_ERROR)
start = time.time()
while not cn.analysis_ready:
time.sleep(0.001)
if engine.katago_process.poll() is not None: # TODO: clean up
raise EngineDiedException(f"Engine for {cn.next_player} ({engine.config}) died")
if time.time() - start > 10:
logger.log(f"Waiting for analysis timed out!", OUTPUT_ERROR)
break
if cn.analysis_ready:
pv = ""
moves = sorted(list(cn.analysis["moves"].values()), key=lambda d: d["order"])
if moves:
pv = " ".join(moves[0]["pv"])
print(
f"CHAT:Visits {cn.ai_thoughts} Winrate {cn.analysis['root']['winrate']:.2%} ScoreLead {cn.analysis['root']['scoreLead']:.1f} ScoreStdev 0.0 PV {move.gtp()} {pv}",
file=sys.stderr,
)
continue continue
elif "play" in line: elif "play" in line:
_, player, move = line.split(" ") _, player, move = line.split(" ")
@@ -1,5 +1,4 @@
# used to scale bots # used to connect many bots to one kata engine
import json
import socket import socket
import sys import sys
import time import time
+2 -2
View File
@@ -1,4 +1,4 @@
# used to scale bots # used to connect many bots to one kata engine
import json import json
import random import random
import socket import socket
@@ -13,7 +13,7 @@ PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8587
ENGINE_SETTINGS = { ENGINE_SETTINGS = {
"katago": "../KataGo/cpp/katago", "katago": "../KataGo/cpp/katago",
"model": " models/b15-1.3.2.txt.gz", "model": "KataGo/models/b15-1.3.2.txt.gz",
"config": "KataGo/analysis_config.cfg", "config": "KataGo/analysis_config.cfg",
"max_visits": 50, "max_visits": 50,
"max_time": 1.0, "max_time": 1.0,
+22
View File
@@ -0,0 +1,22 @@
bot_strategy_names = {
"dev": "P:Noise",
"strong": "Policy",
"influence": "P:Influence",
"territory": "P:Territory",
"balanced": "P:Pick",
"weighted": "P:Weighted",
"local": "P:Local",
"tenuki": "P:Tenuki",
}
greetings = {
"dev": "Experimental!",
"strong": "Play top policy move.",
"influence": "Play an influential style.",
"territory": "Play a territorial style.",
"balanced": "Play the best move out of a random selection.",
"weighted": "Play a policy-weighted move.",
"local": "Prefer local responses.",
"tenuki": "Prefer to tenuki.",
}
+40
View File
@@ -0,0 +1,40 @@
import json
import os
import sys
from bots.settings import bot_strategy_names, greetings
if len(sys.argv) < 2:
exit(0)
bot = sys.argv[1].strip()
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8587
MAXGAMES = 10
BOT_SETTINGS = f" --maxconnectedgames {MAXGAMES} --noautohandicap --maxhandicap 0 --boardsizes 19"
username = f"katrain-{bot}"
with open("config.json") as f:
settings = json.load(f)
all_ai_settings = settings["ai"]
ai_settings = all_ai_settings[bot_strategy_names[bot]]
with open("my/apikey.json") as f:
apikeys = json.load(f)
if bot not in greetings or username not in apikeys:
print("BOT NOT FOUND")
exit(1)
APIKEY = apikeys[username]
settings_dump = ", ".join(f"{k}={v}" for k, v in ai_settings.items() if not k.startswith("_"))
print(settings_dump)
GREETING = f"Hello, welcome to an experimental version of KaTrain AIs - These are based on weakened policy nets of KataGo. Current mode is: {greetings[bot]}"
if settings:
GREETING += "Settings: {settings_dump}."
BYEMSG = "Thank you for playing. If you have any feedback, please message my admin!"
cmd = f'gtp2ogs --debug --apikey {APIKEY} --username {username} --greeting "{GREETING}" --farewell "{BYEMSG}" {BOT_SETTINGS} --aichat --noclock --nopause --speeds blitz,live --persist --minrank 25k --komis automatic,6.5,7.5 -- python bots/ai2gtp.py {bot} {port}'
print(f"starting bot {username} using server port {port} --> {cmd}")
os.system(cmd)
-12
View File
@@ -6,18 +6,6 @@ OUTPUT_DEBUG = 1
OUTPUT_EXTRA_DEBUG = 2 OUTPUT_EXTRA_DEBUG = 2
bot_strategy_names = {
"dev": "P:Noise",
"strong": "Policy",
"influence": "P:Influence",
"territory": "P:Territory",
"balanced": "P:Pick",
"weighted": "P:Weighted",
"local": "P:Local",
"tenuki": "P:Tenuki",
}
def var_to_grid(array_var: List[Any], size: Tuple[int, int]) -> List[List[Any]]: def var_to_grid(array_var: List[Any], size: Tuple[int, int]) -> List[List[Any]]:
"""convert ownership/policy to grid format such that grid[y][x] is for move with coords x,y""" """convert ownership/policy to grid format such that grid[y][x] is for move with coords x,y"""
ix = 0 ix = 0
+24 -23
View File
@@ -1,7 +1,7 @@
{ {
"engine": { "engine": {
"katago": "KataGo/katago-bs", "katago": "KataGo/katago-bs",
"model": "models/b15-1.3.2.txt.gz", "model": "KataGo/models/b15-1.3.2.txt.gz",
"config": "KataGo/analysis_config.cfg", "config": "KataGo/analysis_config.cfg",
"threads": 8, "threads": 8,
"max_visits": 500, "max_visits": 500,
@@ -55,21 +55,22 @@
"random_loss": 1, "random_loss": 1,
"max_loss": 5, "max_loss": 5,
"min_visits": 20, "min_visits": 20,
"_help_right": "Will try to win by `target_score`, lose at most `random_loss` when behind and `max_loss` when ahead.", "_help_left": "Will try to win by `target_score`, lose at most `random_loss` when behind and `max_loss` when ahead.",
"_help_left": "Never plays moves with less than `min_visits` visits, so also check engine settings." "_help_right": "Never plays moves with less than `min_visits` visits, so also check engine settings."
}, },
"Jigo": { "Jigo": {
"target_score": 0.5, "target_score": 0.5,
"_help_right": "Will try to win by `target_score`, without further restrictions.", "_help_left": "Will try to win by `target_score`, without further restrictions.",
"_help_left": "Also affected by engine settings such as `max_visits`." "_help_right": "Also affected by engine settings such as `max_visits`."
}, },
"Policy": { "Policy": {
"_help_right": "No settings available for this mode, strength is mainly affected by `model` in engine settings, but should be high dan regardless.", "opening_moves": 0.05,
"_help_left": "" "_help_left": "Strength is mainly affected by `model` in engine settings, but should be high dan regardless.",
"_help_right": "Plays the P:Weighted strategy during the first `opening_moves` * <number of intersections> moves to allow variety."
}, },
"P:Weighted": { "P:Weighted": {
"_help_right": "`lower_bound` determines the lower bound policy value that is allowed. `weaken_fac` influences how much more likely weaker moves are picked.", "_help_right": "pick_override` determines when top move is chosen without randomness, and `lower_bound` determines the lower bound policy value that is allowed.",
"_help_left": "pick_override` determines when top move is chosen without randomness, and is effectively disabled by default (1.0).", "_help_left": "Plays move with probability=policy^(1/weaken_fac),i.e. `weaken_fac` influences how much more likely weaker moves are picked.",
"pick_override": 1.0, "pick_override": 1.0,
"lower_bound": 0.001, "lower_bound": 0.001,
"weaken_fac": 1.25 "weaken_fac": 1.25
@@ -78,31 +79,31 @@
"pick_override": 0.95, "pick_override": 0.95,
"noise_strength": 0.6, "noise_strength": 0.6,
"lower_bound": 0.001, "lower_bound": 0.001,
"_help_right": "Adds `noise_strength` noise to the policy of all moved > 'lower_bound' and plays the top move.", "_help_left": "Adds `noise_strength` noise to the policy of all moved > 'lower_bound' and plays the top move.",
"_help_left": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes. Noise above 0.9 is near random, below 0.7 is fairly strong." "_help_right": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes. Noise above 0.9 is near random, below 0.7 is fairly strong."
}, },
"P:Pick": { "P:Pick": {
"pick_override": 0.95, "pick_override": 0.95,
"pick_n": 5, "pick_n": 5,
"pick_frac": 0.33, "pick_frac": 0.33,
"_help_right": "Picks `pick_n + pick_frac * <number of legal moves>` at random and plays the best one. Change `pick_frac` to make it see more moves.", "_help_left": "Picks `pick_n + pick_frac * <number of legal moves>` at random and plays the best one. Change `pick_frac` to make it see more moves.",
"_help_left": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes." "_help_right": "Plays top move if policy value is above `pick_override` to avoid obvious mistakes."
}, },
"P:Local": { "P:Local": {
"pick_override": 0.95, "pick_override": 0.95,
"stddev": 1.5, "stddev": 1.5,
"pick_n": 15, "pick_n": 15,
"pick_frac": 0.0, "pick_frac": 0.0,
"_help_right": "Samples `pick_n + pick_frac * <number of legal moves>` near the last move and plays the best one.", "_help_left": "Samples `pick_n + pick_frac * <number of legal moves>` near the last move and plays the best one.",
"_help_left": "Lower `stddev` makes it prefer closer moves." "_help_right": "Lower `stddev` makes it prefer closer moves."
}, },
"P:Tenuki": { "P:Tenuki": {
"pick_override": 0.9, "pick_override": 0.9,
"stddev": 7.5, "stddev": 7.5,
"pick_n": 5, "pick_n": 5,
"pick_frac": 0.7, "pick_frac": 0.7,
"_help_right": "Samples `pick_n + pick_frac * <number of legal moves>` away from the last move and plays the best one.", "_help_left": "Samples `pick_n + pick_frac * <number of legal moves>` away from the last move and plays the best one.",
"_help_left": "Increase `stddev` makes it prefer moves further away." "_help_right": "Increase `stddev` makes it prefer moves further away."
}, },
"P:Influence": { "P:Influence": {
"pick_override": 0.95, "pick_override": 0.95,
@@ -110,17 +111,17 @@
"pick_frac": 0.4, "pick_frac": 0.4,
"threshold": 3.5, "threshold": 3.5,
"line_weight": 10, "line_weight": 10,
"_help_right": "Samples `pick_n + pick_frac * <number of legal moves>` and plays the best one, biased to above the `threshold` line.", "_help_left": "Samples `pick_n + pick_frac * <number of legal moves>` and plays the best one, biased to above the `threshold` line.",
"_help_left": "Increase `line_weight` to penalize moves near the edge more." "_help_right": "Increase `line_weight` to penalize moves near the edge more."
}, },
"P:Territory": { "P:Territory": {
"pick_override": 0.95, "pick_override": 0.95,
"pick_n": 5, "pick_n": 5,
"pick_frac": 0.4, "pick_frac": 0.4,
"threshold": 3.5, "threshold": 3.5,
"line_weight": 5, "line_weight": 2,
"_help_right": "Samples `pick_n + pick_frac * <number of legal moves>` and plays the best one, biased to below the `threshold` line.", "_help_left": "Samples `pick_n + pick_frac * <number of legal moves>` and plays the best one, biased to below the `threshold` line.",
"_help_left": "Increase `line_weight` to penalize moves closer to the center more." "_help_right": "Increase `line_weight` to penalize moves closer to the center more."
} }
}, },
"board_ui": { "board_ui": {
@@ -238,6 +239,6 @@
] ]
}, },
"debug": { "debug": {
"level": 2 "level": 1
} }
} }
+9 -11
View File
@@ -104,7 +104,7 @@ class BadukPanWidget(Widget):
draw_circle((self.gridpos_x[x], self.gridpos_y[y]), stone_size, col) draw_circle((self.gridpos_x[x], self.gridpos_y[y]), stone_size, col)
if outline_col: if outline_col:
Color(*outline_col) Color(*outline_col)
Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size), width=0.05 * stone_size) Line(circle=(self.gridpos_x[x], self.gridpos_y[y], stone_size), width=min(2, 0.035 * stone_size))
if evalcol: if evalcol:
eval_radius = math.sqrt(evalscale) # scale area by evalscale eval_radius = math.sqrt(evalscale) # scale area by evalscale
evalsize = self.stone_size * (self.ui_config["eval_dot_min_size"] + eval_radius * (self.ui_config["eval_dot_max_size"] - self.ui_config["eval_dot_min_size"])) evalsize = self.stone_size * (self.ui_config["eval_dot_min_size"] + eval_radius * (self.ui_config["eval_dot_max_size"] - self.ui_config["eval_dot_min_size"]))
@@ -138,7 +138,7 @@ class BadukPanWidget(Widget):
extra_px_margin_x = (self.width - board_width_with_margins) / 2 extra_px_margin_x = (self.width - board_width_with_margins) / 2
extra_px_margin_y = (self.height - board_height_with_margins) / 2 extra_px_margin_y = (self.height - board_height_with_margins) / 2
self.stone_size = self.grid_size * self.ui_config["stone_size"] self.stone_size = self.grid_size * self.ui_config["stone_size"]
self.gridpos_x = [self.pos[0] + extra_px_margin_x + math.floor((grid_spaces_margin_x[0] + i) * self.grid_size + 0.5) for i in range(board_size_x)] # self.gridpos_x = [self.pos[0] + extra_px_margin_x + math.floor((grid_spaces_margin_x[0] + i) * self.grid_size + 0.5) for i in range(board_size_x)]
self.gridpos_y = [self.pos[1] + extra_px_margin_y + math.floor((grid_spaces_margin_y[0] + i) * self.grid_size + 0.5) for i in range(board_size_y)] self.gridpos_y = [self.pos[1] + extra_px_margin_y + math.floor((grid_spaces_margin_y[0] + i) * self.grid_size + 0.5) for i in range(board_size_y)]
line_color = self.ui_config["line_color"] line_color = self.ui_config["line_color"]
@@ -212,15 +212,13 @@ class BadukPanWidget(Widget):
realized_points_lost = node.parent_realized_points_lost realized_points_lost = node.parent_realized_points_lost
if katrain.game.current_node.is_root and katrain.config("debug/level") >= 3: # secret ;) if katrain.game.current_node.is_root and katrain.config("debug/level") >= 3: # secret ;)
for s in range(0, 19): for y in range(0, board_size_y):
c = s evalcol = self.eval_color(16 * y / board_size_y)
evalcol = self.eval_color(s) self.draw_stone(0, y, stone_color["B"], outline_color["B"], None, evalcol, y / (board_size_y - 1))
evalsize = 1 self.draw_stone(1, y, stone_color["B"], outline_color["B"], stone_color["W"], evalcol, 1)
self.draw_stone(5, c, stone_color["B"], outline_color["B"], None, evalcol, evalsize) self.draw_stone(2, y, stone_color["W"], outline_color["W"], None, evalcol, y / (board_size_y - 1))
self.draw_stone(6, c, stone_color["B"], outline_color["B"], stone_color["W"], evalcol, evalsize) self.draw_stone(3, y, stone_color["W"], outline_color["W"], stone_color["B"], evalcol, 1)
self.draw_stone(7, c, stone_color["W"], outline_color["W"], None, evalcol, evalsize) self.draw_stone(4, y, [*evalcol[:3], 0.5], scale=0.8)
self.draw_stone(8, c, stone_color["W"], outline_color["W"], stone_color["B"], evalcol, evalsize)
self.draw_stone(9, c, [*evalcol[:3], 0.5], scale=0.8)
# ownership - allow one move out of date for smooth animation # ownership - allow one move out of date for smooth animation
ownership = current_node.ownership or (current_node.parent and current_node.parent.ownership) ownership = current_node.ownership or (current_node.parent and current_node.parent.ownership)
+10 -10
View File
@@ -9,6 +9,8 @@ class Controls(BoxLayout):
super(Controls, self).__init__(**kwargs) super(Controls, self).__init__(**kwargs)
self.status = None self.status = None
self.status_node = None self.status_node = None
self.ai_settings_popup = None
self.teacher_settings_popup = None
def set_status(self, msg, at_node=None): def set_status(self, msg, at_node=None):
self.status = msg self.status = msg
@@ -65,9 +67,7 @@ class Controls(BoxLayout):
points_lost = current_node.points_lost points_lost = current_node.points_lost
self.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained" self.score_change.label = f"Points lost" if points_lost and points_lost > 0 else f"Points gained"
self.score_change.text = f"{move.player}: {abs(points_lost):.1f}" if points_lost else "..." self.score_change.text = f"{move.player}: {abs(points_lost):.1f}" if points_lost else "..."
print(current_player_is_ai_playing_human, move, next_player_is_human_or_both_robots)
elif not current_player_is_ai_playing_human: elif not current_player_is_ai_playing_human:
print(current_player_is_ai_playing_human, move, next_player_is_human_or_both_robots)
self.score_change.label = f"Points lost" self.score_change.label = f"Points lost"
self.score_change.text = "" 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.single_move:
@@ -83,13 +83,13 @@ class Controls(BoxLayout):
self.info.text = info self.info.text = info
def configure_ais(self): def configure_ais(self):
config_popup = Popup(title="Edit AI Settings", size_hint=(0.9, 0.9)) if not self.ai_settings_popup: # persist state of popup etc
popup_contents = ConfigAIPopup(self.katrain, config_popup, {self.ai_mode("B"), self.ai_mode("W")}) self.ai_settings_popup = Popup(title="Edit AI Settings", size_hint=(0.7, 0.8)).__self__
config_popup.add_widget(popup_contents) self.ai_settings_popup.add_widget(ConfigAIPopup(self.katrain, self.ai_settings_popup, self.katrain.config("ai")))
config_popup.open() self.ai_settings_popup.open()
def configure_teacher(self): def configure_teacher(self):
config_popup = Popup(title="Edit Teacher Settings", size_hint=(0.7, 0.8)) if not self.teacher_settings_popup:
popup_contents = ConfigTeacherPopup(self.katrain, config_popup) self.teacher_settings_popup = Popup(title="Edit Teacher Settings", size_hint=(0.7, 0.8)).__self__
config_popup.add_widget(popup_contents) self.teacher_settings_popup.add_widget(ConfigTeacherPopup(self.katrain, self.teacher_settings_popup))
config_popup.open() self.teacher_settings_popup.open()
+45 -44
View File
@@ -21,6 +21,7 @@ from gui.kivyutils import (
LightHelpLabel, LightHelpLabel,
ScaledLightLabel, ScaledLightLabel,
StyledButton, StyledButton,
StyledSpinner,
) )
@@ -33,6 +34,7 @@ class QuickConfigGui(BoxLayout):
super().__init__(**kwargs) super().__init__(**kwargs)
self.katrain = katrain self.katrain = katrain
self.popup = popup self.popup = popup
self.orientation = "vertical"
if initial_values: if initial_values:
self.set_properties(self, initial_values) self.set_properties(self, initial_values)
@@ -100,11 +102,13 @@ class ConfigPopup(QuickConfigGui):
self.ignore_cats = ignore_cats self.ignore_cats = ignore_cats
self.orientation = "vertical" self.orientation = "vertical"
super().__init__(katrain, popup, **kwargs) super().__init__(katrain, popup, **kwargs)
Clock.schedule_once(self._build, 0) Clock.schedule_once(self.build, 0)
def build(self, _):
def _build(self, _):
cols = [BoxLayout(orientation="vertical"), BoxLayout(orientation="vertical")]
props_in_col = [0, 0] props_in_col = [0, 0]
cols = [BoxLayout(orientation="vertical"), BoxLayout(orientation="vertical")]
for k1, all_d in sorted(self.config.items(), key=lambda tup: -len(tup[1])): # sort to make greedy bin packing work better for k1, all_d in sorted(self.config.items(), key=lambda tup: -len(tup[1])): # sort to make greedy bin packing work better
if k1 in self.ignore_cats: if k1 in self.ignore_cats:
continue continue
@@ -177,59 +181,57 @@ class ConfigPopup(QuickConfigGui):
class ConfigAIPopup(QuickConfigGui): class ConfigAIPopup(QuickConfigGui):
def __init__(self, katrain, popup: Popup, ai_modes: Set, **kwargs): def __init__(self, katrain, popup: Popup, settings):
self.settings = katrain.config("ai") super().__init__(katrain, popup, settings)
super().__init__(katrain, popup, self.settings, **kwargs) self.settings = settings
self.ai_modes = ai_modes Clock.schedule_once(self.build, 0)
Clock.schedule_once(self._build, 0)
self.orientation = "vertical"
def _build(self, _dt): def build(self, _):
colbox = BoxLayout(spacing=5) ais = list(self.settings.keys())
for i, mode in enumerate(self.ai_modes):
mode_settings = self.settings[mode]
num_rows = len(mode_settings) - 2
column = GridLayout(cols=2, rows=max(num_rows, 4) + 3, spacing=1, padding=3)
column.add_widget(ScaledLightLabel(text=f"Settings for AI"))
column.add_widget(ScaledLightLabel(text=f"{mode}", bold=True))
column.add_widget(LightHelpLabel(size_hint=(1, 3), text=mode_settings.get("_help_left", "")))
column.add_widget(LightHelpLabel(size_hint=(1, 3), text=mode_settings.get("_help_right", "")))
for k, v in mode_settings.items():
if not k.startswith("_"):
column.add_widget(ScaledLightLabel(text=f"{k}"))
column.add_widget(ConfigPopup.type_to_widget_class(v)(text=str(v), input_property=f"{mode}/{k}"))
for _ in range(4 - num_rows):
column.add_widget(ScaledLightLabel(text=f""))
column.add_widget(ScaledLightLabel(text=f""))
colbox.add_widget(column)
if i == 0:
colbox.add_widget(BackgroundLabel(text=f"", size_hint=(0.02, 1), background=(1, 1, 1, 1)))
if len(self.ai_modes) == 1: top_bl = BoxLayout()
colbox.add_widget(ScaledLightLabel(text=f"")) top_bl.add_widget(ScaledLightLabel(text="Settings for AI:"))
ai_spinner = StyledSpinner(values=ais, text=ais[0])
ai_spinner.fbind("text", lambda _, text: self.build_ai_options(text))
top_bl.add_widget(ai_spinner)
self.add_widget(top_bl)
self.options_grid = GridLayout(cols=2, rows=max(len(v) for v in self.settings.values()) - 1, size_hint=(1, 7.5), spacing=1) # -1 for help in 1 col
bottom_bl = BoxLayout(spacing=2)
self.info_label = Label() self.info_label = Label()
bl = BoxLayout(size_hint=(1, 0.15), spacing=2) bottom_bl.add_widget(StyledButton(text=f"Apply", on_press=lambda _: self.update_config(False)))
bl.add_widget(StyledButton(text=f"Apply", on_press=lambda _: self.update_config(False))) bottom_bl.add_widget(self.info_label)
bl.add_widget(self.info_label) bottom_bl.add_widget(StyledButton(text=f"Apply and Save", on_press=lambda _: self.update_config(True)))
bl.add_widget(StyledButton(text=f"Apply and Save", on_press=lambda _: self.update_config(True))) self.add_widget(self.options_grid)
self.add_widget(colbox) self.add_widget(bottom_bl)
self.add_widget(bl) self.build_ai_options(ais[0])
def build_ai_options(self, mode):
mode_settings = self.settings[mode]
self.options_grid.clear_widgets()
self.options_grid.add_widget(LightHelpLabel(size_hint=(1, 4), padding=(2, 2), text=mode_settings.get("_help_left", "")))
self.options_grid.add_widget(LightHelpLabel(size_hint=(1, 4), padding=(2, 2), text=mode_settings.get("_help_right", "")))
for k, v in mode_settings.items():
if not k.startswith("_"):
self.options_grid.add_widget(ScaledLightLabel(text=f"{k}"))
self.options_grid.add_widget(ConfigPopup.type_to_widget_class(v)(text=str(v), input_property=f"{mode}/{k}"))
for _ in range(self.options_grid.rows * self.options_grid.cols - len(self.options_grid.children)):
self.options_grid.add_widget(ScaledLightLabel(text=f""))
self.set_properties(self, self.settings)
def update_config(self, save_to_file=False): def update_config(self, save_to_file=False):
try: try:
for k, v in self.collect_properties(self).items(): for k, v in self.collect_properties(self).items():
k1, k2 = k.split("/") k1, k2 = k.split("/")
self.settings[k1][k2] = v
if self.settings[k1][k2] != v: if self.settings[k1][k2] != v:
self.settings[k1][k2] = v
self.katrain.log(f"Updating setting {k} = {v}", OUTPUT_DEBUG) self.katrain.log(f"Updating setting {k} = {v}", OUTPUT_DEBUG)
if save_to_file: if save_to_file:
self.katrain.save_config() self.katrain.save_config()
self.popup.dismiss() self.popup.dismiss()
except InputParseError as e: except InputParseError as e:
self.info_label.text = str(e) self.info_label.text = str(e)
self.katrain.log(e, OUTPUT_ERROR) self.katrain.log(e, OUTPUT_ERROR)
return return
self.popup.dismiss() self.popup.dismiss()
@@ -239,11 +241,10 @@ class ConfigTeacherPopup(QuickConfigGui):
self.sgf_settings = katrain.config("sgf") self.sgf_settings = katrain.config("sgf")
self.ui_settings = katrain.config("board_ui") self.ui_settings = katrain.config("board_ui")
super().__init__(katrain, popup, self.settings, **kwargs) super().__init__(katrain, popup, self.settings, **kwargs)
Clock.schedule_once(self._build, 0) Clock.schedule_once(self.build, 0)
self.orientation = "vertical"
self.spacing = 2 self.spacing = 2
def _build(self, _dt): def build(self, _dt):
thresholds = self.settings["eval_thresholds"] thresholds = self.settings["eval_thresholds"]
undos = self.settings["num_undo_prompts"] undos = self.settings["num_undo_prompts"]
colors = self.ui_settings["eval_colors"] colors = self.ui_settings["eval_colors"]
+2 -5
View File
@@ -52,14 +52,13 @@
<StyledSpinner>: <StyledSpinner>:
text: self.values[0] if self.values else '' text: self.values[0] if self.values else ''
font_size: self.size[1] * 0.33 font_size: self.size[1] * 0.33
sync_height_frac: 1.0 sync_height_frac: 0.7
background_color: [*[c*255/88 for c in BUTTON_COLOR[:3]], 1] # compensate for texture background_color: [*[c*255/88 for c in BUTTON_COLOR[:3]], 1] # compensate for texture
option_cls: 'StyledSpinnerOption' option_cls: 'StyledSpinnerOption'
<StyledToggleButton>: <StyledToggleButton>:
max_lines: 1 max_lines: 1
<StyledTabButton@StyledToggleButton>: <StyledTabButton@StyledToggleButton>:
bold: True bold: True
radius: (self.size[1]/3,self.size[1]/3,0,0) radius: (self.size[1]/3,self.size[1]/3,0,0)
@@ -542,7 +541,6 @@
StyledSpinner: StyledSpinner:
id: B_AI_mode id: B_AI_mode
values: ['Default'] values: ['Default']
sync_height_frac: 0.7
size_hint: 0.3, 1 size_hint: 0.3, 1
on_text: if B_player_mode.children: B_player_mode.children[0].trigger_action(duration=0) on_text: if B_player_mode.children: B_player_mode.children[0].trigger_action(duration=0)
Label: Label:
@@ -560,7 +558,6 @@
StyledSpinner: StyledSpinner:
id: W_AI_mode id: W_AI_mode
size_hint: 0.3, 1 size_hint: 0.3, 1
sync_height_frac: 0.7
values: ['Default'] values: ['Default']
on_text: if W_player_mode.children: W_player_mode.children[0].trigger_action(duration=0) on_text: if W_player_mode.children: W_player_mode.children[0].trigger_action(duration=0)
BoxLayout: BoxLayout:
@@ -635,7 +632,7 @@
size: self.size size: self.size
orientation: 'vertical' orientation: 'vertical'
BadukPanWidget: BadukPanWidget:
id: board_gui id: board_gui
BadukPanControls: BadukPanControls:
id: board_controls id: board_controls
size_hint_y: None size_hint_y: None
+38 -31
View File
@@ -33,6 +33,8 @@ class KaTrainGui(BoxLayout):
self.debug_level = 0 self.debug_level = 0
self.engine = None # type: Optional[KataGoEngine] self.engine = None # type: Optional[KataGoEngine]
self.game = None self.game = None
self.fileselect_popup = None
self.config_popup = None
self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level) self.logger = lambda message, level=OUTPUT_INFO: self.log(message, level)
self._load_config() self._load_config()
@@ -176,22 +178,23 @@ class KaTrainGui(BoxLayout):
self.game.analyze_extra(mode) self.game.analyze_extra(mode)
def _do_analyze_sgf_popup(self): def _do_analyze_sgf_popup(self):
fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8)) if not self.fileselect_popup:
popup_contents = LoadSGFPopup() self.fileselect_popup = Popup(title="Double Click SGF file to analyze", size_hint=(0.8, 0.8))
fileselect_popup.add_widget(popup_contents) popup_contents = LoadSGFPopup()
popup_contents.filesel.path = os.path.expanduser(self.config("sgf/sgf_load")) self.fileselect_popup.add_widget(popup_contents)
popup_contents.filesel.path = os.path.expanduser(self.config("sgf/sgf_load"))
def readfile(files, _mouse): def readfile(files, _mouse):
fileselect_popup.dismiss() self.fileselect_popup.dismiss()
try: try:
move_tree = KaTrainSGF.parse_file(files[0]) move_tree = KaTrainSGF.parse_file(files[0])
except ParseError as e: except ParseError as e:
self.log(f"Failed to load SGF. Parse Error: {e}", OUTPUT_ERROR) self.log(f"Failed to load SGF. Parse Error: {e}", OUTPUT_ERROR)
return return
self._do_new_game(move_tree=move_tree, analyze_fast=popup_contents.fast.active) self._do_new_game(move_tree=move_tree, analyze_fast=popup_contents.fast.active)
popup_contents.filesel.on_submit = readfile popup_contents.filesel.on_submit = readfile
fileselect_popup.open() self.fileselect_popup.open()
def _do_new_game_popup(self): def _do_new_game_popup(self):
new_game_popup = Popup(title="New Game", size_hint=(0.5, 0.6)) new_game_popup = Popup(title="New Game", size_hint=(0.5, 0.6))
@@ -200,10 +203,11 @@ class KaTrainGui(BoxLayout):
new_game_popup.open() new_game_popup.open()
def _do_config_popup(self): def _do_config_popup(self):
config_popup = Popup(title="Edit Settings", size_hint=(0.9, 0.9)) if not self.config_popup:
popup_contents = ConfigPopup(self, config_popup, dict(self._config), ignore_cats=("trainer", "ai")) self.config_popup = Popup(title="Edit Settings", size_hint=(0.9, 0.9))
config_popup.add_widget(popup_contents) popup_contents = ConfigPopup(self, self.config_popup, dict(self._config), ignore_cats=("trainer", "ai"))
config_popup.open() self.config_popup.add_widget(popup_contents)
self.config_popup.open()
def _do_output_sgf(self): def _do_output_sgf(self):
for pl in Move.PLAYERS: for pl in Move.PLAYERS:
@@ -221,6 +225,21 @@ class KaTrainGui(BoxLayout):
self.log(msg, OUTPUT_INFO) self.log(msg, OUTPUT_INFO)
self.controls.set_status(msg) self.controls.set_status(msg)
def load_sgf_from_clipboard(self):
clipboard = Clipboard.paste()
if not clipboard:
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.")
return
try:
move_tree = KaTrainSGF.parse(clipboard)
except Exception as e:
self.controls.set_status(f"Failed to imported game from clipboard: {e}\nClipboard contents: {clipboard[:50]}...")
return
move_tree.nodes_in_tree[-1].analyze(self.engine, analyze_fast=False) # speed up result for looking at end of game
self._do_new_game(move_tree=move_tree, analyze_fast=True)
self("redo", 999)
self.log("Imported game from clipboard.", OUTPUT_INFO)
def on_touch_up(self, touch): def on_touch_up(self, touch):
if self.board_gui.collide_point(*touch.pos) or self.board_controls.collide_point(*touch.pos): if self.board_gui.collide_point(*touch.pos) or self.board_controls.collide_point(*touch.pos):
if touch.button == "scrollup": if touch.button == "scrollup":
@@ -271,20 +290,8 @@ class KaTrainGui(BoxLayout):
elif keycode[1] == "c" and "ctrl" in modifiers: elif keycode[1] == "c" and "ctrl" in modifiers:
Clipboard.copy(self.game.root.sgf()) Clipboard.copy(self.game.root.sgf())
self.controls.set_status("Copied SGF to clipboard.") self.controls.set_status("Copied SGF to clipboard.")
elif keycode[1] == "v" and "ctrl" in modifiers: # TODO: refactor elif keycode[1] == "v" and "ctrl" in modifiers:
clipboard = Clipboard.paste() self.load_sgf_from_clipboard()
if not clipboard:
self.controls.set_status(f"Ctrl-V pressed but clipboard is empty.")
return
try:
move_tree = KaTrainSGF.parse(clipboard)
except Exception as e:
self.controls.set_status(f"Failed to imported game from clipboard: {e}\nClipboard contents: {clipboard[:50]}...")
return
move_tree.nodes_from_root[-1].analyze(self.engine) # speed up result for looking at end of game
self._do_new_game(move_tree=move_tree, analyze_fast=True)
self("redo", 999)
self.log("Imported game from clipboard.", OUTPUT_INFO)
return True return True
View File
Whitespace-only changes.
-50
View File
@@ -1,50 +0,0 @@
import json
import os
import sys
from common import bot_strategy_names
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.",
"influence": "Play an influential style.",
"territory": "Play a territorial style.",
"balanced": "Play the best move out of a random selection.",
"weighted": "Play a policy-weighted move.",
"local": "Prefer local responses.",
"tenuki": "Prefer to tenuki.",
}
with open("config.json") as f:
settings = json.load(f)
all_ai_settings = settings["ai"]
ai_settings = all_ai_settings[bot_strategy_names[bot]]
with open("my/apikey.json") as f:
apikeys = json.load(f)
if bot not in greetings or username not in apikeys:
print("BOT NOT FOUND")
exit(1)
APIKEY = apikeys[username]
settings_dump = ", ".join(f"{k}={v}" for k, v in ai_settings.items() if not k.startswith("_"))
print(settings_dump)
GREETING = (
f"Hello, welcome to an experimental version of KaTrain AIs - These are based on weakened policy nets of KataGo. Current mode is: {greetings[bot]}. Settings: {settings_dump} "
)
BYEMSG = "Thank you for playing. If you have any feedback, please message my admin!"
MAXGAMES = 10
# --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)