initial move to github

This commit is contained in:
Sander Land committed 2019-12-09 17:37:31 +01:00
1 parent 336a8fb41b
commit acd5923ab5
14 files changed
+1582

No files matched your search

+8
View File
@@ -1,3 +1,11 @@
# Mine
KataGoData
old
models
.idea
gtp.log
*.zip
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
+74
View File
@@ -0,0 +1,74 @@
Manual
======
Installation
------------
* pip install kivy
* change the `engine.command` field in `config.json` to your kata installation (for example, to `path/to/lizzie/katago/katago.exe`)
* start the app by running `python katrain.py` (or `python3` if needed)
Options
-------
* Top row options
* Eval: show the coloured dots on the moves for this player.
* Hints: show suggested moves for this player.
* Undo: automatically undo poor moves for this player and make them try again.
* Lock AI: disallow extra undos, changing hints options, changing auto move, or AI move.
* Show owner: show expected control of territory.
* Temperature/Evaluation/Score: Not that these fields can be hidden by clicking on the text.
* Temperature is the point difference between passing and the best move.
* Evaluation is where on this scale the last move was, from 0% (equivalent to a pass) to 100% (best move).
This can be < 0% in case of suicidal moves, or >100% when Kata did not consider the move before, or further analysis shows it to be better than the best one considered.
* Score: Expected score.
Play
----
* Play against the AI
* Turn on auto move.
* Choose whether to turn on `balance score` to make the AI play slack moves.
* Choose whether to turn on `undo` for your colour to be prompted to re-try poor moves.
* Choose whether or not to turn on `fast` to make the AI play faster but read less deeply (NB: with balance score, faster AI can be a stronger opponent, as there are fewer mediocre moves considered).
* Possibly lock AI to prevent yourself from peeking at hints, etc.
* Possibly hide score or temperature.
* Possibly hide evaluation for the AI player.
* Play by playing a move or clicking AI move if you want white.
* Engine-assisted play
* Turn off auto move.
* Choose whether to turn on `undo` for either colour to be prompted to re-try poor moves.
* Possibly lock AI to prevent peeking at hints.
* Possibly hide score or temperature.
* Play with a friend with instant feedback and/or undos for both, or see how many stones stronger you are with one undo. (But please play unranked and be honest to your opponent on what you're doing)
* Analysis
* Copy the SGF into the text box
* Choose whether or not to turn on `fast` to make the AI weaker but analyze faster.
* Click `Analyze`
* Save game
* Click save to get an sgf as `out.sgf` with comments (and a short version in the text box).
Configuration
-------------
`config.json` has a number of options, many of them are stylistic.
The `trainer` block has the following options to tweak:
* `balance_play_target_score`: indicates how many points the AI aims to win by when using 'balance score'.
* `balance_play_randomize_eval`: when not needing to balance score, the AI will pick a random move which is at least this good.
* `balance_play_min_eval`: when needing to balance score, the AI will pick a move which is at least this good.
* `balance_play_min_visits`: never pick a move with fewer playouts than this.
* `undo_eval_threshold`, `undo_point_threshold`: prompt player to undo if move is worse than this in terms of points AND evaluation.
* `undo_outdated_eval_threshold`: don't prompt undo if last move's evaluation is >= `undo_eval_threshold` and the NEW evaluation is greater than this. (this decreases frustration when hints are on, or when kata over-estimates the best move).
* `num_undo_prompts`: automatically undo bad moves when `undo` is on at most this many times.
* `show_ai_options`: show which moves the AI considered.
The cfg file has additional configuration for kata. In particular, it changes the default to being more exploratory and score-based (and therefore nicer as an opponent, but weaker as analysis tool).
TODO
----
* Prisoner count
* Better name
* ....
View File
Whitespace-only changes.
+46
View File
@@ -0,0 +1,46 @@
{
"analysis": {
"pass_visits": 200,
"pass_visits_fast": 50,
"visits": 3500,
"visits_fast": 1500,
"nopass_visits": 10
},
"board": {
"size": 19,
"komi": 7.5
},
"ui": {
"size_min": 1,
"size_max": 15,
"stones": [ [0.05, 0.05, 0.05], [0.95, 0.95, 0.95] ],
"ghost_alpha": 0.5,
"eval_colors": [[0.537, 0.129, 0.42], [1, 0, 0], [1, 0.95, 0], [0.117, 0.588, 0]],
"undo_circle_col": [0.88,0.02,0.17,0.5],
"undo_alpha": 0.5,
"eval_knots": [0, 0.5, 0.875, 1],
"eval_bounds": [1,12],
"board_margin": 1.5,
"starpoint_size": 0.1,
"stone_size": 0.475,
"board_color": [0.85, 0.68, 0.40],
"line_color": [0,0,0]
},
"engine": {
"command": "../lizzie/katago/katago.exe gtp -model models/b10.gz -config gtp_explore_score.cfg"
},
"trainer": {
"balance_play_target_score": 2,
"balance_play_randomize_eval": 0.95,
"balance_play_min_eval": 0.875,
"balance_play_min_visits": 20,
"undo_eval_threshold": 0.875,
"undo_outdated_eval_threshold": 0.8,
"undo_point_threshold": 1,
"num_undo_prompts": 1,
"show_ai_options": true
},
"debug": {
"level": 1
}
}
+99
View File
@@ -0,0 +1,99 @@
from kivy.storage.jsonstore import JsonStore
from kivy.uix.gridlayout import GridLayout
from engine import KataEngine
from move import Move
Config = JsonStore("config.json")
class EngineControls(GridLayout):
def __init__(self, **kwargs):
super(EngineControls, self).__init__(**kwargs)
self.engine = KataEngine(self, Config)
def restart(self, boardsize=None):
self.engine.restart(boardsize)
def action(self, message, *args):
self.engine.action(message, *args)
@property
def ready(self):
return self.engine.ready
@property
def boardsize(self):
return self.engine.boardsize
@property
def stones(self):
return self.engine.stones
@property
def moves(self):
return self.engine.moves
@property
def current_player(self):
return self.engine.current_player()
def redraw(self, include_board=False):
if include_board:
self.parent.board.draw_board()
self.parent.board.redraw()
def update_analysis(self, analysis, mode, ownership):
for d in analysis:
d["scoreMean"] = float(d["scoreMean"])
if mode == 0:
pm = [d for d in analysis if d["move"] == "pass"]
npm = [d for d in analysis if d["move"] != "pass"]
if pm:
pv = sum([int(d["visits"]) for d in pm], 0)
npv = sum([int(d["visits"]) for d in npm], 0)
print("pass visits", pv, "other", npv)
if pv > npv:
print(analysis)
self.moves[-1].pass_analysis = [d for d in analysis if d["move"] != "pass"]
else:
if ownership:
self.moves[-1].ownership = [float(p) for p in ownership[0].strip().split(" ")]
best = analysis[0]["scoreMean"]
worst = -self.moves[-1].pass_analysis[0]["scoreMean"]
for d in analysis:
d["evaluation"] = (d["scoreMean"] - worst) / (best - worst)
self.moves[-1].analysis = analysis
if self.eval.active(1 - self.current_player):
self.temperature.text = f"{self.moves[-1].temperature():.1f}"
self.score.text = f"{Move.PLAYERS[self.current_player]}{float(analysis[0]['scoreMean']):+.1f}".replace("-", "\u2013") # en dash
if len(self.moves) >= 2 and self.moves[-2].analysis:
self.moves[-1].evaluate(self.moves[-2])
if self.eval.active(1 - self.current_player):
if self.moves[-1].evaluation:
self.evaluation.text = f"{100 * self.moves[-1].evaluation:.1f}%"
else:
self.evaluation.text = "N/A"
self.redraw(include_board=False) # for dots and stuff
def sgf(self):
def sgfify(mvs):
return f"(;GM[1]FF[4]SZ[{self.boardsize}]KM[{self.engine.komi}]RU[CN];" + ";".join(mvs) + ")"
def format_move(m, pm):
undo_comment = "".join(f"\nUndo: {u.gtp()} was {100*u.evaluation:.1f}%" for u in pm.undos if u.evaluation)
undo_cr = "".join(f"MA[{u.sgfcoords(self.boardsize)}]" for u in pm.undos if u.coords[0])
if pm.analysis and pm.analysis[0]["move"] != "pass":
best_sq = f"SQ[{Move(gtpcoords=pm.analysis[0]['move'],player=0).sgfcoords(self.boardsize)}]"
else:
best_sq = ""
return m.sgf(self.boardsize) + f"C[{m.comment}{undo_comment}]{undo_cr}{best_sq}"
sgfmoves_small = [mv.sgf(self.boardsize) for mv in self.moves[1:]]
sgfmoves = [format_move(mv, pmv) for mv, pmv in zip(self.moves[1:], self.moves[:-1])]
with open("out.sgf", "w") as f:
f.write(sgfify(sgfmoves))
return sgfify(sgfmoves_small)
+278
View File
@@ -0,0 +1,278 @@
import re
import random
import shlex
import subprocess
import threading
import time
from queue import Queue
from move import Move
class KataEngine:
def __init__(self, controls, config):
self.controls = controls
self.command = shlex.split(config.get("engine")["command"])
analysis_settings = config.get("analysis")
self.visits = [[analysis_settings["pass_visits"], analysis_settings["visits"]], [analysis_settings["pass_visits_fast"], analysis_settings["visits_fast"]]]
self.min_nopass_visits = analysis_settings["nopass_visits"]
self.train_settings = config.get("trainer")
self.debug = config.get("debug")["level"]
self.boardsize = config.get("board")["size"]
self.komi = config.get("board")["komi"]
self.ready = False
self.stones = []
self.message_queue = None
self.moves = [Move(player=1, coords=(None, None))] # sentinel
self.kata = None
def current_player(self):
return 1 - self.moves[-1].player
def restart(self, boardsize):
self.ready = False
if not self.message_queue:
self.message_queue = Queue()
self.analysis_semaphore = threading.Semaphore(1)
self.stop_analyzing = True
self.thread = threading.Thread(target=self._engine_thread, daemon=True).start()
else:
with self.message_queue.mutex:
self.message_queue.queue.clear()
self.stones = []
self.action("init", boardsize or self.boardsize)
def action(self, message, *args):
self.message_queue.put([message, *args])
def gtpread(self):
lines = []
while self.kata:
lines.append(self.kata.stdout.readline().decode())
if lines[-1].strip() == "":
break
return lines[:-1]
def gtpwrite(self, cmd):
if self.debug:
print("WRITE", cmd)
try:
self.kata.stdin.write((cmd + "\n").encode("utf-8"))
self.kata.stdin.flush()
except Exception:
self.controls.info.text = "Engine died, please restart app"
raise
def gtpcommand(self, cmd):
self.gtpwrite(cmd)
return self.gtpread()
def raw_gtpplaycommand(self, move):
if move == "undo":
output = self.gtpcommand("undo")
else:
output = self.gtpcommand(f"play {Move.PLAYERS[move.player]} {move.gtp()}")
output = "".join(output)
if self.debug and "?" in output:
print(move, output)
return "?" not in output
def update_stones(self):
board_output = self.gtpcommand("showboard")
board = [re.sub(r"[^\.ox]", "", l.lower()) for l in board_output[2:]]
self.stones = []
for y, line in enumerate(board[::-1]):
for x, st in enumerate(line):
if st != ".":
self.stones.append(("xo".index(st), x, y))
self.controls.redraw(include_board=False)
def gtpplaycommand(self, move):
self.stop_analyzing = True
self.analysis_semaphore.acquire()
if self.raw_gtpplaycommand(move): # update moves array if engine accepts move
if move == "undo":
self.moves[-2].undos.append(self.moves[-1])
self.moves.pop()
else:
self.moves[-1].undos = [m for m in self.moves[-1].undos if m.coords != move.coords]
self.moves.append(move)
self.update_stones()
# start analyzing new board position
self.stop_analyzing = False
self.analysis_semaphore.release()
# engine main loop
def _engine_thread(self):
self.kata = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
print(self.command, self.kata)
analysis_thread = threading.Thread(target=self._analyze_thread, args=(25,), daemon=True).start()
self.stop_analyzing = False
msg, *args = self.message_queue.get()
while True:
try:
if self.debug:
print("MESSAGE", msg, args)
getattr(self, f"_do_{msg.replace('-','_')}")(*args)
except Exception as e:
self.controls.info.text = f"Exception in Engine thread: {e}"
raise
msg, *args = self.message_queue.get()
# engine action functions
def _do_play(self, *args):
self.gtpplaycommand(Move(player=self.current_player(), coords=args[0]))
self.controls.undo.disabled = True # undo while waiting for this does weird things
undid = False
self.controls.info.text = ""
if self.controls.auto_undo.active(1 - self.current_player()):
print("undo active", self.current_player(), self.controls.auto_undo.active(self.current_player()))
undid = self._auto_undo()
if self.controls.ai_auto.active and not undid:
self._do_aimove(True)
self.controls.undo.disabled = False
def _evaluate_move(self, show=True):
while not self.moves[-1].analysis: # ensure analysis has started, otherwise race condition on multi ai move
time.sleep(0.01)
self.analysis_semaphore.acquire() and self.analysis_semaphore.release() # wait for analysis to finish
if self.moves[-1].evaluation and show:
self.controls.info.text = f"Your move {self.moves[-1].gtp()} was {100 * self.moves[-1].evaluation:.1f}% efficient and lost {self.moves[-1].points_lost:.1f} point(s).\n"
def _auto_undo(self):
ts = self.train_settings
self.controls.info.text = "Evaluating..."
self._evaluate_move()
if (
self.moves[-1].evaluation
and self.moves[-1].evaluation < ts["undo_eval_threshold"]
and self.moves[-1].points_lost >= ts["undo_point_threshold"]
and ts["num_undo_prompts"] > 0
):
if self.moves[-1].outdated_evaluation:
outdated_points_lost = (1 - self.moves[-1].outdated_evaluation) * self.moves[-1].points_lost / (1 - self.moves[-1].evaluation)
# so if the move was not that far off (>undo_outdated_eval_threshold) and according to last move's analysis it was fine, don't undo.
if (
self.moves[-1].outdated_evaluation
and (self.moves[-1].outdated_evaluation >= ts["undo_eval_threshold"] or outdated_points_lost < ts["undo_point_threshold"])
and (self.moves[-1].evaluation > ts["undo_outdated_eval_threshold"] or outdated_points_lost < ts["undo_point_threshold"])
):
self.controls.info.text += f"\nBut according to my previous evaluation it was {self.moves[-1].outdated_evaluation*100:.1f}% effective and lost {outdated_points_lost:.1f} point(s), so let's continue anyway.\n"
else:
if len(self.moves[-2].undos) < ts["num_undo_prompts"]:
self.controls.info.text += f"\nLet's try again.\n"
self.gtpplaycommand("undo")
return True
else:
evaled_moves = sorted([m for m in self.moves[-2].undos + [self.moves[-1]] if m.evaluation], key=lambda m: -m.evaluation)
if evaled_moves and evaled_moves[0].coords != self.moves[-1].coords:
self.gtpplaycommand("undo")
self.gtpplaycommand(evaled_moves[0])
summary = "\n".join(f"{m.gtp()}: {100*m.evaluation:.1f}% effective" for m in evaled_moves)
self.controls.info.text += f"\nYour moves:\n{summary}.\nLet's continue with {evaled_moves[0].gtp()}.\n"
return False
def _do_aimove(self, auto=False):
ts = self.train_settings
if not auto:
self.controls.info.text = "Thinking..."
self._evaluate_move(auto and not self.controls.auto_undo.active(1 - self.current_player()))
# select move
pos_moves = [(d["move"], float(d["scoreMean"]), d["evaluation"]) for d in self.moves[-1].analysis if int(d["visits"]) >= ts["balance_play_min_visits"]]
if ts["show_ai_options"]:
self.controls.info.text += "AI Options: " + " ".join([f"{move}({100*eval:.0f}%,{score:.1f}pt)" for move, score, eval in pos_moves])
selmove = pos_moves[0][0]
if self.controls.ai_balance.active and pos_moves[0][0] != "pass": # don't play suicidal to balance score - pass when it's best
selmoves = [
move
for move, score, eval in pos_moves
if eval > ts["balance_play_randomize_eval"] or eval > ts["balance_play_min_eval"] and score > ts["balance_play_target_score"]
]
selmove = random.choice(selmoves) # some kind of when further ahead play worse?
self.gtpplaycommand(Move(player=self.current_player(), gtpcoords=selmove, robot=True))
def _do_undo(self):
if self.controls.ai_auto.active and self.moves[-1].robot:
self.gtpplaycommand("undo")
if self.controls.ai_lock.active and self.controls.auto_undo.active(self.moves[-2].player) and len(self.moves[-2].undos) >= self.train_settings["num_undo_prompts"]:
self.controls.info.text = f"Can't undo more than {self.train_settings['num_undo_prompts']} time(s) when locked"
return
self.gtpplaycommand("undo")
def _do_init(self, boardsize, komi=None):
self.boardsize = boardsize
self.stop_analyzing = True
self.analysis_semaphore.acquire()
self.stones = []
self.moves = [Move(player=1, coords=(None, None))] # sentinel
self.controls.redraw(include_board=True)
self.gtpcommand(f"boardsize {boardsize}")
self.gtpcommand(f"komi {komi or self.komi}")
self.gtpcommand("clear_board")
self.ready = True
self.analysis_semaphore.release()
self.stop_analyzing = False
def _do_analyze_sgf(self, sgf):
self._do_init(self.boardsize, self.komi)
sgfmoves = re.findall(r"([BW])\[([a-z]{2})\]", sgf)
for move in [Move(player=Move.PLAYERS.index(p.upper()), sgfcoords=(mv, self.boardsize)) for p, mv in sgfmoves]:
while not self.moves[-1].analysis:
time.sleep(0.01)
self.analysis_semaphore.acquire() and self.analysis_semaphore.release() # wait for analysis to finish
self.gtpplaycommand(move)
self.controls.info.text = f"Analyzing move {move.gtp()}"
self.controls.info.text = "Analysis done!"
# analysis thread
def _analyze_thread(self, interval):
while True:
num_visits = self.visits[1 if self.controls.ai_fast.active else 0]
while self.stop_analyzing: # TODO: cleaner concurrency?
time.sleep(0.01)
self.analysis_semaphore.acquire()
for mode in [0, 1]: # pass, analyze
if self.stop_analyzing:
break
if mode == 0:
passmove = Move(player=self.current_player(), gtpcoords="pass")
if len(self.moves) > 2 and len({self.moves[-2].coords, self.moves[-1].coords} - {(x, y) for _, x, y in self.stones}) == 0: # nothing got captured
undo_mode = 0
self.raw_gtpplaycommand("undo")
self.raw_gtpplaycommand("undo")
self.raw_gtpplaycommand(passmove)
self.raw_gtpplaycommand(self.moves[-1])
self.raw_gtpplaycommand(self.moves[-2])
else:
undo_mode = 1
for coords in [(0, 0), (0, self.boardsize - 1), (self.boardsize - 1, 0), (self.boardsize - 1, self.boardsize - 1), (None, None)]:
if self.raw_gtpplaycommand(Move(player=self.current_player(), coords=coords)):
break
self.gtpwrite(f"kata-analyze interval {interval} minmoves 2 {'ownership true' if mode==1 else ''}")
self.kata.stdout.readline() # =
tot_visits = tot_nopass_visits = 0
while not self.stop_analyzing and (tot_visits < num_visits[mode] or tot_nopass_visits < self.min_nopass_visits):
line = self.kata.stdout.readline().decode()
line, *ownership = line.split("ownership")
moves = [re.sub("pv .*", "", str).split(" ") for str in line.split("info ")[1:]]
move_dicts = [{move[i]: move[i + 1] for i in range(0, len(move) - 1, 2)} for move in moves]
self.controls.update_analysis(move_dicts, mode, ownership)
tot_visits = sum([int(d["visits"]) for d in move_dicts], 0)
tot_nopass_visits = sum([int(d["visits"]) for d in move_dicts if d["move"] != "pass"], 0)
if self.debug:
print("mode=", mode, "visits=", tot_visits, "nopass=", tot_nopass_visits) # , "stop_analyzing?", stop_analyzing
self.gtpcommand("stop") # reads for analyze empty line
self.gtpread() # for stop line empty line
# for modes loop
if mode == 0: # undo A1
self.raw_gtpplaycommand("undo")
if undo_mode == 0:
self.raw_gtpplaycommand("undo")
self.raw_gtpplaycommand("undo")
self.raw_gtpplaycommand(self.moves[-2])
self.raw_gtpplaycommand(self.moves[-1])
else:
self.stop_analyzing = True # ehh
self.analysis_semaphore.release() # signal other threads waiting for analysis to finish
+252
View File
@@ -0,0 +1,252 @@
# Example config for C++ (non-python) gtp bot
# RUNNING ON AN ONLINE SERVER OR IN A REAL TOURNAMENT OR MATCH:
# If you plan to do so, you may want to read through the "Rules" section
# below carefully for proper handling of komi and handicap games and end-of-game cleanup
# and various other details.
# NOTES ABOUT PERFORMANCE AND MEMORY USAGE:
# You will likely want to tune one or more the following:
#
# numSearchThreads:
# The number of CPU threads to use. If your GPU is powerful, it can actually be much higher than
# the number of cores on your processor because you will need many threads to feed large enough
# batches to make good use of the GPU.
#
# nnMaxBatchSize:
# The maximum GPU batch size. Should often be at least as large as numSearchThreads.
# Larger won't do anything, but also won't hurt except use a little bit more GPU memory.
# Smaller can be fine if you have more than one GPU, since the GPUs will be sharing the work
# of servicing the CPU threads.
#
# cudaUseFP16 and cudaUseNHWC:
# These have a good chance of improving peformance at larger threads/batch sizes if
# you are using the CUDA implementation with an NVIDIA GPU with FP16 tensor cores.
#
# nnCacheSizePowerOfTwo:
# This controls the NN Cache size, which is the primary RAM/memory use.
# Each neural net entry takes very approximately 1.5KB, except when using whole-board
# ownership/territory visualizations, each entry will take very approximately 3KB.
# The number of entries is (2 ** nnCacheSizePowerOfTwo), for example 2 ** 18 = 262144.
# Increase this if you don't mind the memory use and want better performance
# for searches with tens of thousands of visits or more (due to birthday paradox
# it can start mattering well before cache actually fills entirely up).
# Decrease this if you want to limit memory usage.
#
# OTHER NOTES:
# If you have more than one GPU, take a look at "OpenCL GPU settings" or "CUDA GPU settings" below.
#
# If using OpenCL, you will want to verify that KataGo is picking up the correct device!
# (e.g. some systems may have both an Intel CPU OpenCL and GPU OpenCL, if KataGo appears to pick
# the wrong one, you correct this by specifying "openclGpuToUse" below).
#
# You may also want to adjust "maxVisits", "ponderingEnabled", "resignThreshold", and possibly
# other parameters depending on your intended usage.
# Logs------------------------------------------------------------------------------------
# Where to output log?
logFile = gtp.log
# Logging options
logAllGTPCommunication = true
logSearchInfo = true
logToStderr = false
# KataGo will display some info to stderr on GTP startup
# Uncomment this to suppress that and remain silent
# startupPrintMessageToStderr = false
# Chat some stuff to stderr, for use in things like malkovich chat to OGS.
# ogsChatToStderr = true
# Configure the maximum length of analysis printed out by lz-analyze and other places.
# Controls the number of moves after the first move in a variation.
# analysisPVLen = 9
# Report winrates for chat and analysis as (BLACK|WHITE|SIDETOMOVE).
# Default is SIDETOMOVE, which is what tools that use LZ probably also expect
# reportAnalysisWinratesAs = SIDETOMOVE
# Rules------------------------------------------------------------------------------------
# koRule = SIMPLE #Simple ko rules (triple ko = no result)
koRule = POSITIONAL #Positional superko
# koRule = SITUATIONAL #Situational superko
# koRule = SPIGHT #Spight superko - https://senseis.xmp.net/?SpightRules
scoringRule = AREA #Area scoring
# scoringRule = TERRITORY #Territory scoring (uses a sort of special computer-friendly territory ruleset)
multiStoneSuicideLegal = false #Is multiple-stone suicide legal? (Single-stone suicide is always illegal).
# Make the bot capture stones that are part of pass-alive territory
# This is necessary to get correct play under tromp-taylor rules since the bot otherwise assumes (and is trained under)
# a ruleset where those stones need not be captured. It obviously should NOT be enabled if playing under territory scoring.
cleanupBeforePass = false
# Uncomment this to make it so that if the game seems to be a handicap game, assume that white gets +1 point per
# black handicap stone. Some Go servers like OGS will silently give white such points without including it in the komi.
# whiteBonusPerHandicapStone = 1
# Resignation occurs if for at least resignConsecTurns in a row,
# the winLossUtility (which is on a [-1,1] scale) is below resignThreshold.
allowResignation = false
resignThreshold = -0.98
resignConsecTurns = 3
# 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 = false
# Search limits-----------------------------------------------------------------------------------
# If provided, limit maximum number of root visits per search to this much. (With tree reuse, visits do count earlier search)
maxVisits = 1000
# If provided, limit maximum number of new playouts per search to this much. (With tree reuse, playouts do not count earlier search)
# maxPlayouts = 1000
# If provided, cap search time at this many seconds (search will still try to follow GTP time controls)
# maxTime = 60
# Ponder on the opponent's turn?
ponderingEnabled = false
# Same limits but for ponder searches if pondering is enabled
# maxVisitsPondering = 1000
# maxPlayoutsPondering = 1000
# maxTimePondering = 60
# Number of seconds to buffer for lag for GTP time controls
lagBuffer = 1.0
# Number of threads to use in search
numSearchThreads = 1
# Play a little faster if the opponent is passing, for friendliness
searchFactorAfterOnePass = 0.50
searchFactorAfterTwoPass = 0.25
# Play a little faster if super-winning, for friendliess
searchFactorWhenWinning = 0.40
searchFactorWhenWinningThreshold = 0.95
# GPU Settings-------------------------------------------------------------------------------
# Maximum number of positions to send to GPU at once. Note that you will also need to increase numSearchThreads
# to make use of this, as every thread in KataGo is synchronous, so with 1 thread max batch will only be 1 anyways.
nnMaxBatchSize = 16
# Cache up to 2 ** this many neural net evaluations in case of transpositions in the tree.
nnCacheSizePowerOfTwo = 18
# Size of mutex pool for nnCache is 2 ** this
nnMutexPoolSizePowerOfTwo = 14
# Randomize board orientation when running neural net evals?
nnRandomize = true
# If provided, force usage of a specific seed for nnRandomize instead of randomizing
# nnRandSeed = abcdefg
# How many threads should there be to feed positions to the neural net?
# Server threads are indexed 0,1,...(n-1) for the purposes of the below GPU settings arguments
# that specify which threads should use which GPUs.
# NOTE: This parameter is probably ONLY useful if you have multiple GPUs, since each GPU will need a thread.
# If you're tuning single-GPU performance, use numSearchThreads instead.
numNNServerThreadsPerModel = 1
# CUDA GPU settings--------------------------------------
# These only apply when using CUDA as the backend for inference.
# (For GTP, we only ever have one model, when playing matches, we might have more than one, see match_example.cfg)
# Default behavior tries to guess the 'best' GPU or device
# You will want to uncomment and adjust one or more of these lines to take advantage of a multi-gpu machine
# cudaDeviceToUse = 0 #use device 0 for all server threads (numNNServerThreadsPerModel) unless otherwise specified per-model or per-thread-per-model
# cudaDeviceToUseModel0 = 3 #use device 3 for model 0 for all threads unless otherwise specified per-thread for this model
# cudaDeviceToUseModel1 = 2 #use device 2 for model 1 for all threads unless otherwise specified per-thread for this model
# cudaDeviceToUseModel0Thread0 = 3 #use device 3 for model 0, server thread 0
# cudaDeviceToUseModel0Thread1 = 2 #use device 2 for model 0, server thread 1
# Uncomment these on NVIDIA devices with FP16 tensor cores for probably a speedup, at the cost of introducing some precision loss in the nn calculation.
# cudaUseFP16 = true
# cudaUseNHWC = true
# OpenCL GPU settings--------------------------------------
# These only apply when using OpenCL as the backend for inference.
# (For GTP, we only ever have one model, when playing matches, we might have more than one, see match_example.cfg)
# Default behavior tries to guess the 'best' GPU or device
# You will want to uncomment and adjust one or more of these lines to take advantage of a multi-gpu machine
# openclDeviceToUse = 0 #use device 0 for all server threads (numNNServerThreadsPerModel) unless otherwise specified per-model or per-thread-per-model
# openclDeviceToUseModel0 = 3 #use device 3 for model 0 for all threads unless otherwise specified per-thread for this model
# openclDeviceToUseModel1 = 2 #use device 2 for model 1 for all threads unless otherwise specified per-thread for this model
# openclDeviceToUseModel0Thread0 = 3 #use device 3 for model 0, server thread 0
# openclDeviceToUseModel0Thread1 = 2 #use device 2 for model 0, server thread 1
# Uncomment to tune OpenCL for every board size separately, rather than only the largest possible size
# openclReTunePerBoardSize = true
# Search randomization------------------------------------------------------------------------------
# Note that multithreading can also introduce a significant amount of nondeterminism.
# If provided, force usage of a specific seed for various things in the search instead of randomizing
# searchRandSeed = hijklmn
# 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
# Use dirichlet noise for the root node policy?
rootNoiseEnabled = false
# Dirichlet noise alpha is set to this divided by number of legal moves. 10.83 produces an alpha of 0.03 on an empty 19x19 board.
rootDirichletNoiseTotalConcentration = 10.83
# Proportion of root policy that is noise
rootDirichletNoiseWeight = 0.25
# 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------------------------------------------------------------------------------
# Scales the utility of winning/losing
winLossUtilityFactor = 0.0
# Scales the utility for trying to maximize score
staticScoreUtilityFactor = 0.6
dynamicScoreUtilityFactor = 0.4
# Adjust dynamic score center this proportion of the way towards zero, capped at a reasonable amount.
dynamicScoreCenterZeroWeight = 0.20
# 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 = 2.0
# FPU reduction constant for mcts
fpuReductionMax = 0.2
# 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
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+223
View File
@@ -0,0 +1,223 @@
import random
import re
import shlex
import signal
import subprocess
import sys
import threading
DEBUG = True
class GoEngine:
GTP_COORD = "ABCDEFGHJKLMNOPQRSTUVWYXYZ"
SGF_COORD = [chr(i) for i in range(97, 123)]
def __init__(self, boardsize=19):
self.boardsize = boardsize
self.moves = []
self.stones = [] # TODO refactor stones vs moves distinction
self.komi = 7.5
self.turn = 0
def start(self, boardsize):
self.__init__(boardsize)
def play(self, coords, player=None, temp=False):
if not temp:
self.moves.append((player or self.turn, *(coords or [None, None]))) # pass is x=y=None
if not player:
self.turn = 1 - self.turn
def generate_move(self):
for _ in range(1000):
move = (random.randint(0, self.boardsize - 1), random.randint(0, self.boardsize - 1))
if move not in [(x, y) for _, x, y in self.stones]:
break
self.play(move)
def undo(self):
if self.moves:
self.moves.pop()
self.turn = 1 - self.turn
def gtp2ix(self, gtpmove):
if "pass" in gtpmove:
return (None, None)
return (GoEngine.GTP_COORD.index(gtpmove[0]), int(gtpmove[1:]) - 1)
def ix2gtp(self, coords):
if not coords:
return "pass"
return GoEngine.GTP_COORD[coords[0]] + str(coords[1] + 1)
def coord2sgf(self, pl, x, y):
if x is None:
return f"{'BW'[pl]}[]"
else:
return f"{'BW'[pl]}[{GoEngine.SGF_COORD[x]}{GoEngine.SGF_COORD[self.boardsize - y - 1]}]"
def sgf(self):
sgfmoves = [self.coord2sgf(pl, x, y) for pl, x, y in self.moves]
return f"(;GM[1]SZ[{self.boardsize}]KM[{self.komi}];" + ";".join(sgfmoves) + ")"
NEXT_BEST_PLAYOUTS = 1000
PASS_PLAYOUTS = 250
class KataEngine(GoEngine):
# CMD = "kg/cpp/katago gtp -model modelb6/model.txt.gz -config katagtp.cfg"
CMD = "../lizzie/katago/katago.exe gtp -model ../lizzie/katanetwork.gz -config katagtp.cfg"
def __init__(self, boardsize=19):
super().__init__(boardsize)
self.stones = []
self.temperature = 0
if getattr(self, "kata", None):
self.stop()
else:
signal.signal(signal.SIGINT, lambda *args: self.stop() and sys.exit(0))
self.lock = threading.Lock()
threading.Thread(target=self.create_pipe, daemon=True).start()
def create_pipe(self):
with self.lock: # prevent other commands until started
self.kata = subprocess.Popen(shlex.split(KataEngine.CMD), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
self._command(f"boardsize {self.boardsize}")
self.calc_temperature()
def stop(self):
if self.kata:
print("STOPPING KATA")
self.kata.terminate()
def start(self, boardsize):
self.__init__(boardsize)
def _read(self):
lines = []
while self.kata:
lines.append(self.kata.stdout.readline().decode())
if DEBUG:
print("READ", lines[-1].rstrip())
if lines[-1].strip() == "":
break
return lines[:-1]
def _write(self, cmd):
if DEBUG:
print("WRITE", cmd)
self.kata.stdin.write((cmd + "\n").encode("utf-8"))
self.kata.stdin.flush()
def _command(self, cmd):
self._write(cmd)
return self._read()
def _eq_command(self, cmd):
return [l for l in self._command(cmd) if "=" in l][0][1:].strip()
def current_player(self):
return "BW"[self.turn]
def generate_move(self):
with self.lock: # lock to ensure temp is done / hacky eh
coords = self.gtp2ix(self.best_analysis[0]["move"])
return self.play(coords)
def _play(self, coords, player=None):
self._command(f"play {player or self.current_player()} {self.ix2gtp(coords)}")
def play(self, coords, player=None):
with self.lock:
self._play(coords, player)
super().play(coords, player)
self.update_position()
best_score = float(self.best_analysis[0]["scoreMean"])
worst_score = -float(self.pass_analysis[0]["scoreMean"])
self.calc_temperature()
last_move_score = -float(self.best_analysis[0]["scoreMean"])
print("BEST", best_score, "WORST", worst_score, "LAST MOVE", last_move_score)
return (last_move_score - worst_score) / (best_score - worst_score)
def undo(self):
with self.lock:
super().undo()
self._command("undo")
self.update_position()
def showboard(self):
with self.lock:
output = self._command("showboard")
return [re.sub("[^\.ox]", "", l.lower()) for l in output[2:]]
def update_position(self):
print("UPDATING POSITION")
board = self.showboard()
self.stones = []
for y, line in enumerate(board[::-1]):
for x, st in enumerate(line):
if st != ".":
self.stones.append(("xo".index(st), x, y))
def analyze(self, nvisits=100, interval=10):
self._write(f"kata-analyze interval {interval} ownership true")
stopped = False
move_dicts = []
while self.kata:
line = self.kata.stdout.readline().decode()
if stopped and line.strip() == "":
self._read() # stop cause previous line break and then =, another double line break
break
elif "info" not in line:
continue
line, ownership = line.split("ownership")
moves = [re.sub("pv .*", "", str).split(" ") for str in line.split("info ")[1:]]
move_dicts = [{move[i]: move[i + 1] for i in range(0, len(move) - 1, 2)} for move in moves]
tot_visits = sum([int(d["visits"]) for d in move_dicts], 0)
if not stopped and tot_visits > nvisits:
stopped = True
self._write("stop")
print("analyzed", move_dicts)
return move_dicts # {d['move']: d for d in move_dict} #/by order?
def calc_temperature(self):
with self.lock:
self.best_analysis = self.analyze(NEXT_BEST_PLAYOUTS)
print("playing pass")
self._play((0, 0)) # pass does some weird things with pass being optimal
print("analyzing post pass")
self.pass_analysis = self.analyze(PASS_PLAYOUTS)
self._command("undo")
# score after best move - score after pass = temp, but score after pass is negated here bc opponent's perspective
self.temperature = float(self.best_analysis[0]["scoreMean"]) + float(self.pass_analysis[0]["scoreMean"])
return self.temperature
if __name__ == "__main__":
# https://github.com/lightvector/KataGo/issues/25
k = KataEngine()
# print(k.genmove("b"))
# print(k.showboard())
# print(k.genmove("w"))
# print(k.showboard())
# print(k.genmove("w"))
# print(k.showboard())
print(k.play((3, 3)))
print("TEMPERATURE:", k.temperature())
print(k.play((15, 3)))
print("TEMPERATURE:", k.temperature())
print(k.showboard())
print(k.analyze(1000))
print(k.play((0, 0)))
print("TEMPERATURE:", k.temperature())
print(k.showboard())
# k.stop()
# k.play('b','pass')
# md = k.analyze()
+288
View File
@@ -0,0 +1,288 @@
#:kivy 1.11.0
<StyledButton@Button>:
text_color: 0.95,0.95,0.95,1
button_color: 0.157,0.455,0.753,1
button_color_down: (*[c/2 for c in self.button_color[:3]],1)
margin: (2,1)
bold: True
font_size: self.size[1] * 0.4
radius: int(self.size[1]/3)
# dont set these
disabled_mask: (0.5,0.5,0.5,1) if self.disabled else (1,1,1,1)
color: self.disabled_mask if self.disabled else self.text_color
background_color: 0,0,0,0
background_normal: ''
face_color: [c*m for c,m in zip(self.button_color if root.state=='normal' else self.button_color_down,self.disabled_mask)]
canvas.before:
Color:
rgba: root.face_color or [0,0,0,0]
RoundedRectangle:
size: self.size[0]-2*self.margin[0],self.size[1]-2*self.margin[1]
pos: (self.pos[0]+self.margin[0],self.pos[1]+self.margin[1])
radius: (root.radius or 0,)
<DarkLabel@Label>:
color: (0.05,0.05,0.05,1)
<LargeLabel@DarkLabel>:
bold: True
<CheckBox>
color: (0.05,0.05,0.05,1)
<CheckBoxHintLabel@ButtonBehavior+DarkLabel>:
halign: 'center'
valign: 'center'
<CheckBoxHint>
orientation: 'vertical'
checkbox: checkbox
text: ''
default_active: False
CheckBoxHintLabel:
size_hint: 1,0.45
font_size: root.height * 0.2
text: root.text
on_press: if not checkbox.disabled: checkbox._do_press()
CheckBox:
size_hint: 1,0.55
id: checkbox
on_active: root.dispatch('on_active')
active: root.default_active
<BWCheckBoxHint>
black: black
white: white
orientation: 'vertical'
text: ''
default_active: False
CheckBoxHintLabel:
size_hint: 1,0.2
text: root.text
font_size: self.height
on_press: if not white.disabled: white._do_press(); black._do_press()
CheckBox:
size_hint: 1,0.4
id: black
active: root.default_active
on_active: root.dispatch('on_active')
CheckBox:
size_hint: 1,0.4
id: white
active: root.default_active
on_active: root.dispatch('on_active')
<CensorableLabel>
orientation: 'horizontal'
text: ''
label: ''
CheckBoxHintLabel:
size_hint: 0.6,1
text: root.label
font_size: self.height * 0.6
on_press: value.opacity = 1 if value.opacity==0 else 0
DarkLabel:
size_hint: 0.4,1
text: root.text
font_size: self.height * 0.6
id: value
bold: True
<Badukpan>:
size: self.parent.height, self.parent.height
engine: self.parent.controls
<EngineControls>
cols: 1
rows: 9
info: info
temperature: temperature
evaluation: evaluation
score: score
hints: hints
ownership: ownership
eval: eval
ai_balance: ai_balance
ai_auto: ai_auto
ai_fast: ai_fast
ai_lock: ai_lock
auto_undo: auto_undo
undo: undo
GridLayout:
cols: 7
rows: 1
size_hint: 1, 0.05
Label:
size_hint: 0.01, 1
BoxLayout:
orientation: 'vertical'
size_hint: 0.1, 0.5
Label:
size_hint: 1, 0.2
DarkLabel:
pos: self.parent.pos[0] + self.width * 0.1, self.parent.pos[1]
size_hint: 1, 0.4
text: 'B'
color: 0.95,0.95,0.95,1
font_size: min(self.height,self.width) * 0.6
halign: 'center'
valign: 'center'
bold: True
canvas.before:
Color:
rgba: 0.05,0.05,0.05,1
Ellipse:
pos: self.pos[0] + self.width/2 - min(self.height,self.width) * 0.48, self.pos[1] + self.height/2 - min(self.height,self.width) * 0.48
size: min(self.height,self.width) * 0.96, min(self.height,self.width) * 0.96
DarkLabel:
size_hint: 1, 0.4
text: 'W'
halign: 'center'
valign: 'center'
font_size: min(self.height,self.width) * 0.6
bold: True
canvas.after:
Color:
rgba: 0.05,0.05,0.05,1
Line:
circle: self.pos[0] + self.width/2, self.pos[1] + self.height/2, min(self.height,self.width)*0.45
width: 1.1
BWCheckBoxHint:
size_hint: 0.2, 0.5
id: eval
text: 'eval'
default_active: True
on_active: root.parent.board.redraw()
BWCheckBoxHint:
size_hint: 0.2, 0.5
id: hints
text: 'hints'
on_active: root.parent.board.redraw()
BWCheckBoxHint:
size_hint: 0.2, 0.5
id: auto_undo
text: 'undo'
on_active: root.parent.board.redraw()
CheckBoxHint:
size_hint: 0.2, 0.5
text: 'lock\nai'
id: ai_lock
on_active: self.checkbox.disabled = hints.black.disabled = hints.white.disabled = ai_move.disabled = auto_undo.black.disabled = auto_undo.white.disabled = ai_auto.checkbox.disabled = True
CheckBoxHint:
size_hint: 0.2, 0.5
id: ownership
text: 'show\nowner'
on_active: root.parent.board.redraw()
GridLayout:
cols: 4
rows: 1
size_hint: 1, 0.05
StyledButton:
id: ai_move
size_hint: 0.5, 0.5
text: 'AI Move'
on_press: root.action("aimove")
CheckBoxHint:
size_hint: 0.166, 0.5
text: 'auto\nmove'
id: ai_auto
default_active: False
CheckBoxHint:
size_hint: 0.166, 0.5
text: 'balance\nscore'
id: ai_balance
default_active: True
CheckBoxHint:
size_hint: 0.166, 0.5
text: 'fast'
id: ai_fast
default_active: True
GridLayout:
cols: 2
rows: 1
size_hint: 1, 0.05
StyledButton:
id: undo
text: 'Undo'
on_press: root.action("undo")
default_active: True
StyledButton:
text: 'Pass'
on_press: root.action("play",(None,None))
CensorableLabel:
id: temperature
size_hint: 1, 0.025
label: 'Temperature'
text: '...'
CensorableLabel:
id: evaluation
size_hint: 1, 0.025
label: 'Evaluation'
text: '...'
CensorableLabel:
id: score
size_hint: 1, 0.025
label: 'Score'
text: '...'
TextInput:
id: info
size_hint: 1, 0.2
valign: 'middle'
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.05
StyledButton:
text: 'Save'
size_hint: 0.37, 1
on_press: info.text = root.sgf()
StyledButton:
text: 'Analyze'
size_hint: 0.49, 1
margin: (0,1)
on_press: root.action("analyze-sgf",info.text)
GridLayout:
size_hint: 1, 0.05
cols: 4
rows: 1
LargeLabel:
size_hint: 0.3, 0.25
text: ' New\nGame'
font_size: 0.3*self.size[1]
StyledButton:
size_hint: 0.233, 1
text: '9'
margin: (1,1)
on_press: root.restart(9)
StyledButton:
size_hint: 0.233, 1
text: '13'
margin: (0,1)
on_press: root.restart(13)
StyledButton:
size_hint: 0.233, 1
text: '19'
margin: (1,1)
on_press: root.restart(19)
<KaTrainGui>:
board: board
controls: controls
canvas.before:
Color:
rgba: 0.95, 0.95, 0.95, 1
Rectangle:
pos: self.pos
size: self.size
Badukpan:
id: board
pos_hint: {"x":0, "top":0}
EngineControls:
id: controls
pos_hint: {"right":1, "top":1}
size_hint: (self.parent.width - self.parent.height)/self.parent.width, 1
+191
View File
@@ -0,0 +1,191 @@
from kivy.app import App
from kivy.graphics import *
from kivy.properties import NumericProperty, ObjectProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget
import math
from controller import Config
from move import Move
from kivyutils import *
# (;GM[1]SZ[9]KM[7.5]RU[JP];B[fe];W[de];B[ec];W[dc];B[eg];W[dg];B[dh];W[ed];B[fd];W[ef];B[ff];W[eb];B[fc];W[eh];B[fg];W[ch];B[ee];W[df];B[dd];W[cd];B[db];W[cc];B[cb];W[fb];B[gb];W[bb];B[ea];W[ca];B[fa];W[fh];B[gh];W[ba];B[fi];W[di];B[da];W[ed];B[bc];W[bd];B[dd];W[ei];B[gi];W[ed])
# (;GM[1]SZ[19]KM[7.5]RU[JP];B[qd];W[pp];B[cd];W[cp];B[ec];W[od];B[oc];W[nc];B[pc];W[nd];B[qf];W[jc];B[eq];W[do];B[hq];W[jq];B[cr];W[qn];B[cj];W[cl];B[nq];W[oq];B[np];W[lp];B[cg];W[nn];B[lr];W[kq];B[mo];W[kn];B[qi];W[mn];B[hc];W[qk];B[lc];W[je];B[jb];W[kb];B[kc];W[ib];B[jd];W[ja];B[lb];W[id];B[kd];W[ic];B[oj];W[pd];B[qe];W[qc];B[qb];W[rc];B[rb];W[pb];B[ob];W[nb];B[pa];W[lf];B[la];W[ma];B[mj];W[nk];B[ge];W[hd];B[hg];W[fc];B[fb];W[gc];B[ed];W[og];B[of];W[nf];B[pg];W[fq];B[fp];W[er];B[fr];W[dq];B[gq];W[dr];B[eo];W[en];B[fn];W[fm];B[gn];W[gm];B[dn];W[em];B[co];W[dp];B[hn];W[ep];B[fq];W[fj];B[jo];W[jn];B[jr];W[kr];B[hr];W[fo];B[js];W[ks];B[iq];W[io];B[ng];W[mg];B[oh];W[ne];B[hm];W[hl];B[fg];W[ip];B[go];W[gs];B[fs];W[ok];B[mh];W[lh];B[li];W[hp];B[im];W[hs];B[il];W[ir];B[ke];W[kf];B[gp];W[bk];B[kl];W[bj];B[lo];W[ko];B[ll];W[ml];B[rj];W[rk];B[kh];W[ci];B[lg];W[if];B[hk];W[ei];B[gi];W[bg];B[bh];W[ch];B[bf];W[dg];B[cf];W[df];B[pj];W[pk];B[sk];W[sl];B[sj];W[rl];B[gb];W[hb];B[gl];W[fl];B[gj];W[de];B[bi];W[ai];B[ag];W[dd];B[dc];W[ce];B[be];W[cc];B[bd];W[qj];B[ri];W[eh];B[lm];W[ln];B[jg];W[jf];B[mf];W[me];B[ig];W[mk];B[lk];W[nj];B[ni];W[eo];B[mm];W[nm];B[fd];W[gd];B[hf];W[ga];B[ea];W[jm];B[jl];W[pe];B[fh];W[fi];B[es];W[ds];B[fk];W[ek];B[gk];W[mb];B[eg];W[oa];B[na];W[fe];B[he];W[oa];B[pb];W[ee];B[cb];W[ie];B[ff];W[dh];B[pf];W[mg];B[ej];W[dj];B[kg];W[mf];B[in];W[jp];B[na];W[aj];B[ah];W[oa];B[pq];W[or];B[na];W[ha];B[oa];W[fa];B[eb];W[];B[gr];W[is];B[oe];W[ho];B[km];W[ef])
COLORS = Config.get("ui")["stones"]
GHOST_ALPHA = Config.get("ui")["ghost_alpha"]
class Badukpan(Widget):
def __init__(self, **kwargs):
super(Badukpan, self).__init__(**kwargs)
self.ghost_stone = []
self.gridpos = []
self.grid_size = 0
self.stone_size = 0
self.last_eval = 0
self.EVAL_COLORS = Config.get("ui")["eval_colors"]
self.EVAL_KNOTS = Config.get("ui")["eval_knots"]
self.EVAL_BOUNDS = Config.get("ui")["eval_bounds"]
# stone placement functions
def _find_closest(self, pos):
return sorted([(abs(p - pos), i) for i, p in enumerate(self.gridpos)])[0]
def on_touch_down(self, touch):
xd, xp = self._find_closest(touch.x)
yd, yp = self._find_closest(touch.y)
prevghost = self.ghost_stone
if self.engine.ready and max(yd, xd) < self.grid_size / 2 and (xp, yp) not in [(x, y) for _, x, y in self.engine.stones]:
self.ghost_stone = (xp, yp)
else:
self.ghost_stone = None
if prevghost != self.ghost_stone:
self.redraw()
def on_touch_move(self, touch): # on_motion on_touch_move
return self.on_touch_down(touch)
def on_touch_up(self, touch):
if self.ghost_stone:
self.engine.action("play", self.ghost_stone)
self.ghost_stone = None
self.redraw()
# drawing functions
def on_size(self, *args):
self.draw_board()
self.redraw()
def draw_stone(self, x, y, col, innercol=None, evalcol=None, evalsize=10.0):
draw_circle((self.gridpos[x], self.gridpos[y]), self.stone_size, col)
if evalcol:
evalsize = min(self.EVAL_BOUNDS[1], max(evalsize, self.EVAL_BOUNDS[0])) / self.EVAL_BOUNDS[1]
draw_circle((self.gridpos[x], self.gridpos[y]), math.sqrt(evalsize) * self.stone_size * 0.5, evalcol)
if innercol:
Color(*innercol)
Line(circle=(self.gridpos[x], self.gridpos[y], self.stone_size * 0.45 / 0.85), width=1.75)
def _eval_spectrum(self, score):
score = max(0, score)
for i in range(len(self.EVAL_KNOTS) - 1):
if self.EVAL_KNOTS[i] <= score < self.EVAL_KNOTS[i + 1]:
t = (score - self.EVAL_KNOTS[i]) / (self.EVAL_KNOTS[i + 1] - self.EVAL_KNOTS[i])
return [a + t * (b - a) for a, b in zip(self.EVAL_COLORS[i], self.EVAL_COLORS[i + 1])]
return self.EVAL_COLORS[-1]
def draw_board(self):
self.canvas.before.clear()
with self.canvas.before:
# board
sz = self.height
Color(*Config.get("ui")["board_color"])
board = Rectangle(pos=(0, 0), size=(sz, sz))
# grid lines
margin = Config.get("ui")["board_margin"]
self.grid_size = board.size[0] / (self.engine.boardsize - 1 + 1.5 * margin)
self.stone_size = self.grid_size * Config.get("ui")["stone_size"]
self.gridpos = [math.floor((margin + i) * self.grid_size + 0.5) for i in range(self.engine.boardsize)]
line_color = Config.get("ui")["line_color"]
Color(*line_color)
lo, hi = self.gridpos[0], self.gridpos[-1]
for i in range(self.engine.boardsize):
Line(points=[(self.gridpos[i], lo), (self.gridpos[i], hi)])
Line(points=[(lo, self.gridpos[i]), (hi, self.gridpos[i])])
# star points
star_point_pos = 3 if self.engine.boardsize <= 11 else 4
starpt_size = self.grid_size * Config.get("ui")["starpoint_size"]
for x in [star_point_pos - 1, self.engine.boardsize - star_point_pos, int(self.engine.boardsize / 2)]:
for y in [star_point_pos - 1, self.engine.boardsize - star_point_pos, int(self.engine.boardsize / 2)]:
draw_circle((self.gridpos[x], self.gridpos[y]), starpt_size, line_color)
# coordinates
Color(0.25, 0.25, 0.25)
for i in range(self.engine.boardsize):
draw_text(pos=(self.gridpos[i], lo / 2), text=Move.GTP_COORD[i], font_size=self.grid_size / 1.5)
draw_text(pos=(lo / 2, self.gridpos[i]), text=str(i + 1), font_size=self.grid_size / 1.5)
def redraw(self):
self.canvas.clear()
with self.canvas:
# stones
last_move = self.engine.moves[-1].coords
eval_map = {m.coords: (m.evaluation, m.previous_temperature) for m in self.engine.moves}
eval_on = [self.engine.eval.active(0), self.engine.eval.active(1)]
has_stone = {}
for i, (ci, x, y) in enumerate(self.engine.stones):
has_stone[(x, y)] = ci
eval, evalsize = eval_map.get((x, y), (None, None))
evalcol = self._eval_spectrum(eval) if eval_on[ci] and eval else None
inner = COLORS[1 - ci] if ((x, y) == last_move) else None
self.draw_stone(x, y, COLORS[ci], inner, evalcol, evalsize)
# ownership
ownership = self.engine.moves[-1].ownership
if self.engine.ownership.active and ownership:
rsz = self.grid_size * 0.2
ix = 0
cp = self.engine.current_player
for y in range(self.engine.boardsize - 1, -1, -1):
for x in range(self.engine.boardsize):
ix_owner = cp if ownership[ix] > 0 else 1 - cp
if ix_owner != (has_stone.get((x, y), -1)):
Color(*COLORS[ix_owner], abs(ownership[ix]))
Rectangle(pos=(self.gridpos[x] - rsz / 2, self.gridpos[y] - rsz / 2), size=(rsz, rsz))
ix = ix + 1
# undos
undo_coords = set()
alpha = Config.get("ui")["undo_alpha"]
for m in self.engine.moves[-1].undos:
if m.evaluation and m.coords[0] is not None:
undo_coords.add(m.coords)
evalcol = (*self._eval_spectrum(m.evaluation), alpha)
self.draw_stone(m.coords[0], m.coords[1], (*COLORS[m.player][:3], alpha), Config.get("ui")["undo_circle_col"], evalcol, self.EVAL_BOUNDS[1])
# hints
if self.engine.moves[-1].analysis and self.engine.hints.active(self.engine.current_player):
for d in self.engine.moves[-1].analysis:
move = Move(gtpcoords=d["move"], player=0)
c = [*self._eval_spectrum(d["evaluation"]), 0.5]
if move.coords[0] is not None and move.coords not in undo_coords:
self.draw_stone(move.coords[0], move.coords[1], c)
# hover next move ghost stone
if self.ghost_stone:
self.draw_stone(*self.ghost_stone, (*COLORS[self.engine.current_player], GHOST_ALPHA))
# pass circle
passed = len(self.engine.moves) > 1 and self.engine.moves[-1].gtp() == "pass"
if passed:
if len(self.engine.moves) > 2 and self.engine.moves[-2].gtp() == "pass":
text = "game\nend"
else:
text = "pass"
Color(0.45, 0.05, 0.45, 0.5)
center = self.gridpos[int(self.engine.boardsize / 2)]
Ellipse(pos=(center - self.grid_size * 1.5, center - self.grid_size * 1.5), size=(self.grid_size * 3, self.grid_size * 3))
Color(0.15, 0.15, 0.15)
draw_text(pos=(center, center), text=text, font_size=self.grid_size * 0.66, halign="center", outline_color=[0.95, 0.95, 0.95])
class KaTrainGui(FloatLayout):
pass
class KaTrainApp(App):
def build(self):
self.icon = "./icon.png"
self.gui = KaTrainGui()
return self.gui
def on_start(self):
self.gui.controls.restart()
if __name__ == "__main__":
KaTrainApp().run()
+41
View File
@@ -0,0 +1,41 @@
from kivy.core.text import Label as CoreLabel
from kivy.graphics import *
from kivy.uix.boxlayout import BoxLayout
class CheckBoxHint(BoxLayout):
__events__ = ("on_active",)
@property
def active(self):
return self.checkbox.active
def on_active(self, *args):
pass
class BWCheckBoxHint(BoxLayout):
__events__ = ("on_active",)
def active(self, player):
return [self.black, self.white][player].active
def on_active(self, *args):
pass
class CensorableLabel(BoxLayout):
@property
def text(self):
return self.value.text
def draw_text(pos, text, **kw):
label = CoreLabel(text=text, bold=True, **kw)
label.refresh()
Rectangle(texture=label.texture, pos=(pos[0] - label.texture.size[0] / 2, pos[1] - label.texture.size[1] / 2), size=label.texture.size)
def draw_circle(pos, r, col):
Color(*col)
Ellipse(pos=(pos[0] - r, pos[1] - r), size=(2 * r, 2 * r))
BIN
View File
Binary file not shown.
+82
View File
@@ -0,0 +1,82 @@
from kivy.storage.jsonstore import JsonStore
class Move:
GTP_COORD = "ABCDEFGHJKLMNOPQRSTUVWYXYZ"
PLAYERS = "BW"
SGF_COORD = [chr(i) for i in range(97, 123)]
def __init__(self, player, coords=None, gtpcoords=None, sgfcoords=None, robot=False):
self.player = player
self.robot = robot
self.coords = coords or (gtpcoords and self.gtp2ix(gtpcoords)) or self.sgf2ix(sgfcoords)
self.analysis = None
self.outdated_evaluation = None
self.pass_analysis = None
self.evaluation = None
self.ownership = None
self.points_lost = 0
self.previous_temperature = None
self.undos = []
self.comment = ""
def __repr__(self):
return f"{Move.PLAYERS[self.player]}{self.gtp()}"
def temperature(self):
if self.analysis:
best_score = float(self.analysis[0]["scoreMean"])
worst_score = -float(self.pass_analysis[0]["scoreMean"])
return best_score - worst_score
else:
return 0
def evaluate(self, previous_move):
best_score = float(previous_move.analysis[0]["scoreMean"])
worst_score = -float(previous_move.pass_analysis[0]["scoreMean"])
last_move_score = -float(self.analysis[0]["scoreMean"])
self.previous_temperature = best_score - worst_score
self.points_lost = best_score - last_move_score
prev_analysis_current_move = [d for d in previous_move.analysis if d["move"] == self.gtp()]
if abs(self.previous_temperature) > 0.5:
self.evaluation = (last_move_score - worst_score) / (best_score - worst_score)
self.move_options = [previous_move.analysis[0]["scoreMean"]]
else:
self.evaluation = None
if self.evaluation:
self.comment = f"Evaluation: {100*self.evaluation:.1f}%{' (AI Move)' if self.robot else ''}\n"
if prev_analysis_current_move:
self.outdated_evaluation = (prev_analysis_current_move[0]["scoreMean"] - worst_score) / (best_score - worst_score)
self.comment += f"(Was considered last move as: {100 * self.outdated_evaluation:.1f}%)\n"
else:
self.comment = "Temperature too low for evaluation\n"
self.comment += f"Estimate point loss: {self.points_lost:.1f}\n"
self.comment += f"Last move score was {last_move_score:.1f}\n"
self.comment += f"Score of top move was {previous_move.analysis[0]['scoreMean']:.1f} @ {previous_move.analysis[0]['move']}\n"
self.comment += f"Pass score was {worst_score:.1f}\n"
def gtp2ix(self, gtpmove):
if "pass" in gtpmove:
return (None, None)
return Move.GTP_COORD.index(gtpmove[0]), int(gtpmove[1:]) - 1
def sgf2ix(self, sgfmove_with_boardsize):
sgfmove, boardsize = sgfmove_with_boardsize
if sgfmove == "":
return (None, None)
return Move.SGF_COORD.index(sgfmove[0]), boardsize - Move.SGF_COORD.index(sgfmove[1]) - 1
def gtp(self):
if self.coords[0] is None:
return "pass"
return Move.GTP_COORD[self.coords[0]] + str(self.coords[1] + 1)
def sgfcoords(self, boardsize):
return f"{Move.SGF_COORD[self.coords[0]]}{Move.SGF_COORD[boardsize - self.coords[1] - 1]}"
def sgf(self, boardsize):
if self.coords[0] is None:
return f"{Move.PLAYERS[self.player]}[]"
else:
return f"{Move.PLAYERS[self.player]}[{self.sgfcoords(boardsize)}]"