move bot code to sep repo
This commit is contained in:
1 parent
d5a78e79dd
commit
545d273cb9
13 files changed
-750
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,3 +0,0 @@
|
|||||||
# Bots
|
|
||||||
This directory contains the source code used to run the katrain-* bots on OGS,
|
|
||||||
and some code to test them. It is liable to break without warning.
|
|
||||||
Whitespace-only changes.
-174
@@ -1,174 +0,0 @@
|
|||||||
# This is a script that turns a KaTrain AI into a sort-of GTP compatible bot
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
|
|
||||||
from katrain.core.ai import ai_move
|
|
||||||
from katrain.core.common import OUTPUT_ERROR, OUTPUT_INFO
|
|
||||||
from bots.settings import bot_strategy_names
|
|
||||||
from katrain.core.engine import EngineDiedException, KataGoEngine
|
|
||||||
from katrain.core.game import Game
|
|
||||||
from katrain.core.sgf_parser import Move
|
|
||||||
|
|
||||||
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
|
|
||||||
REPORT_SCORE_THRESHOLD = 1.5
|
|
||||||
MAX_WAIT_ANALYSIS = 10
|
|
||||||
MAX_PASS = 3 # after opponent passes this many times, we always pass
|
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
|
||||||
def log(self, msg, level):
|
|
||||||
if level <= OUTPUT_INFO:
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
logger = Logger()
|
|
||||||
|
|
||||||
|
|
||||||
ENGINE_SETTINGS = {
|
|
||||||
"katago": f"python bots/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,
|
|
||||||
"max_time": 5.0,
|
|
||||||
"_enable_ownership": False,
|
|
||||||
"threads": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
engine = KataGoEngine(logger, ENGINE_SETTINGS)
|
|
||||||
|
|
||||||
with open("katrain/config.json") as f:
|
|
||||||
settings = json.load(f)
|
|
||||||
all_ai_settings = settings["ai"]
|
|
||||||
|
|
||||||
if bot == "dev":
|
|
||||||
engine.override_settings["maxVisits"] = 500
|
|
||||||
all_ai_settings["dev"] = all_ai_settings["ScoreLoss"]
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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 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.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:
|
|
||||||
comment = cn.comment(sgf=True).replace("\n", " ")
|
|
||||||
msg += f" -> Detailed move analysis: {comment}"
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
sys.stderr.flush()
|
|
||||||
|
|
||||||
|
|
||||||
while True:
|
|
||||||
line = input()
|
|
||||||
logger.log(f"GOT INPUT {line}", OUTPUT_ERROR)
|
|
||||||
if "boardsize" in line:
|
|
||||||
_, *size = line.strip().split(" ")
|
|
||||||
if len(size) > 1:
|
|
||||||
size = f"{size[0]}:{size[1]}"
|
|
||||||
else:
|
|
||||||
size = int(size[0])
|
|
||||||
game = Game(Logger(), engine, {"init_size": size})
|
|
||||||
logger.log(f"Init game {game.root.properties}", OUTPUT_ERROR)
|
|
||||||
elif "komi" in line:
|
|
||||||
_, komi = line.split(" ")
|
|
||||||
game.root.set_property("KM", komi.strip())
|
|
||||||
game.root.set_property("RU", "chinese")
|
|
||||||
logger.log(f"Setting komi {game.root.properties}", OUTPUT_ERROR)
|
|
||||||
elif "place_free_handicap" in line:
|
|
||||||
_, n = line.split(" ")
|
|
||||||
n = int(n)
|
|
||||||
game.place_handicap_stones(n)
|
|
||||||
handicaps = set(game.root.get_list_property("AB"))
|
|
||||||
bx, by = game.board_size
|
|
||||||
while len(handicaps) < min(n, bx * by): # really obscure cases
|
|
||||||
handicaps.add(Move((random.randint(0, bx - 1), random.randint(0, by - 1)), player="B").sgf(board_size=game.board_size))
|
|
||||||
game.root.set_property("AB", list(handicaps))
|
|
||||||
game._calculate_groups()
|
|
||||||
gtp = [Move.from_sgf(m, game.board_size, "B").gtp() for m in handicaps]
|
|
||||||
logger.log(f"Chose handicap placements as {gtp}", OUTPUT_ERROR)
|
|
||||||
print(f"= {' '.join(gtp)}\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
game.analyze_all_nodes() # re-evaluate root
|
|
||||||
while engine.queries: # and make sure this gets processed
|
|
||||||
time.sleep(0.001)
|
|
||||||
continue
|
|
||||||
elif "set_free_handicap" in line:
|
|
||||||
_, *stones = line.split(" ")
|
|
||||||
game.root.set_property("AB", [Move.from_gtp(move.upper()).sgf(game.board_size) for move in stones])
|
|
||||||
game._calculate_groups()
|
|
||||||
game.analyze_all_nodes() # re-evaluate root
|
|
||||||
while engine.queries: # and make sure this gets processed
|
|
||||||
time.sleep(0.001)
|
|
||||||
logger.log(f"Set handicap placements to {game.root.get_list_property('AB')}", OUTPUT_ERROR)
|
|
||||||
elif "genmove" in line:
|
|
||||||
_, player = line.strip().split(" ")
|
|
||||||
if player[0].upper() != game.next_player:
|
|
||||||
logger.log(f"ERROR generating move: UNEXPECTED PLAYER {player} != {game.next_player}.", OUTPUT_ERROR)
|
|
||||||
print(f"= ??\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
continue
|
|
||||||
logger.log(f"{ai_strategy} generating move", OUTPUT_ERROR)
|
|
||||||
game.current_node.analyze(engine)
|
|
||||||
malkovich_analysis(game.current_node)
|
|
||||||
game.root.properties[f"P{game.current_node.next_player}"] = [f"KaTrain {ai_strategy}"]
|
|
||||||
num_passes = sum([int(n.is_pass or False) for n in game.current_node.nodes_from_root[::-1][0 : 2 * MAX_PASS : 2]])
|
|
||||||
bx, by = game.board_size
|
|
||||||
if num_passes >= MAX_PASS and game.current_node.depth - 2 * MAX_PASS >= bx + by:
|
|
||||||
logger.log(f"Forced pass as opponent is passing {MAX_PASS} times", OUTPUT_ERROR)
|
|
||||||
pol = game.current_node.policy
|
|
||||||
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)).move
|
|
||||||
else:
|
|
||||||
move, node = ai_move(game, ai_strategy, ai_settings)
|
|
||||||
logger.log(f"Generated move {move}", OUTPUT_ERROR)
|
|
||||||
print(f"= {move.gtp()}\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
malkovich_analysis(game.current_node)
|
|
||||||
continue
|
|
||||||
elif "play" in line:
|
|
||||||
_, player, move = line.split(" ")
|
|
||||||
node = game.play(Move.from_gtp(move.upper(), player=player[0].upper()), analyze=False)
|
|
||||||
logger.log(f"played {player} {move}", OUTPUT_ERROR)
|
|
||||||
elif "final_score" in line:
|
|
||||||
score = game.current_node.format_score()
|
|
||||||
game.game_id += f"_{score}"
|
|
||||||
sgf = game.write_sgf("sgf_ogs/")
|
|
||||||
logger.log(f"Game ended. Score was {score} -> saved sgf to {sgf}", OUTPUT_ERROR)
|
|
||||||
print(f"= {score}\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
continue
|
|
||||||
elif "quit" in line:
|
|
||||||
print(f"= \n")
|
|
||||||
break
|
|
||||||
print(f"= \n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
Binary file not shown.
@@ -1,26 +0,0 @@
|
|||||||
# used to connect many bots to one kata engine
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8587
|
|
||||||
|
|
||||||
restart = False
|
|
||||||
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
sock = socket.create_connection(("localhost", PORT)).makefile(mode="rw")
|
|
||||||
while True:
|
|
||||||
line = input()
|
|
||||||
sock.write(line + "\n")
|
|
||||||
sock.flush()
|
|
||||||
response = sock.readline()
|
|
||||||
print(response.strip())
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
|
||||||
|
|
||||||
if not restart:
|
|
||||||
break
|
|
||||||
|
|
||||||
print("Failed to connect or disconnected, waiting to reconnect", file=sys.stderr)
|
|
||||||
time.sleep(5)
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# used to connect many bots to one kata engine
|
|
||||||
import json
|
|
||||||
import random
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
from katrain.core.common import OUTPUT_INFO
|
|
||||||
from katrain.core.engine import KataGoEngine
|
|
||||||
|
|
||||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8587
|
|
||||||
|
|
||||||
ENGINE_SETTINGS = {
|
|
||||||
"katago": "my/katago25",
|
|
||||||
# "katago": "KataGo/katago",
|
|
||||||
"model": "katrain/models/g170e-b15c192-s1672170752-d466197061.bin.gz",
|
|
||||||
"config": "katrain/KataGo/analysis_config.cfg",
|
|
||||||
"max_visits": 50,
|
|
||||||
"max_time": 1.0,
|
|
||||||
"_enable_ownership": False,
|
|
||||||
"threads": 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
|
||||||
def log(self, msg, level):
|
|
||||||
if level <= OUTPUT_INFO:
|
|
||||||
print(f"[{level} {msg}")
|
|
||||||
|
|
||||||
|
|
||||||
engine = KataGoEngine(Logger(), ENGINE_SETTINGS)
|
|
||||||
|
|
||||||
|
|
||||||
def engine_thread(conn, addr):
|
|
||||||
sockfile = conn.makefile(mode="rw")
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
print(f"Waiting for input from {addr}")
|
|
||||||
line = sockfile.readline()
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
query = {"id": "???"}
|
|
||||||
try:
|
|
||||||
query = json.loads(line)
|
|
||||||
tag = f"{int(random.random()*1000000000):09d}__"
|
|
||||||
query["id"] = tag + str(query["id"])
|
|
||||||
|
|
||||||
def callback(analysis, *args):
|
|
||||||
print(f"Returning {analysis['id']} for {addr} -> {len(engine.queries)} outstanding queries")
|
|
||||||
analysis["id"] = analysis["id"][len(tag) :]
|
|
||||||
sockfile.write(json.dumps(analysis) + "\n")
|
|
||||||
sockfile.flush()
|
|
||||||
|
|
||||||
engine.send_query(query, callback=callback, error_callback=callback)
|
|
||||||
except Exception as e:
|
|
||||||
print("Sent error to {addr}")
|
|
||||||
traceback.print_exc()
|
|
||||||
sockfile.write(json.dumps({"id": query["id"], "error": str(e)}) + "\n")
|
|
||||||
sockfile.flush()
|
|
||||||
except Exception as e:
|
|
||||||
traceback.print_exc()
|
|
||||||
print(f"Error: {e}")
|
|
||||||
print(f"Disconnected: {addr}")
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
sock.bind(("localhost", PORT))
|
|
||||||
sock.listen(100)
|
|
||||||
print("Listening..")
|
|
||||||
while True:
|
|
||||||
conn, addr = sock.accept()
|
|
||||||
print(f"Accepted connection from {addr}")
|
|
||||||
threading.Thread(target=engine_thread, args=(conn, addr), daemon=True).start()
|
|
||||||
-193
@@ -1,193 +0,0 @@
|
|||||||
# Example config for C++ (non-python) gtp bot
|
|
||||||
|
|
||||||
# SEE NOTES ABOUT PERFORMANCE AND MEMORY USAGE IN gtp_example.cfg
|
|
||||||
|
|
||||||
# Logs------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Where to output log?
|
|
||||||
logFile = gtp.log
|
|
||||||
|
|
||||||
# Controls the number of moves after the first move in a variation.
|
|
||||||
# analysisPVLen = 15
|
|
||||||
|
|
||||||
# Report winrates for analysis as (BLACK|WHITE|SIDETOMOVE).
|
|
||||||
reportAnalysisWinratesAs = BLACK
|
|
||||||
|
|
||||||
# Bot behavior---------------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Handicap -------------
|
|
||||||
|
|
||||||
# Assume that if black makes many moves in a row right at the start of the game, then the game is a handicap game.
|
|
||||||
# This is necessary on some servers and for some GUIs and also when initializing from many SGF files, which may
|
|
||||||
# set up a handicap games using repeated GTP "play" commands for black rather than GTP "place_free_handicap" commands.
|
|
||||||
# However, it may also lead to incorrect undersanding of komi if whiteBonusPerHandicapStone = 1 and a server does NOT
|
|
||||||
# have such a practice.
|
|
||||||
# Defaults to true! Uncomment and set to false to disable this behavior.
|
|
||||||
# assumeMultipleStartingBlackMovesAreHandicap = true
|
|
||||||
|
|
||||||
# Passing and cleanup -------------
|
|
||||||
|
|
||||||
# Make the bot never assume that its pass will end the game, even if passing would end and "win" under Tromp-Taylor rules.
|
|
||||||
# Usually this is a good idea when using it for analysis or playing on servers where scoring may be implemented non-tromp-taylorly.
|
|
||||||
# Defaults to true! Uncomment and set to false to disable this.
|
|
||||||
conservativePass = true
|
|
||||||
|
|
||||||
# When using territory scoring, self-play games continue beyond two passes with special cleanup
|
|
||||||
# rules that may be confusing for human players. This option prevents the special cleanup phases from being
|
|
||||||
# reachable when using the bot for GTP play.
|
|
||||||
# Defaults to true! Uncomment and set to false if you want KataGo to be able to enter special cleanup.
|
|
||||||
# For example, if you are testing it against itself, or against another bot that has precisely implemented the rules
|
|
||||||
# documented at https://lightvector.github.io/KataGo/rules.html
|
|
||||||
# preventCleanupPhase = true
|
|
||||||
|
|
||||||
# Search limits-----------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# By default, if NOT specified in an individual request, limit maximum number of root visits per search to this much
|
|
||||||
maxVisits = 500
|
|
||||||
# If provided, cap search time at this many seconds
|
|
||||||
# maxTime = 60
|
|
||||||
|
|
||||||
# Number of threads to use in each search in parallel for any SINGLE position.
|
|
||||||
# NOTE: Analysis engine can specify number of POSITIONS to be able to search in parallel via command line argument
|
|
||||||
# so this number does not necessarily need to be larger than 1, although you can still set it larger if you prefer
|
|
||||||
# to analyze fewer positions in parallel but spend more threads on each position.
|
|
||||||
# Generally, having more threads on a single position will worsen the quality of search slightly, holding fixed the
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# GPU Settings-------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Maximum number of positions to send to GPU at once.
|
|
||||||
nnMaxBatchSize = 32
|
|
||||||
# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
|
|
||||||
nnCacheSizePowerOfTwo = 14
|
|
||||||
# Size of mutex pool for nnCache is 2 ** this
|
|
||||||
nnMutexPoolSizePowerOfTwo = 14
|
|
||||||
# Randomize board orientation when running neural net evals?
|
|
||||||
nnRandomize = true
|
|
||||||
|
|
||||||
|
|
||||||
# TO USE MULTIPLE GPUS:
|
|
||||||
# Set this to the number of GPUs you have and/or would like to use...
|
|
||||||
# AND if it is more than 1, uncomment the appropriate CUDA or OpenCL section below.
|
|
||||||
# numNNServerThreadsPerModel = 1
|
|
||||||
|
|
||||||
|
|
||||||
# CUDA GPU settings--------------------------------------
|
|
||||||
# These only apply when using the CUDA version of KataGo.
|
|
||||||
|
|
||||||
# IF USING ONE GPU: optionally uncomment and change this if the GPU you want to use turns out to be not device 0
|
|
||||||
# cudaDeviceToUse = 0
|
|
||||||
|
|
||||||
# IF USING TWO GPUS: Uncomment these two lines (AND set numNNServerThreadsPerModel above):
|
|
||||||
# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
|
|
||||||
# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
|
|
||||||
|
|
||||||
# IF USING THREE GPUS: Uncomment these three lines (AND set numNNServerThreadsPerModel above):
|
|
||||||
# cudaDeviceToUseThread0 = 0 # change this if the first GPU you want to use turns out to be not device 0
|
|
||||||
# cudaDeviceToUseThread1 = 1 # change this if the second GPU you want to use turns out to be not device 1
|
|
||||||
# cudaDeviceToUseThread2 = 2 # change this if the third GPU you want to use turns out to be not device 2
|
|
||||||
|
|
||||||
# You can probably guess the pattern if you have four, five, etc. GPUs.
|
|
||||||
|
|
||||||
# KataGo will automatically use FP16 or not based on the compute capability of your NVIDIA GPU. If you
|
|
||||||
# want to try to force a particular behavior though you can uncomment these lines and change them
|
|
||||||
# to "true" or "false". E.g. it's using FP16 but on your card that's giving an error, or it's not using
|
|
||||||
# FP16 but you think it should.
|
|
||||||
# cudaUseFP16 = auto
|
|
||||||
# cudaUseNHWC = auto
|
|
||||||
|
|
||||||
|
|
||||||
# OpenCL GPU settings--------------------------------------
|
|
||||||
# These only apply when using the OpenCL version of KataGo.
|
|
||||||
|
|
||||||
# Uncomment to tune OpenCL for every board size separately, rather than only the largest possible size
|
|
||||||
# openclReTunePerBoardSize = true
|
|
||||||
|
|
||||||
# IF USING ONE GPU: optionally uncomment and change this if the best device to use is guessed incorrectly.
|
|
||||||
# The default behavior tries to guess the 'best' GPU or device on your system to use, usually it will be a good guess.
|
|
||||||
# openclDeviceToUse = 0
|
|
||||||
|
|
||||||
# IF USING TWO GPUS: Uncomment these two lines and replace X and Y with the device ids of the devices you want to use.
|
|
||||||
# It might NOT be 0 and 1, some computers will have many OpenCL devices. You can see what the devices are when
|
|
||||||
# KataGo starts up - it should print or log all the devices it finds.
|
|
||||||
# (AND also set numNNServerThreadsPerModel above)
|
|
||||||
# openclDeviceToUseThread0 = X
|
|
||||||
# openclDeviceToUseThread1 = Y
|
|
||||||
|
|
||||||
# IF USING THREE GPUS: Uncomment these three lines and replace X and Y and Z with the device ids of the devices you want to use.
|
|
||||||
# It might NOT be 0 and 1 and 2, some computers will have many OpenCL devices. You can see what the devices are when
|
|
||||||
# KataGo starts up - it should print or log all the devices it finds.
|
|
||||||
# (AND also set numNNServerThreadsPerModel above)
|
|
||||||
# openclDeviceToUseThread0 = X
|
|
||||||
# openclDeviceToUseThread1 = Y
|
|
||||||
# openclDeviceToUseThread2 = Z
|
|
||||||
|
|
||||||
# You can probably guess the pattern if you have four, five, etc. GPUs.
|
|
||||||
|
|
||||||
|
|
||||||
# Root move selection and biases------------------------------------------------------------------------------
|
|
||||||
# Uncomment and edit any of the below values to change them from their default.
|
|
||||||
# Not all of these parameters are applicable to analysis, some are only used for actual play
|
|
||||||
|
|
||||||
# Temperature for the early game, randomize between chosen moves with this temperature
|
|
||||||
# chosenMoveTemperatureEarly = 0.5
|
|
||||||
# Decay temperature for the early game by 0.5 every this many moves, scaled with board size.
|
|
||||||
# chosenMoveTemperatureHalflife = 19
|
|
||||||
# At the end of search after the early game, randomize between chosen moves with this temperature
|
|
||||||
# chosenMoveTemperature = 0.10
|
|
||||||
# Subtract this many visits from each move prior to applying chosenMoveTemperature
|
|
||||||
# (unless all moves have too few visits) to downweight unlikely moves
|
|
||||||
# chosenMoveSubtract = 0
|
|
||||||
# The same as chosenMoveSubtract but only prunes moves that fall below the threshold, does not affect moves above
|
|
||||||
# chosenMovePrune = 1
|
|
||||||
|
|
||||||
# Number of symmetries to sample (WITH replacement) and average at the root
|
|
||||||
# rootNumSymmetriesToSample = 1
|
|
||||||
|
|
||||||
# Using LCB for move selection?
|
|
||||||
# useLcbForSelection = true
|
|
||||||
# How many stdevs a move needs to be better than another for LCB selection
|
|
||||||
# lcbStdevs = 5.0
|
|
||||||
# Only use LCB override when a move has this proportion of visits as the top move
|
|
||||||
# minVisitPropForLCB = 0.15
|
|
||||||
|
|
||||||
# Internal params------------------------------------------------------------------------------
|
|
||||||
# Uncomment and edit any of the below values to change them from their default.
|
|
||||||
|
|
||||||
# Scales the utility of winning/losing
|
|
||||||
# winLossUtilityFactor = 1.0
|
|
||||||
# Scales the utility for trying to maximize score
|
|
||||||
# staticScoreUtilityFactor = 0.10
|
|
||||||
# dynamicScoreUtilityFactor = 0.30
|
|
||||||
# Adjust dynamic score center this proportion of the way towards zero, capped at a reasonable amount.
|
|
||||||
# dynamicScoreCenterZeroWeight = 0.20
|
|
||||||
# dynamicScoreCenterScale = 0.75
|
|
||||||
# The utility of getting a "no result" due to triple ko or other long cycle in non-superko rulesets (-1 to 1)
|
|
||||||
# noResultUtilityForWhite = 0.0
|
|
||||||
# The number of wins that a draw counts as, for white. (0 to 1)
|
|
||||||
# drawEquivalentWinsForWhite = 0.5
|
|
||||||
|
|
||||||
# Exploration constant for mcts
|
|
||||||
# cpuctExploration = 0.9
|
|
||||||
# cpuctExplorationLog = 0.4
|
|
||||||
# FPU reduction constant for mcts
|
|
||||||
# fpuReductionMax = 0.2
|
|
||||||
# rootFpuReductionMax = 0.1
|
|
||||||
# Use parent average value for fpu base point instead of point value net estimate
|
|
||||||
# fpuUseParentAverage = true
|
|
||||||
# Amount to apply a downweighting of children with very bad values relative to good ones
|
|
||||||
# valueWeightExponent = 0.5
|
|
||||||
# Slight incentive for the bot to behave human-like with regard to passing at the end, filling the dame,
|
|
||||||
# not wasting time playing in its own territory, etc, and not play moves that are equivalent in terms of
|
|
||||||
# points but a bit more unfriendly to humans.
|
|
||||||
# rootEndingBonusPoints = 0.5
|
|
||||||
# Make the bot prune useless moves that are just prolonging the game to avoid losing yet
|
|
||||||
# rootPruneUselessMoves = true
|
|
||||||
|
|
||||||
# How big to make the mutex pool for search synchronization
|
|
||||||
# mutexPoolSize = 8192
|
|
||||||
# How many virtual losses to add when a thread descends through a node
|
|
||||||
# numVirtualLossesPerThread = 1
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
# This is a script I use to test the performance of AIs
|
|
||||||
import pickle
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
from collections import defaultdict
|
|
||||||
from concurrent.futures.thread import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from katrain.core.ai import ai_move
|
|
||||||
from katrain.core.common import OUTPUT_ERROR, OUTPUT_INFO
|
|
||||||
from elote import EloCompetitor
|
|
||||||
from katrain.core.engine import KataGoEngine
|
|
||||||
from katrain.core.game import Game
|
|
||||||
import json
|
|
||||||
|
|
||||||
DB_FILENAME = "bots/ai_performance.pickle"
|
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
|
||||||
def log(self, msg, level):
|
|
||||||
if level <= OUTPUT_INFO:
|
|
||||||
print(msg)
|
|
||||||
if level <= OUTPUT_ERROR:
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
logger = Logger()
|
|
||||||
|
|
||||||
with open("config.json") as f:
|
|
||||||
settings = json.load(f)
|
|
||||||
DEFAULT_AI_SETTINGS = settings["ai"]
|
|
||||||
|
|
||||||
|
|
||||||
class AI:
|
|
||||||
DEFAULT_ENGINE_SETTINGS = {
|
|
||||||
"katago": "KataGo/katago",
|
|
||||||
"model": "KataGo/models/b15-1.3.2.txt.gz",
|
|
||||||
"config": "bots/lowmem.cfg",
|
|
||||||
"max_visits": 1,
|
|
||||||
"max_time": 300.0,
|
|
||||||
"_enable_ownership": False,
|
|
||||||
}
|
|
||||||
NUM_THREADS = 32
|
|
||||||
IGNORE_SETTINGS_IN_TAG = {"threads", "_enable_ownership", "katago"} # katago for switching from/to bs version
|
|
||||||
ENGINES = []
|
|
||||||
LOCK = threading.Lock()
|
|
||||||
|
|
||||||
def __init__(self, strategy, ai_settings, engine_settings=None):
|
|
||||||
self.elo_comp = EloCompetitor(initial_rating=1000)
|
|
||||||
self.strategy = strategy
|
|
||||||
self.ai_settings = ai_settings
|
|
||||||
self.engine_settings = engine_settings or {}
|
|
||||||
fmt_settings = [f"{k}={v}" for k, v in {**self.ai_settings, **self.engine_settings}.items() if k not in AI.IGNORE_SETTINGS_IN_TAG]
|
|
||||||
self.name = f"{strategy}({ ','.join(fmt_settings) })"
|
|
||||||
self.fix_settings()
|
|
||||||
|
|
||||||
def fix_settings(self):
|
|
||||||
self.ai_settings = {**DEFAULT_AI_SETTINGS[self.strategy], **self.ai_settings}
|
|
||||||
self.engine_settings = {**AI.DEFAULT_ENGINE_SETTINGS, **self.engine_settings, "threads": AI.NUM_THREADS}
|
|
||||||
|
|
||||||
def get_engine(self): # factory
|
|
||||||
with AI.LOCK:
|
|
||||||
for existing_engine_settings, engine in AI.ENGINES:
|
|
||||||
if existing_engine_settings == self.engine_settings:
|
|
||||||
return engine
|
|
||||||
engine = KataGoEngine(logger, self.engine_settings)
|
|
||||||
AI.ENGINES.append((self.engine_settings, engine))
|
|
||||||
print("Creating new engine for", self.engine_settings, "now have", len(AI.ENGINES), "engines up")
|
|
||||||
return engine
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.name == other.name # should capture all relevant setting differences
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(DB_FILENAME, "rb") as f:
|
|
||||||
ai_database_loaded, all_results = pickle.load(f)
|
|
||||||
ai_database = []
|
|
||||||
for ai in ai_database_loaded:
|
|
||||||
try:
|
|
||||||
ai.fix_settings() # update as required
|
|
||||||
ai_database.append(ai)
|
|
||||||
except:
|
|
||||||
print("Error loading AI", ai.strategy)
|
|
||||||
except FileNotFoundError:
|
|
||||||
ai_database = []
|
|
||||||
all_results = []
|
|
||||||
|
|
||||||
|
|
||||||
def add_ai(ai):
|
|
||||||
if ai not in ai_database:
|
|
||||||
ai_database.append(ai)
|
|
||||||
print(f"Adding {ai.name}")
|
|
||||||
else:
|
|
||||||
print(f"AI {ai.name} already in DB")
|
|
||||||
|
|
||||||
|
|
||||||
def retrieve_ais(selected_ais):
|
|
||||||
return [ai for ai in ai_database if ai in selected_ais]
|
|
||||||
|
|
||||||
|
|
||||||
test_ais = [
|
|
||||||
AI("Default", {}, {"model": "bots/6b.bin.gz", "max_visits": 500}),
|
|
||||||
AI("Default", {}, {"model": "bots/6b104-s22347264.txt.gz", "max_visits": 500}),
|
|
||||||
AI("Default", {}, {"model": "bots/6b104-s42364928.txt.gz", "max_visits": 500}),
|
|
||||||
# AI("Default", {}, {"model": "KataGo/models/b10-1.3.txt.gz", "max_visits": 500}),
|
|
||||||
AI("Policy", {}),
|
|
||||||
AI("P:Local", {}),
|
|
||||||
AI("P:Weighted", {}),
|
|
||||||
AI("P:Pick", {}),
|
|
||||||
AI("ScoreLoss", {"max_visits": 500}),
|
|
||||||
# AI("P:Tenuki", {}),
|
|
||||||
# AI("P:Local", {}),
|
|
||||||
AI("P:Influence", {}),
|
|
||||||
# AI("P:Territory", {}),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
for ai in test_ais:
|
|
||||||
add_ai(ai)
|
|
||||||
|
|
||||||
N_GAMES = 1
|
|
||||||
BOARDSIZE = 19
|
|
||||||
|
|
||||||
ais_to_test = retrieve_ais(test_ais)
|
|
||||||
|
|
||||||
results = defaultdict(list)
|
|
||||||
|
|
||||||
|
|
||||||
def play_games(black: AI, white: AI):
|
|
||||||
players = {"B": black, "W": white}
|
|
||||||
engines = {"B": black.get_engine(), "W": white.get_engine()}
|
|
||||||
tag = f"{black.name} vs {white.name}"
|
|
||||||
try:
|
|
||||||
game = Game(Logger(), engines, {"init_size": BOARDSIZE})
|
|
||||||
game.root.add_list_property("PW", [white.name])
|
|
||||||
game.root.add_list_property("PB", [black.name])
|
|
||||||
start_time = time.time()
|
|
||||||
while not game.ended and game.current_node.depth < 300:
|
|
||||||
p = game.current_node.next_player
|
|
||||||
move, node = ai_move(game, players[p].strategy, players[p].ai_settings)
|
|
||||||
while not game.current_node.analysis_ready:
|
|
||||||
time.sleep(0.001)
|
|
||||||
game.game_id += f"_{game.current_node.format_score()}"
|
|
||||||
print(
|
|
||||||
f"{tag}\tGame finished in {time.time()-start_time:.1f}s @ move {game.current_node.depth} {game.current_node.format_score()} -> {game.write_sgf('sgf_selfplay/')}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
score = game.current_node.score
|
|
||||||
if score > 0.3:
|
|
||||||
black.elo_comp.beat(white.elo_comp)
|
|
||||||
elif score > -0.3:
|
|
||||||
black.elo_comp.tied(white.elo_comp)
|
|
||||||
|
|
||||||
results[tag].append(score)
|
|
||||||
all_results.append((black.name, white.name, score))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Exception in playing {tag}: {e}")
|
|
||||||
print(f"Exception in playing {tag}: {e}", file=sys.stderr)
|
|
||||||
traceback.print_exc()
|
|
||||||
traceback.print_exc(file=sys.stderr)
|
|
||||||
|
|
||||||
|
|
||||||
def fmt_score(score):
|
|
||||||
return f"{'B' if score >= 0 else 'W'}+{abs(score):.1f}"
|
|
||||||
|
|
||||||
|
|
||||||
print(len(ais_to_test), "ais to test")
|
|
||||||
global_start = time.time()
|
|
||||||
|
|
||||||
for n in range(N_GAMES):
|
|
||||||
for _, e in AI.ENGINES: # no caching/replays
|
|
||||||
e.shutdown()
|
|
||||||
AI.ENGINES = []
|
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=16) as threadpool:
|
|
||||||
for b in ais_to_test:
|
|
||||||
for w in ais_to_test:
|
|
||||||
if b is not w:
|
|
||||||
threadpool.submit(play_games, b, w)
|
|
||||||
print("POOL EXIT")
|
|
||||||
|
|
||||||
print(f"---- RESULTS ({n}) ----")
|
|
||||||
for k, v in results.items():
|
|
||||||
b_win = sum([s > 0.3 for s in v])
|
|
||||||
w_win = sum([s < -0.3 for s in v])
|
|
||||||
print(f"{b_win} {k} {w_win} : {list(map(fmt_score,v))}")
|
|
||||||
|
|
||||||
print("---- ELO ----")
|
|
||||||
for ai in sorted(ai_database, key=lambda a: -a.elo_comp.rating):
|
|
||||||
wins = [(b, w, s) for (b, w, s) in all_results if s > 0.3 and b == ai.name or w == ai.name and s < -0.3]
|
|
||||||
losses = [(b, w, s) for (b, w, s) in all_results if s < -0.3 and b == ai.name or w == ai.name and s > -0.3]
|
|
||||||
draws = [(b, w, s) for (b, w, s) in all_results if -0.3 <= s <= 0.3 and (b == ai.name or w == ai.name)]
|
|
||||||
out = f"{'*' if ai in ais_to_test else ' '} {ai.name}: ELO {ai.elo_comp.rating:.1f} WINS {len(wins)} LOSSES {len(losses)} DRAWS {len(draws)}"
|
|
||||||
# print("Wins:",wins)
|
|
||||||
print(out)
|
|
||||||
print(out, file=sys.stderr)
|
|
||||||
|
|
||||||
with open(DB_FILENAME, "wb") as f:
|
|
||||||
pickle.dump((ai_database, all_results), f)
|
|
||||||
print(f"Saving {len(all_results)} to pickle", file=sys.stderr)
|
|
||||||
|
|
||||||
print(f"Done!Time taken {time.time()-global_start:.1f}s", file=sys.stderr)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
bot_strategy_names = {
|
|
||||||
# "dev": "P:Noise",
|
|
||||||
"dev": "ScoreLoss",
|
|
||||||
"dev-beta": "P:Weighted",
|
|
||||||
"strong": "Policy",
|
|
||||||
"influence": "P:Influence",
|
|
||||||
"territory": "P:Territory",
|
|
||||||
"balanced": "P:Pick",
|
|
||||||
"weighted": "P:Weighted",
|
|
||||||
"local": "P:Local",
|
|
||||||
"tenuki": "P:Tenuki",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
greetings = {
|
|
||||||
# "dev": "Policy+Dirichlet noise.",
|
|
||||||
"dev": "Point loss-weighted random move.",
|
|
||||||
"dev-beta": "Play a policy-weighted move.",
|
|
||||||
"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.",
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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
|
|
||||||
if True or bot in ["dev", "local"]:
|
|
||||||
GTP2OGS = "node ../gtp2ogs"
|
|
||||||
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}"
|
|
||||||
|
|
||||||
with open("katrain/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, play with these bots at any time by downloading KaTrain at github.com/sanderland/katrain - Current mode is {bot_strategy_names[bot]} ({greetings[bot]})"
|
|
||||||
if settings:
|
|
||||||
GREETING += f" Settings: {settings_dump}."
|
|
||||||
BYEMSG = "Thank you for playing. If you have any feedback, please message my admin! "
|
|
||||||
|
|
||||||
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)
|
|
||||||
Reference in new issue
Block a user